Skip to content

fix: isolate think scratchpads by conversation - #4592

Open
aheritier wants to merge 2 commits into
mainfrom
fix/4588-think-conversation-isolation
Open

aheritier wants to merge 2 commits into
mainfrom
fix/4588-think-conversation-isolation

Conversation

@aheritier

Copy link
Copy Markdown
Collaborator

🤖 Automated implementer agent — this comment was posted by the implementer bot from Docker Agentic Platform, not by a human developer

Summary

Fixes #4588. Builds on merged #4570 and preserves its synchronization and sequential/64-way concurrency coverage.

  • Make think toolsets stateless and bind their scratchpads to the actual conversation ID, calling agent and configured/extra toolset ordinal through the host-owned runtime. Forward this binding through Code Mode.
  • Store thoughts atomically in the session store: mutex-protected in-memory state and SQLite migration 31 with ordered snapshots and root-owned foreign-key cleanup. Establish root persistence through the existing synchronous observer before starting the producer.
  • Resolve durable ancestry for resumed nested children and their delegated descendants, with explicit ownership/cancellation/store errors instead of silent resets.
  • Preserve Origin in Session.Clone() (pkg/session/branch.go:82) and assert it in the existing scalar-copy test (pkg/session/clone_test.go:23,55). This is the explicitly approved three-line, two-file micro-fix; the server workaround and Fork/Branch behavior are unchanged.
  • Add credential-free store, public-handler and real-runtime regressions for overlapping distinct conversations sharing the same team/raw think instance, same-ID reload/clone, new-ID forks, restart, delegation, Code Mode, deletion and observer ordering.

Approved design: #4588 (comment). No config/schema changes or cache fixes.

Retention and limitations

Output remains exactly "Thoughts:\n" + strings.Join(thoughts, "\n").

Thoughts survive turns, runtime closure and compaction. SQLite retains them across reloads/restarts; the in-memory store retains them for its lifetime. Same-ID clones share the durable scratchpad; new-ID branches/forks start empty. JSON export/import does not transport this state, and there is no legacy backfill.

Raw thoughts are stored before response-output redaction: session database and backup privacy policies apply. Deletion is logical, not secure erasure or backup removal. Root deletion cleans up all root-owned thoughts, including aborted/unpersisted children. Persisted-child deletion prunes known subtrees; never-persisted descendants beneath a deleted non-root child remain root-owned until root deletion. Stop writers before deletion.

Retention is lifecycle-bounded, not hard-size-bounded: no quota, TTL, eviction or output cap. Full output and storage grow with retained thoughts. Ordinal identity requires unchanged agent/toolset topology and ordering; no remapping/portability guarantees. Scoped persistence/ownership/unsupported-store failures are tool errors; only genuinely unscoped Nop hosts use an ephemeral single thought.

Validation

Final author validation on reviewed tree c852a05122fce07a5c023257ddfc488192a5f8b1:

CGO_ENABLED=1 task --force build
CGO_ENABLED=1 task test
CGO_ENABLED=1 task --force lint
CGO_ENABLED=1 go test -race -shuffle=on -count=100 -timeout=5m ./pkg/tools/builtin/think
CGO_ENABLED=1 go test -v -race -shuffle=on -count=20 ./pkg/tools/builtin/think ./pkg/runtime/toolexec
CGO_ENABLED=1 go test ./pkg/session -run Think -race -count=20 -shuffle=on
CGO_ENABLED=1 go test ./pkg/runtime -run Think -race -count=3 -shuffle=on
CGO_ENABLED=1 go test ./pkg/runtime ./pkg/session ./pkg/runtime/toolexec ./pkg/tools/builtin/think -race -count=3 -shuffle=on
CGO_ENABLED=1 go test ./pkg/runtime -run '^TestThink(ResumedChildDelegates|OverlappingConversationIsolation|RootIgnoresInheritedCandidate)$' -race -count=10 -shuffle=on
CGO_ENABLED=1 go test ./pkg/runtime -run '^TestThinkRetentionReloadCloneFork$' -race -count=1
CGO_ENABLED=1 go test ./pkg/session -run '^TestClone_CopiesScalarFields$' -race -count=20 -shuffle=on
GOOS=js GOARCH=wasm go build ./pkg/tools/builtin/think
GOOS=js GOARCH=wasm go test -exec="$(go env GOROOT)/lib/wasm/go_js_wasm_exec" ./pkg/tools/builtin/think
git diff --cached --check

All final commands PASS, no race reports; lint reports zero issues and clean project cops/tidy. Fresh independent review of the complete 16-file final tree APPROVE: all three earlier findings confirmed fixed. The reviewer independently ran canonical commands, repeated affected/targeted races, Node-backed think tests and task test-wasm-js. It also reproduced the real Clone regression on both stores through a read-only negative overlay removing only the Origin copy; no test-side compensation, skip or weakened assertion.

Historical disposition is retained: earlier canonical validation failed the real Clone regression and unchanged TestFileCache_dedupSkipsRedundantWrite (pkg/cache/cache_test.go:118); unchanged-base cache control failed 28/30 runs. The latest canonical cache result is CACHED PASS, not fresh proof the cache flake is absent or fixed. Earlier runtime tests excluding Clone were supplemental, not full validation. Full-repository race tests were NOT run; the affected-package races above are not described as full-repository validation.

This PR is intentionally draft for CI and follow-up. No merge requested.

Persist caller- and instance-scoped thoughts in the session store, bind tool execution to the durable conversation root, and cover reload, concurrent isolation, and cleanup. Preserve Origin in same-ID session clones so retained thoughts remain addressable.

Signed-off-by: Arnaud Héritier <aheritier@users.noreply.github.com>
@aheritier aheritier added area/docs Documentation changes area/runtime Runtime engine, agent loop execution, tool dispatch, loop detection area/sessions For features/issues/fixes related to session lifecycle (resume, persistence, export) area/tools For features/issues/fixes related to the usage of built-in and MCP tools kind/fix PR fixes a bug (maps to fix:). Use on PRs only. labels Oct 14, 2026
@aheritier
aheritier marked this pull request as ready for review October 14, 2026 08:38
@aheritier
aheritier requested a review from a team as a code owner October 14, 2026 08:38
Copilot AI balanced review requested due to automatic review settings October 14, 2026 08:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Root cleanup can leave orphaned thoughts when SQLite foreign keys are disabled, and in-memory ownership checks scale quadratically.

2 open findings
What changed in this PR

Isolates think scratchpads by conversation, agent, and toolset while adding durable session-store persistence.

Changes:

  • Moves scratchpad ownership from toolsets into atomic session stores.
  • Adds runtime lineage binding, Code Mode forwarding, and extensive regressions.
  • Preserves session origin during cloning and documents retention semantics.
File Description
pkg/​tools/​runtime.go Adds scratchpad runtime capability.
pkg/​tools/​builtin/​think/​think.go Makes think toolsets stateless.
pkg/​tools/​builtin/​think/​think_test.go Tests output, concurrency, and errors.
pkg/​session/​think_scratchpad.go Implements memory and SQLite storage.
pkg/​session/​think_scratchpad_test.go Tests persistence, deletion, and concurrency.
pkg/​session/​store.go Adds memory state and deletion cleanup.
pkg/​session/​migrations.go Adds scratchpad table migration.
pkg/​session/​migrations_pinned_test.go Updates migration digest.
pkg/​session/​clone_test.go Verifies origin cloning.
pkg/​session/​branch.go Preserves origin in clones.
pkg/​runtime/​toolexec/​dispatcher.go Forwards scratchpad appends.
pkg/​runtime/​toolexec/​dispatcher_test.go Tests dispatcher forwarding.
pkg/​runtime/​tool_dispatch.go Resolves scratchpad ownership and ancestry.
pkg/​runtime/​think_test.go Adds runtime integration regressions.
pkg/​runtime/​loop.go Persists roots before producers start.
docs/​tools/​think/​index.md Documents scope and retention.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/session/store.go Outdated
Comment thread pkg/session/think_scratchpad.go Outdated
Comment on lines +54 to +58
for owner, entries := range s.scratchpads {
if owner != rootID {
for existing := range entries {
if existing.SessionID == key.SessionID {
return nil, ErrScratchpadOwner

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated implementer agent — this comment was posted by the implementer bot from Docker Agentic Platform, not by a human developer

Fixed in d63148c. Ownership validation now uses a sessionID -> rootID map guarded by the existing scratchpad mutex, rather than scanning retained roots and keys. Successful appends register ownership; deletion prunes entries for deleted session IDs or owning roots.

TestThinkScratchpadOwnershipAcrossKeys, TestThinkScratchpadOwnershipDeletion, and TestThinkScratchpadRejectedAppendDoesNotClaimOwnership cover agent/toolset separation, ownership reuse and pruning, unrelated survival, and rejected/cancelled appends not claiming ownership. Repeated store and affected-package race tests passed, and fresh independent delta review approved the published tree. Constant-time ownership validation follows from the map lookup, not a timing assertion.

Signed-off-by: Arnaud Héritier <aheritier@users.noreply.github.com>
vx-daniel added a commit to vx-daniel/number-5 that referenced this pull request Oct 14, 2026
docker-agent runs script tools through the user's shell ($SHELL -c, or
PowerShell / cmd.exe on Windows) and passes their arguments as environment
variables. `cmd: issue-team-pr-threads "$pr"` therefore only worked under bash
and zsh, and the helpers themselves were bash.

- helpers/{base-ref,change-diff,pr-threads}.ts replace the bash scripts; each
  is started by bin/issue-team-<name> (sh) or bin/issue-team-<name>.cmd.
  base-ref shares the launcher's default-branch lookup instead of a copy.
- team.yaml script tools name the helpers bare; the helpers read `pr` and
  `since` from the command line or the environment.
- Equivalence with the bash versions, checked before replacing them:
  change-diff byte-identical in all five scope cases (and with `since` from
  the environment); pr-threads identical JSON on docker/docker-agent#4592,
  with keys now in GitHub's order (gh --jq sorted them alphabetically);
  base-ref identical.
- Prompts write PR bodies and comments to the system temp directory instead
  of /tmp.

Tests run the helpers directly and by bare name through sh, pwsh and cmd.exe,
with pr-threads against a fake gh.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
vx-daniel added a commit to vx-daniel/number-5 that referenced this pull request Oct 14, 2026
Windows CI stderr showed the real cause of the pr-threads failure: Bun refuses
to pass an argument containing cmd.exe special characters (the multi-line
GraphQL query) to a .cmd program, here the fake gh.cmd; the same would hit a
gh installed as a .cmd shim. The query now goes through -F query=@<temp file>.
Output on docker/docker-agent#4592 is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Documentation changes area/runtime Runtime engine, agent loop execution, tool dispatch, loop detection area/sessions For features/issues/fixes related to session lifecycle (resume, persistence, export) area/tools For features/issues/fixes related to the usage of built-in and MCP tools kind/fix PR fixes a bug (maps to fix:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Think scratchpad carries thoughts across conversations sharing a team

3 participants