Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
5531 lines
333 KiB
Markdown
5531 lines
333 KiB
Markdown
# Troubleshooting: Agent Sessions And Lifecycle
|
|
|
|
[Agent runtime index](./agent-runtime.md) · [All troubleshooting](./README.md)
|
|
|
|
### A newly opened Agent window shows processing but no transcript
|
|
|
|
- **Symptom:** A Session is already running, but opening it in another window
|
|
shows only the processing status and omits the submitted user message. The
|
|
original window may still show the complete conversation.
|
|
- **Quick checks:** Correlate the new renderer's selected Session with its first
|
|
canonical detail and message reads. If the message read fails or times out
|
|
while the Session projection still reports active work, the transcript is
|
|
temporarily unhydrated rather than deleted.
|
|
- **Root cause:** The focused Session reconcile reached a terminal transport
|
|
failure, but the synchronization lease did not observe that failure or
|
|
request recovery. The new window therefore retained only the live processing
|
|
projection.
|
|
- **Fix:** Preserve timeout as an explicit reconcile failure. While the Session
|
|
remains focused, report the failure and perform one bounded state-and-message
|
|
retry. Stop focus-owned recovery when the window releases the synchronization
|
|
lease.
|
|
- **Validation:** Make the first focused message hydration fail, then return a
|
|
canonical user message on the retry. Assert exactly two reads, one reported
|
|
initial failure, and a restored transcript. Separately cover the reducer's
|
|
timeout outcome so it remains observable.
|
|
- **References:**
|
|
[sessionReconcile.reducer.ts](../../../packages/agent/activity-core/src/engine/sessionReconcile.reducer.ts),
|
|
[workspaceAgentActivityReconcileBridge.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/workspaceAgentActivityReconcileBridge.ts)
|
|
|
|
### Claude cancellation reaches `context deadline exceeded`
|
|
|
|
- **Symptom:** Canceling a Claude Code Turn, closing its Session, or leaving a
|
|
room waits until the caller deadline and reports `context deadline exceeded`.
|
|
Repeating the action leaves the same Turn active.
|
|
- **Quick checks:** Filter daemon logs by `CLAUDE_CODE_CANCEL_DIAGNOSTIC`, then
|
|
group by `requestId`, `agentSessionId`, `turnId`, and `generationId`. Compare
|
|
`interrupt_started` with `interrupt_succeeded`, `interrupt_timed_out`, or
|
|
`interrupt_failed`; then require `query_close_succeeded` and either
|
|
`consumption_settled` or `consumption_timed_out`. The payload deliberately
|
|
excludes prompts, tool inputs, credentials, and provider output.
|
|
- **Root cause:** The SDK interrupt is a control request whose Promise has no
|
|
native timeout. A wedged Claude Code process may never return its control
|
|
response. A caller-side Go deadline only stops the RPC waiter and cannot
|
|
terminate that Promise or the provider process.
|
|
- **Fix:** Treat cancellation as a Query-lifecycle protocol. Revoke the exact
|
|
generation, bound cooperative interrupt, close the owned SDK transport when
|
|
ACK is missing or fails, and separately bound consumer drain before settling
|
|
canonical Turns. Do not increase only the outer RPC timeout.
|
|
- **Validation:** Cover an interrupt Promise that never settles, immediate
|
|
interrupt rejection, and a consumer Promise that never drains. The first two
|
|
must close transport and cancel the Turn; the last must return an explicit
|
|
bounded failure.
|
|
- **References:**
|
|
[queryGeneration.ts](../../../packages/agent/claude-sdk-sidecar/src/queryGeneration.ts),
|
|
[sessionRuntime.ts](../../../packages/agent/claude-sdk-sidecar/src/sessionRuntime.ts),
|
|
[claude_sdk_execution.go](../../../packages/agent/daemon/runtime/claude_sdk_execution.go)
|
|
|
|
### Claude Goal stays active after the model becomes idle
|
|
|
|
- **Symptom:** Claude has stopped producing output and the Session becomes
|
|
idle, but the Goal banner remains active indefinitely. The elapsed time may
|
|
also reset after leaving and reopening the workspace.
|
|
- **Quick checks:** Inspect sidecar events and the root Claude transcript. A
|
|
failed evaluator has an active `active_goal` or `goal_status.met=false`, a
|
|
root `session_state_changed(state=idle)`, and no later
|
|
`goal_status.met=true`.
|
|
- **Root cause:** Claude's SDK stream may omit top-level `goal_status`
|
|
attachments. A native evaluator timeout can then reach idle without a
|
|
terminal verdict. Separately, a timer based on React mount time cannot
|
|
survive workspace navigation.
|
|
- **Fix:** Tail only new root transcript rows, drain once more at provider idle,
|
|
and transition a still-active Goal to canonical `blocked`. Persist one
|
|
`startedAtUnixMs` per Goal generation and derive active elapsed time from it.
|
|
- **Validation:** Cover both boundaries: terminal transcript evidence written
|
|
immediately before idle must win, while idle without terminal evidence must
|
|
emit `blocked`. Unmount and remount the Goal banner with the same canonical
|
|
start and require the elapsed value not to reset.
|
|
- **References:**
|
|
[goalProjection.ts](../../../packages/agent/claude-sdk-sidecar/src/goalProjection.ts),
|
|
[messageRouter.ts](../../../packages/agent/claude-sdk-sidecar/src/messageRouter.ts),
|
|
[AgentGoalBanner.tsx](../../../packages/agent/gui/agent-gui/agentGuiNode/AgentGoalBanner.tsx)
|
|
|
|
### Older extension session fails because its launch identity is incomplete
|
|
|
|
- **Symptom:** Sending a new message to a previously created extension session
|
|
fails with `session runtime snapshot is unavailable: launch identity is
|
|
incomplete`, while a newly created session can still launch.
|
|
- **Quick checks:** Compare the persisted Session provider with
|
|
`internal_runtime_context.sessionRuntimeSnapshot.provider`. The historical
|
|
defect has an open extension provider such as `acp:kimi-code` on the Session,
|
|
an empty provider in the snapshot, and a provider-native fingerprint produced
|
|
with the same empty provider. An intermediate build may retain the provider
|
|
field while still carrying that legacy empty-provider fingerprint.
|
|
- **Root cause:** Registered-only provider normalization was used when writing
|
|
the snapshot and its provider-native model fingerprint. Extension-owned ACP
|
|
provider IDs were therefore collapsed to empty even though the durable
|
|
Session retained the correct provider.
|
|
- **Fix:** Persist and fingerprint provider-native configurations with open
|
|
provider normalization. For existing records, recover only when the Session
|
|
carries a valid extension-owned provider and the snapshot exactly matches the
|
|
legacy empty-provider fingerprint; keep malformed, registered-provider, plan,
|
|
and fingerprint-mismatched snapshots fail-closed.
|
|
- **Validation:** Prove a newly written extension snapshot binds its provider,
|
|
the exact historical shape resumes through its current enabled Agent Target,
|
|
a strict context-only read still rejects the incomplete identity, and altered
|
|
fingerprints or registered-provider fallbacks remain unavailable.
|
|
- **References:**
|
|
[session_runtime_snapshot.go](../../../services/tuttid/service/agent/session_runtime_snapshot.go),
|
|
[model_plan_binding.go](../../../services/tuttid/service/agent/model_plan_binding.go)
|
|
|
|
### Initial Goal session remains unnamed
|
|
|
|
- Symptom:
|
|
A new session created from `/goal <objective>` keeps an empty canonical title
|
|
and the UI shows the localized unnamed-conversation label after the optimistic
|
|
activation disappears.
|
|
- Quick checks:
|
|
Read `workspace_agent_sessions.title` and the first
|
|
`workspace_agent_messages` row. The affected shape has `kind=session_audit`,
|
|
`role=user`, `goalControl=true`, and no normal text Turn.
|
|
- Root cause:
|
|
Typed initial Goal intentionally skips the ordinary initial Turn. Title
|
|
derivation existed only in the normal initial-content `Exec` path, so the
|
|
session was persisted without a title even though the Goal audit contained the
|
|
submitted command.
|
|
- Fix:
|
|
Derive the title in Host before provider startup from `InitialDisplayPrompt`;
|
|
when that field is absent, synthesize `/goal <objective>` or `/goal <action>`.
|
|
Mark the derived title established so later runtime state cannot replace it.
|
|
- Validation:
|
|
Run the shared initial-Goal conformance scenario through both direct Host and
|
|
service-adapter drivers. Assert the returned canonical title and verify the
|
|
zero-Turn Goal operation still replays without a second provider startup.
|
|
- References:
|
|
[packages/agent/host/README.md](../../../packages/agent/host/README.md)
|
|
[lifecycle.go](../../../packages/agent/host/lifecycle.go)
|
|
[session_lifecycle_scenarios.go](../../../packages/agent/host/conformance/session_lifecycle_scenarios.go)
|
|
|
|
### Goal-control row has no copy action
|
|
|
|
- Symptom:
|
|
A visible `/goal ...` user bubble has no copy button on hover, while ordinary
|
|
user text bubbles have one.
|
|
- Quick checks:
|
|
Inspect the row kind. `goal-control` renders through `AgentGoalControlRow`,
|
|
not `AgentMessageBlock`; search direct `AgentRichTextReadonly` uses for a
|
|
renderer that bypasses `AgentCopyableMessageGroup`.
|
|
- Root cause:
|
|
The special row reused the read-only rich-text renderer but not the shared
|
|
message action wrapper.
|
|
- Fix:
|
|
Wrap the row in `AgentCopyableMessageGroup` and write through the host
|
|
clipboard, with the browser clipboard as the renderer fallback.
|
|
- Validation:
|
|
Render a durable goal-control row, hover it, assert the copy button exists,
|
|
click it, and verify the exact command body is written.
|
|
- References:
|
|
[AgentGoalControlRow.tsx](../../../packages/agent/gui/shared/agentConversation/components/AgentGoalControlRow.tsx)
|
|
[AgentMessageActions.tsx](../../../packages/agent/gui/shared/agentConversation/components/AgentMessageActions.tsx)
|
|
|
|
### Replay startup reports only `building isolated tuttid`
|
|
|
|
- **Symptom:** Replay fails before the isolated Workspace becomes ready, but
|
|
the toast shows only the runner's first progress line instead of the daemon
|
|
startup failure.
|
|
- **Quick checks:** Inspect the managed Replay failure status. New builds store
|
|
`errorCause.code` and `errorCause.message`; the outer `errorMessage` remains
|
|
the supervision summary.
|
|
- **Root cause:** The former UI formatter selected the first line from one
|
|
combined process-log string. The actionable daemon stderr appeared on a
|
|
later line and was discarded.
|
|
- **Fix:** Preserve daemon startup stderr as a structured `{code, message}`
|
|
cause across Desktop startup, the Replay runner event, and primary Desktop
|
|
status. Render the structured cause before the outer summary.
|
|
- **Validation:** Make isolated `tuttid` reject a Cassette during startup and
|
|
require the toast to show the daemon rejection while logs retain both the
|
|
outer startup error and structured cause.
|
|
|
|
### Replay Workspace stays on the starting toast
|
|
|
|
- **Symptom:** The source Desktop keeps showing the Replay Workspace starting
|
|
toast. The isolated Desktop may appear only after the daemon startup timeout.
|
|
- **Quick checks:** Inspect the isolated runtime under
|
|
`.tmp/agent-session-replay-workspace-*`. The listener must appear at
|
|
`state/run/tuttid.listener.json`; inspect `logs/desktop.log` and the managed
|
|
child output before debugging renderer readiness.
|
|
- **Root cause:** The runner waited for the obsolete
|
|
`state/cassette/tuttid.listener.json` path after the daemon moved its listener
|
|
metadata into the canonical runtime `run` directory.
|
|
- **Fix:** Resolve Replay daemon discovery through
|
|
`state/run/tuttid.listener.json`, matching every other managed daemon
|
|
consumer. Keep the path in one runner helper and cover it directly.
|
|
- **Validation:** Launch a managed Replay Workspace and prove its isolated
|
|
daemon becomes discoverable without waiting for the startup timeout.
|
|
|
|
### Replay fails because Desktop and tuttid use different event catalogs
|
|
|
|
- **Symptom:** Replay stops during Desktop startup with
|
|
`desktop/tuttid event stream catalog mismatch (fail-fast)`, or the isolated
|
|
Desktop log contains `Event stream catalog revision mismatch`.
|
|
- **Quick checks:** Compare the revisions reported by the runner for the
|
|
prepared `apps/desktop/out` bundle, the event-protocol source, and the
|
|
`tuttid` binary. A prepared Electron bundle can be stale even when the
|
|
current source checkout and daemon build match.
|
|
- **Root cause:** The Desktop renderer and daemon perform a strict event-stream
|
|
catalog handshake. Reusing a prepared renderer or daemon from a different
|
|
generated-protocol revision makes the handshake fail before Replay can drive
|
|
a Session.
|
|
- **Fix:** For an unmanaged launch, the runner falls back to
|
|
`pnpm-dev-desktop` when the current event-protocol source matches `tuttid`.
|
|
For a managed launch, rebuild `apps/desktop/out` and `tuttid` from the same
|
|
checkout, or clear the prepared Electron environment before retrying.
|
|
- **Validation:** Confirm the runner reports matching revisions before launch,
|
|
then run the Replay scenario and require the first checkpoint to become
|
|
ready without a catalog mismatch in `desktop.log`.
|
|
- **References:**
|
|
[event-stream-catalog.mjs](../../../tools/scripts/agent-session-replay-runner/event-stream-catalog.mjs),
|
|
[agent-session-replay.md](../../../docs/architecture/agent-session-replay.md)
|
|
|
|
### Replay starts but the first Turn never becomes idle
|
|
|
|
- **Symptom:** The isolated Replay Workspace is ready, but playback waits on the
|
|
first Session until transport verification reports unconsumed connections.
|
|
- **Quick checks:** Query the Cassette-scoped transport playback state and
|
|
verification endpoint. If the provider tape still has outbound frames while
|
|
the current UI no longer emits the recorded probe sequence, the Cassette is
|
|
incompatible with the current strict transport contract.
|
|
- **Root cause:** Session Replay intentionally validates provider traffic in
|
|
order. Benign-looking UI probe or request-order changes can therefore make an
|
|
older Cassette unable to reach its recorded completion frame.
|
|
- **Fix:** Record a new Cassette with the current architecture. Do not weaken
|
|
transport ordering or add a compatibility fallback unless the product
|
|
contract explicitly changes.
|
|
- **Validation:** The new Cassette must consume every recorded provider
|
|
connection, reach the expected stable checkpoints, and pass final transport
|
|
verification.
|
|
|
|
### Replay checkpoint waits forever with `observation:null` while the session detail is visible
|
|
|
|
- **Symptom:** Replay playback dispatches intents and streams provider frames
|
|
normally, but a checkpoint wait stalls on
|
|
`{"ready":false,"observation":null,"hasDetail":true}` and the coordinator
|
|
binding reports `mounted:true, selectedAgentSessionId:null,
|
|
detailHydrated:false`.
|
|
- **Quick checks:** Enumerate `[data-workbench-window-id]` elements over CDP.
|
|
If two AgentGUI nodes exist — one showing the replayed session and one empty —
|
|
the cassette binding is attached to the wrong node.
|
|
- **Root cause:** Something launched an AgentGUI surface before the Replay
|
|
Workspace bootstrap (for example a dock-launch preparation step). The replay
|
|
driver then interacts with the pre-launched node through the DOM while the
|
|
canonical-observation binding waits on the coordinator-launched node, which
|
|
never selects a session.
|
|
- **Fix:** In replay mode, never open AgentGUI surfaces outside
|
|
`bridge.bootstrap`. The coordinator-launched node must be the only AgentGUI
|
|
node; the runner waits for its composer after bootstrap instead of
|
|
dock-launching first.
|
|
- **Validation:** During replay, exactly one AgentGUI node exists, its binding
|
|
reports the selected session, and the checkpoint wait resolves within the
|
|
recorded timing.
|
|
|
|
### Replay fails because a recorded SQLite column does not exist
|
|
|
|
- **Symptom:** Replay startup fails with an error such as
|
|
`table tutti_mode_turn_snapshots has no column named speed`.
|
|
- **Quick checks:** Confirm the Cassette schema. Schema v5 contains
|
|
`initial-state.json`/`expected-state.json`; it never contains table rows or
|
|
SQL column names.
|
|
- **Root cause:** An obsolete row-fixture experiment recorded `SELECT *`
|
|
output from a database migrated by a different checkout. Import coupled
|
|
Replay to that checkout's physical SQLite schema.
|
|
- **Fix:** Re-record with the current schema. Do not add column projection, a
|
|
legacy reader, or a migration fallback. The daemon restores typed semantic
|
|
state before Host recovery.
|
|
- **Validation:** Add or reorder an unrelated SQLite column and prove semantic
|
|
capture bytes and Replay behavior are unchanged.
|
|
|
|
### Managed Replay leaves an orphan Desktop that crashes with `EPIPE`
|
|
|
|
- **Symptom:** Closing or replacing a managed Replay run leaves an isolated
|
|
Electron process behind. It later shows an Electron main-process JavaScript
|
|
error with `Error: write EPIPE` from the desktop log sink.
|
|
- **Quick checks:** Inspect the Electron process parent PID and open files.
|
|
`PPID=1`, a Replay workspace log under
|
|
`.tmp/agent-session-replay-workspace-*`, and disconnected stdout/stderr
|
|
identify an orphaned managed Replay process. Map the built stack location
|
|
back to `apps/desktop/src/main/logging.ts` before attributing the crash to
|
|
cassette data or renderer code.
|
|
- **Root cause:** The multi-cassette managed Replay path launched a detached
|
|
Desktop with piped output but did not bind the existing managed shutdown
|
|
lifecycle. When the owner exited, Electron and its managed daemon survived
|
|
while their output pipe had no reader. A later log write emitted an
|
|
unhandled `EPIPE`.
|
|
- **Fix:** Bind every managed Replay Desktop to the original owner PID and stop
|
|
its process tree when that owner disappears or its output pipe breaks. Treat
|
|
desktop and forwarded daemon console output as best effort so a closed
|
|
diagnostic pipe cannot crash the Electron main process.
|
|
- **Validation:** Prove the shutdown binding stops Desktop once when the owner
|
|
is gone, coalesces a concurrent `EPIPE`, and removes its listeners on
|
|
disposal. Exercise the process log sink with a writable stream that emits
|
|
`EPIPE`, then verify later writes are ignored without an uncaught exception.
|
|
- **References:**
|
|
[run-agent-session-replay.mjs](../../../tools/scripts/run-agent-session-replay.mjs),
|
|
[logging.ts](../../../apps/desktop/src/main/logging.ts),
|
|
[tuttidManager.ts](../../../apps/desktop/src/main/daemon/tuttidManager.ts)
|
|
|
|
### Replay shows the user prompt but never releases assistant output
|
|
|
|
- **Symptom:** Replay creates the Session and persists the user prompt, but the
|
|
provider tape stops before `turn/started` or assistant notifications.
|
|
- **Quick checks:** Compare the next outbound provider frame with daemon RPC
|
|
logs. If the tape expects `thread/read` or `thread/goal/get`, check whether
|
|
that observer query was issued in this renderer lifecycle. Also compare
|
|
recorded and replay JSON-RPC request IDs separately from method and params.
|
|
- **Root cause:** Observer-only provider queries and their auxiliary probe
|
|
connections are timing-dependent. Treating them as causal byte-stream frames
|
|
deadlocks replay when the current renderer observes at a different moment.
|
|
Earlier skipped queries can also shift later request IDs.
|
|
- **Fix:** Keep causal method/params matching strict, map recorded request IDs
|
|
to replay request IDs, and allow only the declared observer RPCs and
|
|
query-only auxiliary connections to be absent. Suppress only their paired
|
|
responses; preserve interleaved provider notifications. If a causal
|
|
provider response arrives while replay is waiting briefly for a declared
|
|
observer RPC, skip that observer immediately and match the causal response
|
|
against the next recorded frame.
|
|
- **Validation:** Replay a real three-Turn Cassette through a fresh Replay
|
|
Workspace, verify every checkpoint and assistant response, require transport
|
|
exhaustion, and compare final durable state.
|
|
|
|
### One hung provider startup blocks unrelated Agent sessions
|
|
|
|
- **Symptom:** One provider process starts but never reaches runtime-ready, then
|
|
new or resumed Sessions for other providers also wait without starting their
|
|
own provider processes.
|
|
- **Quick checks:** Order `agent_session.process.start.sent` with Host
|
|
`runtime_started` and `runtime_session_ready` lifecycle steps. If later,
|
|
unrelated Sessions accumulate wait time before their own process-start event,
|
|
inspect Controller startup-lock ownership separately from the provider that
|
|
first stopped making progress.
|
|
- **Root cause:** The runtime Controller held one process-wide `startMu` across
|
|
`Adapter.Start` and `Adapter.Resume`. A slow or unbounded provider handshake
|
|
therefore formed a lock convoy across every room, Session, and provider.
|
|
- **Fix:** Serialize explicit startup requests by `(roomID, agentSessionID)`.
|
|
Keep the legacy no-ID start path idempotent with a narrower room-and-provider
|
|
key, and let startup-lock waiters return when their context is canceled.
|
|
Provider credential coordination remains a separate Host gate.
|
|
- **Validation:** Block each of `Start` and `Resume` for one provider and prove
|
|
both operations still complete for an independent provider. Also verify all
|
|
same-Session combinations remain serialized, anonymous start replay reuses
|
|
one Session, canceled waiters release their lock references, and repeated
|
|
shuffled race runs stay clean.
|
|
- **References:**
|
|
[controller_session_lifecycle.go](../../../packages/agent/daemon/runtime/controller_session_lifecycle.go),
|
|
[controller_test.go](../../../packages/agent/daemon/runtime/controller_test.go)
|
|
|
|
### A shared-device connection banner looks terminal while the host is still retrying
|
|
|
|
- **Symptom:** AgentGUI keeps showing a strong connection-lost notice for a
|
|
shared device, so the user cannot tell whether recovery is still running.
|
|
- **Quick checks:** Inspect host logs for
|
|
`shared-agent-query.caller.connection.retry`,
|
|
`shared-agent-query.caller.connection.dormant`, and
|
|
`shared-agent-query.caller.connection.dormant-summary`. Correlate them with
|
|
`desktop.device_link.diagnostic` when distinguishing direct, rendezvous, and
|
|
relay transport attempts. A dormant state means the host has moved from
|
|
short retry delays to low-frequency recovery; it is not a terminal failure.
|
|
- **Root cause:** The query channel correctly blocked runtime-dependent
|
|
commands, but its recoverable unavailable state was projected directly as a
|
|
terminal target-connection error. The target event also omitted retry
|
|
progress, so an unchanged banner made periodic recovery invisible.
|
|
- **Fix:** Keep Session runtime availability as the command-safety gate.
|
|
Separately project target-scoped connection status with a retry attempt.
|
|
Present recoverable unavailable states as neutral reconnecting progress,
|
|
reserve the strong unavailable notice for explicitly non-retryable failures,
|
|
and remove the notice silently after recovery.
|
|
- **Validation:** Force the query channel into dormant recovery and verify each
|
|
failed probe increments the target retry attempt while the composer remains
|
|
blocked. Confirm the reconnecting notice updates without restarting its
|
|
visibility delay, a non-retryable failure is still immediate, and recovery
|
|
produces no success banner.
|
|
- **References:**
|
|
[agent-gui-node.md](../../architecture/agent-gui-node.md)
|
|
|
|
### Deleting an Agent Session leaves a Tutti plan active or emits duplicate removal events
|
|
|
|
- **Symptom:** Deleting one Session removes the conversation but its pending
|
|
Tutti Mode Plan remains visible, or clients receive duplicate/missing
|
|
`session_deleted` and `workspace.workflow.updated` invalidations. A workspace
|
|
clear may leave orphan workflows in an active state.
|
|
- **Quick checks:** Confirm production wiring assigns the Tutti Mode Plan
|
|
service as the Agent service's source-session deletion coordinator. Inspect
|
|
the transaction result for the complete removed Session closure and affected
|
|
workflow/current-checkpoint identities. Verify workflow events are published
|
|
only after the transaction returns and that the activity projection is not
|
|
also running its persistence deleter on the coordinator path.
|
|
- **Root cause:** Session persistence, activation cleanup, and workflow
|
|
cancellation form one cross-aggregate use case. Letting
|
|
`data/workspace` choose cancellable states hides business policy in the data
|
|
layer and cannot publish the canonical post-commit workflow event. Calling a
|
|
service coordinator and the legacy projection deleter together instead
|
|
splits the transaction and duplicates `session_deleted` events.
|
|
- **Fix:** Keep cancellation policy in `service/tuttimodeplan`: allowed source
|
|
states, canceled targets, actor, reason, and timestamp are all explicit. Pass
|
|
one command to the workspace store so Session closure, Tutti activation/Turn
|
|
snapshots, and workflow transitions commit or roll back together. Return
|
|
affected identities, then publish workflow and Session invalidations from
|
|
the respective services after commit. Reserve persistence-only deletion and
|
|
standalone activation cleanup for isolated tests or legacy orphan fallback.
|
|
- **Validation:** Force the workflow update to fail and assert the Session and
|
|
activation still exist. Cover single, descendant batch, runtime-only, and
|
|
workspace-clear scopes; exact cancellation-state selection; no event on
|
|
rollback; one workflow event per reported change; and one
|
|
`session_deleted` event per unique removed Session.
|
|
- **References:**
|
|
[workspace-workflows.md](../../architecture/workspace-workflows.md),
|
|
[source_session_deletion.go](../../../services/tuttid/service/tuttimodeplan/source_session_deletion.go),
|
|
[source_session_deletion.go](../../../services/tuttid/data/workspace/source_session_deletion.go)
|
|
|
|
### Tutti badge is active but no Tutti Mode Plan panel opens
|
|
|
|
- **Symptom:** The composer shows the Tutti badge and the Agent may even emit a
|
|
provider-native Plan, but no Tutti Mode Plan review panel appears.
|
|
- **Quick checks:** Read
|
|
`GET /v1/workspaces/{workspaceID}/agent-sessions/{agentSessionID}/tutti-mode-activation`
|
|
and confirm the current revision is active. Then inspect whether the Agent
|
|
actually invoked `tutti plan propose` with a valid absolute Markdown file
|
|
and Agent Session context. Query
|
|
`GET /v1/workspaces/{workspaceID}/workflows?sourceSessionId=...` for a pending
|
|
authoritative snapshot. Do not inspect transcript markers or provider Plan
|
|
card or historical `capabilityRefs` as current activation or workflow
|
|
evidence.
|
|
- **Root cause:** `TuttiModeActivation` and `WorkspaceWorkflow` are independent
|
|
Tutti-owned roots. The badge records a user preference that is snapshotted
|
|
into each new Turn's Host Context; it does not fabricate a proposal. Durable
|
|
review state begins only when the Agent invokes the always-available Tutti
|
|
CLI and commits a `WorkspaceWorkflow`, immutable revision, and pending
|
|
checkpoint. A missing CLI invocation, invalid `tutti-mode-plan/v1` document,
|
|
wrong source Session, or disconnected desktop workflow runtime leaves
|
|
nothing recoverable to render.
|
|
- **Fix:** Keep `/plan` and `/tutti` as compatible independent modifiers. Let
|
|
AgentSessionEngine project the daemon activation and let the Agent freely
|
|
invoke the Tutti CLI. AgentGUI must list workflows by active source Session
|
|
and re-pull after `workspace.workflow.updated`; it must never synthesize a
|
|
panel from Agent text, a provider Plan card, the Tutti badge, or capability
|
|
audit records. User decisions go only through the checkpoint HTTP endpoint.
|
|
- **Validation:** Cover activation create/update/reload, immutable per-Turn
|
|
snapshot binding before provider dispatch, CLI proposal persistence,
|
|
immutable revision verification, Session-scoped list recovery,
|
|
advisory-event refresh, accept/reject/cancel decisions, and Plan plus Tutti
|
|
badge coexistence. Verify an accepted task graph materializes exactly one
|
|
Issue and reports `issue_created` rather than asking the Agent to create it
|
|
again.
|
|
- **References:**
|
|
[workspace-workflows.md](../../architecture/workspace-workflows.md),
|
|
[commands.go](../../../services/tuttid/service/cli/providers/tuttimodeplan/commands.go),
|
|
[useTuttiModePlanPanels.ts](../../../packages/agent/gui/workspaceWorkflow/tuttiModePlan/useTuttiModePlanPanels.ts)
|
|
|
|
### Existing Session shows Tutti active but the Agent reports Default mode
|
|
|
|
- **Symptom:** Tutti Mode is enabled on an already-existing Session, but the
|
|
next Turn says the conversation is still in provider Default mode or has not
|
|
entered Tutti Mode because `plan propose` has not run.
|
|
- **Quick checks:** Confirm the activation revision is active in the exported
|
|
Session or activation endpoint, then confirm the affected Turn owns an active
|
|
`TuttiModeTurnSnapshot`. For Codex, inspect that Turn's provider rollout
|
|
`turn_context`: the collaboration mode may legitimately remain `default`,
|
|
while its developer instructions must contain `<tutti-host-context>` with
|
|
`"state":"active"`. If both facts are present, existing-Session snapshot
|
|
delivery works and the response is an interpretation failure. If the host
|
|
context is absent, trace snapshot preparation and runtime dispatch instead.
|
|
- **Root cause:** Tutti activation, provider Default/Plan collaboration mode,
|
|
and Tutti workflow existence are three independent facts. An instruction
|
|
that says a workflow exists only after `plan propose` can make a provider
|
|
incorrectly use the missing workflow as evidence that activation is
|
|
inactive, even though the active snapshot reached the reused provider
|
|
Session.
|
|
- **Fix:** Make the Host Context's snapshot state the sole authority for
|
|
reporting Tutti Mode status. Provider Default/Plan mode and workflow
|
|
existence remain independent facts and cannot override it. A clear plan
|
|
request must still run `plan propose` instead of returning a chat-only plan.
|
|
- **Validation:** On an existing Session, activate Tutti Mode and send a new
|
|
Turn while the provider collaboration mode remains Default. Verify the
|
|
provider receives the active Host Context, a status-only question reports
|
|
active without proposing a workflow, and a clear plan-generation request
|
|
invokes `plan propose`. Repeat with an inactive revision and verify it
|
|
reports inactive.
|
|
- **References:**
|
|
[workspace-workflows.md](../../architecture/workspace-workflows.md),
|
|
[tutti_mode_host_context.go](../../../packages/agent/daemon/runtime/tutti_mode_host_context.go),
|
|
[tutti_mode_host_context_test.go](../../../packages/agent/daemon/runtime/tutti_mode_host_context_test.go)
|
|
|
|
### Tutti Mode review only offers request changes after preference drift
|
|
|
|
- **Symptom:** A pending plan review shows only a request-changes action, or
|
|
accepting an existing plan is silently converted into a rejection after the
|
|
Composer effect or speed changes. The plan may also contain values that never
|
|
matched the Turn that produced it.
|
|
- **Quick checks:** Compare three distinct values: the producing Turn's
|
|
`TuttiModeTurnSnapshot`, the current revision's explicit
|
|
`execution.effect`/`execution.speed`, and the Session's current Composer
|
|
preferences. If the first two differ, inspect the `plan propose` or
|
|
`plan revise` response for `tutti_mode_preference_snapshot_mismatch`. If only
|
|
the current Composer preferences differ, the drift happened legitimately
|
|
after proposal and both review decisions must remain available.
|
|
- **Root cause:** Range validation alone allowed an Agent to persist plausible
|
|
but invented preference values. A hardcoded Host Context example could teach
|
|
the Agent different values than the frozen Turn snapshot. AgentGUI then
|
|
treated every post-proposal preference difference as implicit rejection, so
|
|
the user lost a direct way to accept the already-reviewed document.
|
|
- **Fix:** Render Host Context examples from the exact Turn snapshot. Require
|
|
Agent CLI proposals and revisions to carry that active Turn ID, and have the
|
|
plan service validate both explicit values through the activation-service
|
|
snapshot reader before allocating IDs, writing revision content, or mutating
|
|
workflow state. In AgentGUI, expose `Request changes` beside `Accept` only
|
|
when both frozen values prove a real preference change; otherwise expose only
|
|
`Accept`. Accept must keep the visible checkpoint identity and never convert
|
|
into request-changes feedback.
|
|
- **Validation:** Prove mismatched, missing-value, and missing-snapshot
|
|
documents leave revision content and workflow state untouched; prove a
|
|
matching document succeeds. In the UI, verify the divergent state exposes
|
|
both actions, request-changes rejects with current-preference feedback,
|
|
explicit accept records `accepted`, and matching empty-send still accepts.
|
|
- **References:**
|
|
[workspace-workflows.md](../../architecture/workspace-workflows.md),
|
|
[agent-gui-node.md](../../architecture/agent-gui-node.md),
|
|
[service.go](../../../services/tuttid/service/tuttimodeplan/service.go),
|
|
[tutti_mode_host_context.go](../../../packages/agent/daemon/runtime/tutti_mode_host_context.go),
|
|
[useAgentGUITuttiWorkflow.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/view/useAgentGUITuttiWorkflow.ts)
|
|
|
|
### Tutti Mode Plan stops loading after a task-graph revision
|
|
|
|
- **Symptom:** The configuration review panel works and `tutti plan revise`
|
|
returns a pending task-review checkpoint, but AgentGUI replaces the panel
|
|
with its load-failure state.
|
|
- **Quick checks:** Call the session-scoped workspace-workflow list endpoint
|
|
and inspect every task in the returned revision. Required collection fields,
|
|
especially `dependsOn`, must be JSON arrays even when empty. A successful
|
|
CLI mutation and HTTP `200` do not prove that the response still satisfies
|
|
the generated client contract.
|
|
- **Root cause:** A Go transport projection copied an empty dependency slice
|
|
onto a nil slice. `encoding/json` emitted `null`, violating the non-nullable
|
|
OpenAPI array contract. The generated TypeScript client therefore exposed a
|
|
value typed as `string[]` whose runtime value was `null`, and the desktop
|
|
adapter failed while cloning it before panel projection.
|
|
- **Fix:** Normalize required collections at the daemon API boundary so empty
|
|
values serialize as `[]`. Keep the OpenAPI field non-nullable and do not move
|
|
the repair into AgentGUI business projection, where it would only conceal a
|
|
malformed wire response.
|
|
- **Validation:** Exercise the real HTTP route, decode its JSON, and assert an
|
|
empty task dependency list is exactly `[]`, not `null`. Then verify a
|
|
task-graph revision with a root task can be listed and rendered.
|
|
- **References:**
|
|
[workspace-workflows.md](../../architecture/workspace-workflows.md),
|
|
[daemon_workspace_workflows.go](../../../services/tuttid/api/daemon_workspace_workflows.go),
|
|
[desktopWorkspaceWorkflowRuntime.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/desktopWorkspaceWorkflowRuntime.ts)
|
|
|
|
### Retrying a Tutti Mode Plan mutation duplicates or rejects the revision
|
|
|
|
- **Symptom:** A retry after a CLI timeout creates another workflow/revision,
|
|
or intentionally submitting the same Markdown again with a new request ID
|
|
fails with a revision/document-path uniqueness error.
|
|
- **Quick checks:** Confirm `plan propose` or `plan revise` received a stable
|
|
`--request-id`. Read `workspace_workflow_mutations` for the exact workspace,
|
|
source Session, mutation kind, workflow scope, and request ID. Compare its
|
|
`input_sha256` and committed result IDs. For upgraded local databases, check
|
|
that `workspace_workflow_plan_revisions` no longer has a unique index on
|
|
`document_path`.
|
|
- **Root cause:** Transport response loss is not the same as repeated content.
|
|
Without a caller mutation key, the daemon cannot distinguish a retry from
|
|
intentional reapplication. Conversely, a content-addressed document path may
|
|
legitimately be shared by multiple immutable revision metadata rows, so it
|
|
cannot be revision identity.
|
|
- **Fix:** Reuse the same request ID only for a retry of the same bytes. Use a
|
|
new request ID for an intentional mutation, even when the bytes are equal.
|
|
Keep the ledger claim and workflow/revision/checkpoint write in one SQLite
|
|
transaction. Preserve revision-ID and sequence uniqueness, and let the
|
|
corrective migration remove legacy document-path uniqueness.
|
|
- **Validation:** Cover concurrent same-key claims, same key plus same digest
|
|
replay, same key plus different digest conflict, new key plus identical bytes
|
|
creating the next sequence, and upgrade fixtures that retain checkpoints,
|
|
operations, mutations, and a clean `PRAGMA foreign_key_check`.
|
|
|
|
### One corrupt Tutti Mode Plan prevents daemon startup recovery
|
|
|
|
- **Symptom:** The daemon cannot finish startup after a task graph was accepted,
|
|
especially when its immutable Markdown file is missing or corrupt and its
|
|
`create_issue` operation is pending or failed.
|
|
- **Quick checks:** Inspect accepted workflows with pending/failed
|
|
`create_issue` operations, verify the referenced revision path and SHA-256,
|
|
and check whether the operation received a durable
|
|
`startup_recovery_failed` outcome. Do not require an Agent `wait` call to
|
|
start recovery.
|
|
- **Root cause:** Returning the first operation-local read, parse, validation,
|
|
or materialization error from the startup scan makes one damaged workflow a
|
|
process-wide availability failure.
|
|
- **Fix:** Run one recovery scan after workflow and Issue service composition
|
|
but before exposing the daemon. Isolate each operation: persist a failed
|
|
outcome and continue. Abort startup only when the scan fails or the daemon
|
|
cannot write a durable outcome. Keep deterministic Issue materialization and
|
|
do not add a background workflow worker.
|
|
- **Validation:** Cover pending and failed recovery, failed-before-retry state,
|
|
missing/corrupt revisions, a later operation succeeding after an earlier
|
|
failure, second-scan non-duplication, and outcome-write failure stopping
|
|
startup.
|
|
|
|
### A stuck edit-and-retry operation crashes the daemon on every launch
|
|
|
|
- **Symptom:** After updating, `tuttid` exits immediately on every launch and the
|
|
desktop reports `tuttid exited before it published its listener info`; the daemon
|
|
log shows `recover agent host: process runtime operation <id>: agent session not
|
|
found` (or `... agent history was rolled back but the edited turn still needs to
|
|
be resent`). The app never opens.
|
|
- **Quick checks:** Look for a durable `edit_retry` runtime operation stuck at the
|
|
`resend_pending` checkpoint — the last Turn was rolled back on the provider but
|
|
the replacement was never re-sent. While the daemon runs, the live worker only
|
|
logs this at 1 Hz; the fatality appears only on the next cold restart. The
|
|
affected session's `workspace_agent_session_history.recovery_state` is fenced
|
|
(non-`ready`).
|
|
- **Root cause:** The cold-recovery pass (`RecoverRuntimeOperations`) treats a
|
|
per-operation error as fatal to `build tuttid server`, so a persisted edit-retry
|
|
operation that can never make progress becomes a boot poison pill. The live
|
|
worker tolerates the same error, which is why the app worked until the restart.
|
|
- **Fix:** Durable edit-and-retry is disabled (`Config.EditRetryDisabled`, set in
|
|
production wiring). Recovery quarantines any leftover `edit_retry` operation —
|
|
marks it failed AND clears the session's effective-history fence back to `ready`
|
|
so the conversation can still send — and returns non-fatally, so existing poison
|
|
pills self-heal on the next boot. New edit-retries are refused at the entry
|
|
points. Sessions fenced before the neutralization (owning operation already
|
|
failed, so recovery cannot see it) self-heal at the send gate: the first send
|
|
clears an abandoned fence instead of rejecting. The durable rule: never let one
|
|
runtime operation's error abort daemon startup.
|
|
- **Rescue:** For an install that cannot update yet, quit Tutti and run
|
|
`tools/scripts/rescue-edit-retry-poison-pill.sh`, which quarantines the stuck
|
|
rows and clears the session fence in `~/.tutti/tuttid.db` after backing it up.
|
|
- **Validation:** `packages/agent/host/edit_retry_disabled_test.go` builds a
|
|
genuinely stuck operation, asserts enabled recovery is boot-fatal, and asserts
|
|
the disabled path quarantines it, returns nil, and leaves the session at
|
|
`recovery_state = ready`.
|
|
|
|
### Many stopped Tutti Mode conversations start again when the app opens
|
|
|
|
- **Symptom:** Starting the desktop app makes several old Tutti Mode source
|
|
conversations begin new Turns even though their tasks were previously
|
|
stopped or already terminal.
|
|
- **Quick checks:** Correlate the startup time with
|
|
`workspace_tutti_mode_legacy_repair_v5`, listener-ready wake recovery, and
|
|
new canonical Turns. In the execution tables, look for a migration
|
|
checkpoint whose `creation_reason` is
|
|
`legacy_execution_startup_repair`, an idle execution changed to
|
|
`awaiting_main`, and a prepared or dispatched main wake. Do not attribute the
|
|
behavior to generic Agent Host resume unless the exact Session already had a
|
|
recoverable active Turn.
|
|
- **Root cause:** The former V5 legacy backfill treated ambiguous idle plans as
|
|
live executions and created a durable main wake. Listener readiness later
|
|
delivered those historical wakes into the source conversations. Startup Run
|
|
settlement repair could also create another checkpoint for an execution
|
|
that was already terminal.
|
|
- **Fix:** Make legacy migration side-effect-free: keep an execution running
|
|
only when an authoritative Run is running, classify idle history as
|
|
`completed`, and create no migration wake. Suppress settlement checkpoints
|
|
for terminal/orphaned executions. For databases that already applied V5,
|
|
run the exact-fingerprint V6 cleanup to cancel the old recovery chain and
|
|
normalize execution status from current Runs.
|
|
- **Validation:** Cover fresh idle and running legacy fixtures with zero
|
|
migration wakes, a V5-corrupted idle fixture repaired to `completed`, a
|
|
V5-corrupted running fixture retained as `running`, and terminal settlement
|
|
replay producing no checkpoint or wake. Re-run V6 to prove idempotency.
|
|
- **References:**
|
|
[issue-execution.md](../../architecture/issue-execution.md),
|
|
[migrations_tutti_mode_legacy.go](../../../services/tuttid/data/workspace/migrations_tutti_mode_legacy.go),
|
|
[sqlite_tutti_mode_settlement.go](../../../services/tuttid/data/workspace/sqlite_tutti_mode_settlement.go)
|
|
|
|
### Tutti capability audit persistence races the first Turn projection
|
|
|
|
- **Symptom:** Create or SendInput fails with `sql: no rows in result set`
|
|
immediately after `runtime.submitted`, or a live `turn_update` frame is
|
|
rejected because its outcome is null.
|
|
- **Quick checks:** Verify capability provenance is carried on the
|
|
controller-owned submitted event or a lifecycle-neutral same-Turn guidance
|
|
report. Check that no second post-Exec writer tries to update a Turn before
|
|
the asynchronous projection creates it. Confirm the event schema accepts a
|
|
null outcome while a Turn is live.
|
|
- **Root cause:** `capabilityRefs` are historical submission audit data, not
|
|
current Tutti activation. Writing them through a second store call races the
|
|
canonical Turn projection. Attaching a synthetic lifecycle phase to guidance
|
|
can also corrupt the running Turn. A bounded reporter queue that falls back
|
|
to inline reporting may reorder projection work or deadlock on reporter
|
|
re-entry.
|
|
- **Fix:** Persist provenance with the submitted `TurnTransition`; use a
|
|
lifecycle-neutral state report for same-Turn guidance. Route synchronous
|
|
reporter work through one non-blocking, single-consumer FIFO. Queue pressure
|
|
must neither report inline nor block a reporter observer that can re-enter
|
|
the Controller.
|
|
- **Validation:** Verify runtime acceptance may precede Turn projection, live
|
|
null-outcome events validate, later transitions retain references, and a
|
|
backlog beyond the former bounded queue plus reporter re-entry preserves FIFO
|
|
order without deadlocking. Unsupported capability/source values must fail at
|
|
HTTP ingress and event publication without changing activation.
|
|
|
|
### `tutti agent send` reports unknown delivery after Codex accepted the prompt
|
|
|
|
- **Symptom:** `tutti agent send` returns
|
|
`agent_submit_delivery_unknown` with an error saying the workspace, Session,
|
|
Turn, and client submit IDs are required. The prompt and attachment may still
|
|
appear in the transcript and Codex may complete the Turn, so retrying the
|
|
command can duplicate the work.
|
|
- **Quick checks:** Correlate the command timestamp with the target Session in
|
|
the durable store and runtime logs. If the user message, provider Turn, and
|
|
assistant response exist but the submission provenance has no
|
|
`clientSubmitId`, the runtime accepted the command and only the
|
|
post-dispatch delivery confirmation failed.
|
|
- **Root cause:** The Agent service requires a caller-stable client submit ID to
|
|
reconcile ambiguous delivery after runtime dispatch. The CLI Agent provider
|
|
populated workspace and Session context but omitted the typed
|
|
`ClientSubmitID` on both SendInput and initial Session creation.
|
|
- **Fix:** Generate one UUID per CLI command invocation and pass it through the
|
|
typed `ClientSubmitID` field. Keep Turn ID allocation in Agent Host; the CLI
|
|
identity exists only to make one semantic submission idempotent and
|
|
recoverable.
|
|
- **Validation:** Exercise both `agent start` and `agent send` through the CLI
|
|
provider and assert each service input contains a valid UUID client submit
|
|
identity.
|
|
- **References:**
|
|
[session_commands.go](../../../services/tuttid/service/cli/providers/agentcontext/session_commands.go)
|
|
|
|
### Codex finishes but AgentGUI keeps showing the Session as working
|
|
|
|
- **Symptom:** Codex has emitted its final assistant message, but AgentGUI keeps
|
|
showing a busy spinner and the Session still has an active Turn. The UI may
|
|
settle several minutes later without a restart.
|
|
- **Quick checks:** Search `tuttid.log` for
|
|
`agent_session.activity_report.queue_backlog` and compare its `queue_depth`
|
|
with the final message's `occurredAtUnixMs` and canonical
|
|
`updatedAtUnixMs`. A large gap means provider completion reached the runtime
|
|
before canonical persistence. When prod and dev are both running, identify
|
|
them by listener, PID, state, and workspace paths; a shared log file alone
|
|
does not prove they share a database.
|
|
- **Root cause:** Streaming text, reasoning, and tool-output snapshots entered
|
|
the unbounded report FIFO before coalescing. When the single reporter was
|
|
slower than producers, thousands of superseded snapshots accumulated ahead
|
|
of the same Session's terminal report. Tool-call output snapshots were not
|
|
eligible for coalescing at all.
|
|
- **Fix:** Coalesce pending streaming snapshots by Session and message while
|
|
they are enqueued. Preserve the latest cumulative tool output, original tool
|
|
input, and earliest start time. Keep terminal and submit-provenance reports
|
|
as same-Session FIFO barriers.
|
|
- **Validation:** Enqueue at least 2048 text and tool-output snapshots and prove
|
|
each pending Session occupies one queue slot. Verify the latest snapshot is
|
|
retained, terminal and submit-provenance barriers stay ordered, reporter
|
|
re-entry remains non-blocking, and focused race tests pass.
|
|
- **References:**
|
|
[controller_report_queue.go](../../../packages/agent/daemon/runtime/controller_report_queue.go),
|
|
[report_coalescer.go](../../../packages/agent/daemon/runtime/report_coalescer.go),
|
|
[controller_report_queue_test.go](../../../packages/agent/daemon/runtime/controller_report_queue_test.go)
|
|
|
|
### Claude fails before provider Turn identity but AgentGUI keeps thinking
|
|
|
|
- **Symptom:** Claude returns no assistant result and the provider invocation
|
|
has already failed, but AgentGUI keeps showing the Turn as processing. Later
|
|
sends may be rejected because the runtime still reports an active Turn.
|
|
- **Quick checks:** Correlate the canonical Turn ID across the submit report,
|
|
Claude sidecar event, and runtime controller. The characteristic sequence has
|
|
a durable submitted Turn, `turn_failed` without `providerTurnId`, dispatch
|
|
disposition `applied_without_provider_turn`, and a canonical failed report,
|
|
while `HasActiveTurn` remains true. Do not interpret
|
|
`applied_without_provider_turn` itself as a failure; cancel-before-acceptance
|
|
legitimately uses the same admission result.
|
|
- **Root cause:** Provider acceptance and canonical completion are independent
|
|
contracts. The acceptance wrapper treated an exact providerless
|
|
`turn.failed` as premature provider output, while the blocking controller
|
|
released its active-Turn fence only for failures carrying explicit rejection
|
|
metadata. A non-rejection failure could therefore be durable but never close
|
|
the runtime fence.
|
|
- **Fix:** Hold an exact canonical terminal behind the acceptance barrier and
|
|
return it to the controller without inventing provider identity. Classify
|
|
completion from the typed terminal event for the exact Turn, never from the
|
|
dispatch disposition or error text. Partition every pre-acceptance batch so
|
|
an exact terminal cannot carry provider-dependent assistant/tool output
|
|
through the barrier. Release the active-Turn fence only after the terminal
|
|
crosses the synchronous durable-report barrier. If that commit fails or its
|
|
acknowledgement is lost, retain the terminal and retry it idempotently until
|
|
the commit succeeds or daemon reconciliation proves the Turn already settled.
|
|
Provider-root completion with an identity continues through canonical
|
|
aggregation.
|
|
- **Validation:** Cover Claude translation of a providerless `turn_failed`,
|
|
mixed terminal/provider-output batches, controller settlement after a
|
|
successful terminal report, transient report failure, commit-success/ACK-loss,
|
|
explicit rejection, cancel-before-acceptance, and outcome-unknown. Run the
|
|
Host conformance scenario for initial and ordinary idempotent submissions.
|
|
Retain a root-provider lifecycle test proving provider-root completion does
|
|
not clear the canonical Turn before daemon reconciliation.
|
|
- **References:**
|
|
[claude_sdk_execution.go](../../../packages/agent/daemon/runtime/claude_sdk_execution.go),
|
|
[controller_turn_completion.go](../../../packages/agent/daemon/runtime/controller_turn_completion.go),
|
|
[controller_turn_exec.go](../../../packages/agent/daemon/runtime/controller_turn_exec.go)
|
|
|
|
### Tutti mode is active in the composer but disappears after the first submit
|
|
|
|
- **Symptom:** The home composer shows Tutti enabled and the submit trace records
|
|
`tutti_mode_active=true`, but the created Session has no activation and its
|
|
first `tutti_mode_turn_snapshots` row is `inactive`.
|
|
- **Quick checks:** Trace `initialTuttiModeActivation` at the composer submit,
|
|
controller activation, desktop activity service, renderer HTTP adapter, and
|
|
daemon create ingress. Log only a presence boolean at each boundary. Compare
|
|
the Session export with the durable activation and Turn snapshot rows;
|
|
`capabilityRefs` do not prove current activation.
|
|
- **Root cause:** Session creation crosses several adapters that reconstruct
|
|
object literals. A manually projected create input can omit
|
|
`initialTuttiModeActivation` or its Tutti `capabilityRefs` even when the
|
|
upstream type carries them. Reading mutable draft state after submit can also
|
|
lose the exact composer selection. AgentGUI treats
|
|
`capabilityMenuState.tuttiMode.enabled === true` as the host capability for
|
|
the hero toggle, badge activation, and `/tutti`; Tutti Desktop always supplies
|
|
it, while hosts that omit it or set `enabled: false` fail closed.
|
|
- **Fix:** Snapshot active/inactive state and orchestration intensity atomically
|
|
with the composer submit. Preserve both activation and capability provenance
|
|
through every create projection, and use the same host capability as the
|
|
visible control for slash actions.
|
|
- **Validation:** Keep boundary tests for the service, engine host, and HTTP
|
|
adapter. In a real development launch, submit once with Tutti enabled and
|
|
verify the HTTP boundary sees the activation, the activation revision is
|
|
`active`, and the first Turn snapshot retains its source and intensity.
|
|
|
|
### A new Tutti conversation briefly reports session not found after submit
|
|
|
|
- **Symptom:** The optimistic conversation appears after the first submit and
|
|
briefly shows `workspace agent session not found`, then recovers after the
|
|
provider starts and the durable Session and Turn complete normally.
|
|
- **Quick checks:** Correlate the Session ID across desktop and daemon submit
|
|
traces. The characteristic order is
|
|
`renderer_adapter.create.http_requested`, then
|
|
`agent.activity.reconcile_session_absent`, then
|
|
`renderer_adapter.create.resolved` and `api.create.completed`. Confirm the
|
|
missing reconcile occurs while the engine still has a requested or uncertain
|
|
new-session activation.
|
|
- **Root cause:** Initial activation can publish activity or mode updates before
|
|
the create transaction is query-visible or its HTTP response returns. The
|
|
renderer treated those independent signals as permission to reconcile an
|
|
exact Session, so a normal not-yet-visible window became a transient 404 even
|
|
though the create command was still in flight. A topic-specific guard covered
|
|
only one of those signals and left the admission boundary fragmented.
|
|
- **Fix:** Keep the new Session behind the admission fence while its activation
|
|
is still `requested`. The activity-core reconcile reducer records demand but
|
|
defers transport reads whenever the exact Session has no canonical record
|
|
and the create command still owns visibility. If the create result becomes
|
|
uncertain or its confirmation expires, enqueue one authoritative recovery
|
|
reconcile; that recovery is allowed to discover a Session that was committed
|
|
even though the create response was lost. A normal successful create still
|
|
releases the merged demand through `session/upserted`, regardless of whether
|
|
the trigger was an activity event, a mode update, or direct Session
|
|
synchronization. Do not broadly swallow reconcile 404s: existing Sessions
|
|
and settled activations still report real failures. Tombstone only from
|
|
explicit deletion evidence such as `session_deleted` or a successful delete
|
|
command.
|
|
- **Validation:** Hold create in flight, publish both
|
|
`agent.activity.updated` and `workspace.tuttimode.updated`, and request direct
|
|
Session synchronization. Verify no Session read is called while activation
|
|
is requested. Then simulate a lost create response and verify one recovery
|
|
reconcile is admitted, discovers the canonical Session, and confirms the
|
|
activation. Also prove an existing Session still reconciles and preserves
|
|
its not-found diagnostic, while an explicit `session_deleted` event
|
|
tombstones it.
|
|
- **References:**
|
|
[sessionReconcile.reducer.ts](../../../packages/agent/activity-core/src/engine/sessionReconcile.reducer.ts)
|
|
[rootReducer.ts](../../../packages/agent/activity-core/src/engine/rootReducer.ts)
|
|
[workspaceAgentActivityReconcileBridge.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/workspaceAgentActivityReconcileBridge.ts)
|
|
|
|
### A Tutti submission remains `delivery is still being confirmed`
|
|
|
|
- **Symptom:** Retrying the same composer submission keeps returning
|
|
`agent submit delivery is still being confirmed`. The Session may already
|
|
exist and the Tutti badge may remain active, but the daemon does not start a
|
|
second Turn.
|
|
- **Quick checks:** Read the `workspace_agent_submit_claims` row for the exact
|
|
workspace, Session, and `clientSubmitId`. Compare its immutable
|
|
`canonical_turn_id` with `tutti_mode_turn_snapshots.turn_id` and with the
|
|
Turn found by durable message provenance for that `clientSubmitId`. Do not
|
|
change the claim to a runtime-returned mismatched Turn ID. If the barrier
|
|
reports `conflicts with durable submit provenance`, compare the JSON-encoded
|
|
existing and projected message payloads as well as their Go container types.
|
|
In particular, compare `payload.seq` and `occurred_at_unix_ms` from the
|
|
ordinary runtime report and the durable provenance report for the same
|
|
`clientSubmitId`.
|
|
Identical JSON with `[]any` on the SQLite-decoded side and
|
|
`[]map[string]any` on the fresh-report side is an in-memory normalization
|
|
defect, not conflicting submit evidence. Different sequence values for the
|
|
same canonical message mean the two report paths reconstructed its occurrence
|
|
independently.
|
|
- **Root cause:** Provider handoff crossed an ambiguity boundary: the daemon
|
|
reserved and snapshotted a canonical Turn before dispatch, but did not
|
|
durably confirm the exact accepted Turn. The ordinary reporter may have
|
|
persisted its Session/Turn state while the atomic client-submit message
|
|
barrier failed, or the process may have stopped around that barrier. A host
|
|
wired only to the compatibility `ActivityReporter` cannot satisfy the
|
|
runtime's required `DurableActivityReporter` contract. The barrier can also
|
|
falsely reject an ordinary message replay when a fresh JSON payload retains
|
|
typed Go slices but the same payload decoded from SQLite uses `[]any`, and
|
|
idempotency compares those concrete types directly. Retrying the provider
|
|
call would risk duplicate work. Separately, the runtime adapter and durable
|
|
provenance barrier can construct the same user message on different
|
|
goroutines. If each path samples its own transient event timestamp, whichever
|
|
path reaches SQLite second carries a different derived `payload.seq`; the
|
|
strict provenance guard correctly rejects it. Scheduling determines whether
|
|
the ordinary report exists before the barrier, so this form is intermittent.
|
|
A compatibility reporter can still supply different transport-only sequence,
|
|
source, or submission timestamps for the same protected user-message
|
|
provenance.
|
|
- **Fix:** Preserve the prepared submit claim, Tutti snapshot, activation, and
|
|
Session. Ensure the host supplies `DurableActivityReporter`; decorators
|
|
should embed or otherwise preserve that required interface instead of
|
|
probing an optional capability. Its provenance barrier must run outside the
|
|
Session lifecycle lock, after earlier same-FIFO reports, and atomically write
|
|
the stable user message against an existing Turn.
|
|
Normalize every incoming message payload through the durable JSON
|
|
representation before projection, merge, and idempotency comparison so
|
|
fresh reports and SQLite-decoded rows share one in-memory shape. Do not
|
|
special-case individual typed slices or bypass genuine payload conflicts.
|
|
For the two canonical user-message paths, preserve the durable submit claim's
|
|
immutable creation time through the typed Host and Runtime inputs and derive
|
|
both reports' occurrence and sequence from it. Use `clientSubmitId` only as
|
|
the idempotency identity, not as a numeric sequence. Build both messages with
|
|
the shared canonical user-message constructor. Keep the store's exact-payload
|
|
conflict check for all genuine conflicts; the protected provenance replay may
|
|
accept only transport-only metadata differences after Turn, role, kind,
|
|
status, semantics, `clientSubmitId`, content, content mode, display prompt,
|
|
and text all match.
|
|
Reconcile only from exact durable `clientSubmitId` provenance. If it resolves
|
|
to the reserved Turn, idempotently accept the snapshot and claim; if it is
|
|
absent or resolves elsewhere, keep delivery unknown and never re-dispatch
|
|
that client submission. Definite pre-dispatch rejection is the only path
|
|
allowed to abandon the prepared evidence.
|
|
- **Validation:** Cover exact acceptance, accepted Turn-ID mismatch, snapshot
|
|
and claim acceptance failures, message-write rollback, process interruption
|
|
after snapshot binding, reporter re-entry, duplicate replay without a new
|
|
message version, and multiple guidance submissions sharing one active Turn.
|
|
Include an ordinary-message-first replay whose payload contains nested typed
|
|
slices and integer values; assert the provenance barrier reuses the existing
|
|
message version while a real content change still conflicts.
|
|
Exercise both runtime-message-first and provenance-first orderings and assert
|
|
their complete message updates, occurrence times, and derived payload
|
|
sequences are identical. Reopen the submit-claim store and prove an
|
|
idempotent retry retains the original claim creation time. Exercise the
|
|
narrow protected replay with differing transport sequence, source, and
|
|
submission time, while proving a real content or semantics change still
|
|
conflicts.
|
|
Assert the unknown paths execute the provider zero additional times and never
|
|
close the provisional Session or delete its activation.
|
|
- **References:**
|
|
[workspace-workflows.md](../../architecture/workspace-workflows.md),
|
|
[submit_claims.go](../../../services/tuttid/service/agent/submit_claims.go),
|
|
[service_tutti_mode_activation.go](../../../services/tuttid/service/agent/service_tutti_mode_activation.go)
|
|
|
|
Turn state, loading, cancel, restore, file-change undo, rail projection, event updates, imports, and performance.
|
|
|
|
### AgentGUI restores a provisional conversation after creation fails
|
|
|
|
- Symptom:
|
|
A new conversation fails to start, but a renderer reload selects the same
|
|
Session ID again. The Rail may show an optimistic row while the canonical
|
|
store has no matching Session.
|
|
- Quick checks:
|
|
Correlate the activation request, Workbench
|
|
`lastActiveAgentSessionId`, renderer reload, and canonical Session lookup by
|
|
exact ID. If navigation persistence precedes canonical activation
|
|
confirmation and the lookup returns typed `session.not_found`, this is a
|
|
provisional-selection leak. A bounded Rail page that omits the Session is
|
|
not enough evidence.
|
|
- Root cause:
|
|
The new-conversation controller persisted its generated Session ID when it
|
|
selected the optimistic row. A crash or reload could therefore restore that
|
|
ID before Host committed a canonical Session. Reconcile errors also lost
|
|
their typed code in the frontend engine, so AgentGUI could not distinguish a
|
|
proven missing Session from a transient read failure.
|
|
- Fix:
|
|
Keep provisional selection in mounted UI only. Persist it after the engine
|
|
confirms activation from canonical Session state. Preserve reconcile
|
|
`errorCode`; clear only the exact global and per-target navigation memories
|
|
when the active reconcile settles with `session.not_found`. Preserve them
|
|
for timeout, transport failure, and bounded-list absence.
|
|
- Validation:
|
|
Verify a requested activation performs no persistence, canonical
|
|
confirmation writes once, create failure plus reload cannot restore the
|
|
provisional ID, typed `session.not_found` clears only the matching memories,
|
|
and a transient reconcile error keeps the selection.
|
|
- References:
|
|
[agent-gui-node.md](../../architecture/agent-gui-node.md)
|
|
[sessionReconcile.reducer.ts](../../../packages/agent/activity-core/src/engine/sessionReconcile.reducer.ts)
|
|
[useAgentGUIConversationSelectionController.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUIConversationSelectionController.ts)
|
|
|
|
### New Goal spinner stops before the first reply, then starts after it appears
|
|
|
|
- Symptom:
|
|
Creating a Session with an initial Goal starts the processing indicator, then
|
|
stops it as soon as the canonical Session appears. The provider continues
|
|
working without an indicator. When the first assistant message and canonical
|
|
Turn arrive, the indicator starts again until the Turn settles. The inverse
|
|
symptom is a completed or failed Turn whose rail status has settled while the
|
|
Composer action still spins.
|
|
- Quick checks:
|
|
Compare the Claude SDK `session_state_changed` lifecycle log, activity stream
|
|
connection, Engine runtime activity, canonical Session, and canonical latest
|
|
Turn. The characteristic gap has SDK state `running`, a canonical Session, no
|
|
latest Turn, and Engine runtime activity still `idle`. Do not use assistant
|
|
message arrival as Turn identity. If the desktop log reports `Event stream
|
|
catalog revision mismatch`, fully restart `dev:desktop`; renderer HMR cannot
|
|
replace the already running Go daemon binary.
|
|
- Root cause:
|
|
Claude emits an exact session-level `running` observation before the first
|
|
provider Turn identity, but the daemon previously logged and discarded it.
|
|
Goal-only creation correctly has `initialTurnExpected = false`, so neither a
|
|
pending prompt nor a canonical Turn can bridge that interval. For the inverse
|
|
symptom, AgentGUI bypassed the Engine's occurrence-time fence and read the
|
|
stale raw `running` flag directly after a canonical Turn had settled.
|
|
- Fix:
|
|
Normalize the SDK observation to provider-neutral `running`/`idle` runtime
|
|
activity, publish it as an ephemeral activity-stream event, and let the
|
|
workspace Engine drive AgentGUI and rail busy projection. Once a canonical
|
|
Session exists, AgentGUI must consume the Engine's fenced display status;
|
|
use raw runtime activity only before that projection exists. Clear ephemeral
|
|
runtime activity on disconnect. Keep Goal turnless and do not invent
|
|
lifecycle state, provider-specific timers, or synthetic Turn IDs.
|
|
- Validation:
|
|
Cover SDK projection without Turn identity, post-commit event publication,
|
|
activity-stream ingestion before canonical Session hydration, AgentGUI busy
|
|
projection, `idle`, disconnect cleanup, and both completed and failed Turns
|
|
remaining settled when an older raw runtime observation still says
|
|
`running`.
|
|
- References:
|
|
[claude_sdk_events.go](../../../packages/agent/daemon/runtime/claude_sdk_events.go)
|
|
[workspaceEventCoordinator.ts](../../../packages/agent/activity-core/src/workspaceEventCoordinator.ts)
|
|
[useAgentGUISessionPresentation.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUISessionPresentation.ts)
|
|
[agent-gui-node.md](../../architecture/agent-gui-node.md)
|
|
|
|
### AgentGUI detail misses or misplaces a canonical Turn error
|
|
|
|
- Symptom:
|
|
The AgentGUI rail marks a conversation failed, but opening the conversation
|
|
shows only the preceding tool or assistant rows. Reloading the session does
|
|
not reveal why the Turn failed. A related symptom is a failure from a much
|
|
older Turn appearing at the bottom of the currently loaded conversation,
|
|
immediately before current processing UI.
|
|
- Quick checks:
|
|
Inspect the canonical Turn snapshot before debugging React state. Confirm the
|
|
owning Turn is terminal with `outcome = failed` or `interrupted` and has a
|
|
non-empty `error.message`. Then inspect that Turn's timeline messages for a
|
|
structured `visibleError` or a plain assistant message with the same error
|
|
text. Finally, distinguish the session-wide Turn list from the hydrated
|
|
message window: check whether any currently loaded timeline item has the
|
|
exact owning `turnId` and whether older message pages remain.
|
|
- Root cause:
|
|
Turn outcome and error are durable canonical state, while provider transcript
|
|
messages are optional evidence. If detail rendering only projects transcript
|
|
messages, a runtime that settles `AgentActivityTurn.error` without emitting a
|
|
visible-error message leaves the rail and detail inconsistent. Reading only
|
|
the active Turn also loses the error as soon as settlement clears
|
|
`activeTurnId`. Conversely, treating the session-wide Turn list as transcript
|
|
membership manufactures an empty Turn for an error whose messages are outside
|
|
the current page, appends it after newer Turns, and makes an old failure look
|
|
current.
|
|
- Fix:
|
|
Reconcile terminal `AgentActivityTurn.error` in the shared transcript
|
|
projection by exact `turnId`. Reuse a structured visible error, upgrade a
|
|
matching plain assistant failure, or add one view-only row with a stable
|
|
`(agentSessionId, turnId)` identity. Normally the owning Turn must already
|
|
exist in the hydrated transcript projection. The exact latest failed Turn is
|
|
the narrow exception: if it emitted no transcript item, create its view-only
|
|
error row so the current failure reason remains visible. Historical Turns
|
|
outside the message window still wait for an older page to supply an anchor.
|
|
Do not restore session `lastError`, let session-operation selectors fall back
|
|
to Turn errors, reinterpret a successful attach as activation failure,
|
|
persist a duplicate message, or add component-local failure state.
|
|
- Validation:
|
|
Cover a failed Turn with no provider error message, a matching plain failure,
|
|
and an existing structured visible error. The first must render one fallback
|
|
row when its Turn is loaded, and the latter two must remain single rows. With
|
|
a full canonical Turn list and a newest-page-only transcript window, an older
|
|
failed or interrupted Turn must not create a row or change Turn order. After
|
|
prepending the older page, its error must appear exactly once on the owning
|
|
Turn. Also cover the exact latest failed Turn with zero hydrated transcript
|
|
items producing one error row, plus an older failed Turn with a newer active
|
|
Turn whose processing ownership remains current.
|
|
- References:
|
|
[workspaceAgentTurnErrorProjection.ts](../../../packages/agent/gui/shared/workspaceAgentTurnErrorProjection.ts)
|
|
[workspaceAgentTurnErrorProjection.spec.ts](../../../packages/agent/gui/shared/workspaceAgentTurnErrorProjection.spec.ts)
|
|
[workspaceAgentTimelineCanonical.ts](../../../packages/agent/gui/shared/workspaceAgentTimelineCanonical.ts)
|
|
[agent-gui-node.md](../../architecture/agent-gui-node.md)
|
|
|
|
### Mid-turn provider failure renders a generic "request failed" card
|
|
|
|
- Symptom:
|
|
A root-provider-lifecycle turn (Codex, Cursor, OpenCode) fails mid-turn —
|
|
for example quota exhaustion — but the conversation shows only a generic
|
|
"request failed" card with no detail and no classification code. The real
|
|
reason appears only in the durable Turn record and daemon logs.
|
|
- Quick checks:
|
|
Confirm the canonical Turn settled with a non-empty `error.message`. If the
|
|
durable Turn carries the reason but the live card does not, inspect the
|
|
synthesized `daemonRootTurnSettlement` event metadata, not the provider
|
|
adapter — the adapter already reported the failure correctly.
|
|
- Root cause:
|
|
Root-provider adapters do not emit `EventTurnFailed`; the daemon synthesizes
|
|
it in `ReconcileRootTurnSettlement` after the durable settle commits. The
|
|
settlement previously forwarded only `turnId` and `outcome`, so the
|
|
synthesized event had an empty detail and the visible-error projection
|
|
classified it as `unknown` instead of (for example) `quota_or_rate_limit`.
|
|
- Fix:
|
|
Forward `Turn.ErrorMessage` through `RootTurnSettlement` and stamp it on the
|
|
synthesized event metadata under the `error` / `errorMessage` keys read by
|
|
`BestEffortErrorMessage`. Keep classification text-based in
|
|
`visibleFailureCode`; do not special-case providers at this layer.
|
|
- Validation:
|
|
`TestReconcileRootTurnSettlementPublishesFailureDetail` settles a failed
|
|
root turn with a quota message and asserts the live stream emits a
|
|
`quota_or_rate_limit` visible error carrying the original detail.
|
|
- References:
|
|
[controller_root_turn.go](../../../packages/agent/daemon/runtime/controller_root_turn.go)
|
|
[visible_error.go](../../../packages/agent/daemon/runtime/visible_error.go)
|
|
[agent_runtime_adapter.go](../../../services/tuttid/agent_runtime_adapter.go)
|
|
|
|
### Failed Claude Turn looks like no reply or renders raw 522 payload
|
|
|
|
- Symptom:
|
|
Claude produces no normal assistant reply, and the eventual failed message is
|
|
a raw `API Error: 522` payload instead of the standard timeout card.
|
|
- Quick checks:
|
|
Confirm the canonical Turn settled as failed with `request_timed_out`, while
|
|
its persisted assistant message is plain failed text containing the explicit
|
|
522 signature rather than a structured visible error.
|
|
- Root cause:
|
|
The SDK can report a gateway timeout as a failed assistant text message. The
|
|
generic failed-message recovery recognized network socket markers but not the
|
|
unambiguous HTTP 522 signature, so the raw provider payload reached Markdown.
|
|
- Fix:
|
|
Classify only explicit 522 markers as `request_timed_out` in the shared failed
|
|
message presentation. Keep generic timeout prose unclassified to avoid
|
|
rewriting ordinary assistant content.
|
|
- Validation:
|
|
Cover both classification and rendered output: the timeout card is visible
|
|
and the raw payload is absent.
|
|
- References:
|
|
[agentErrorPresentation.ts](../../../packages/agent/gui/shared/agentEnv/agentErrorPresentation.ts)
|
|
[AgentMessageBlock.tsx](../../../packages/agent/gui/shared/agentConversation/components/AgentMessageBlock.tsx)
|
|
|
|
### Codex WebSocket reconnect rejects a long prompt metadata header
|
|
|
|
- Symptom:
|
|
A long-running Codex turn disconnects, then its WebSocket reconnect fails with
|
|
`did not receive a valid HTTP request; Separator is found, but chunk is longer than limit`.
|
|
The same prompt may succeed when the original connection stays open.
|
|
- Quick checks:
|
|
Inspect the Codex app-server `turn/start` params without logging prompt text.
|
|
Compare prompt byte count with any client metadata byte count. An unbounded
|
|
prompt copy in `responsesapiClientMetadata` can cross a WebSocket HTTP
|
|
parser's roughly 32 KiB line limit during reconnect.
|
|
- Root cause:
|
|
Codex may forward Responses API client metadata through the WebSocket HTTP
|
|
handshake. Duplicating the full prompt into that metadata turns prompt size
|
|
into header size. Reconnect then fails before the provider can process the
|
|
normal `input` payload.
|
|
- Fix:
|
|
Keep the full materialized prompt in `turn/start.input`. Do not duplicate it
|
|
into auxiliary metadata, truncate it in AgentGUI, or compensate with reconnect
|
|
retries. Provider-specific metadata stays inside the provider adapter and must
|
|
follow a demonstrated provider contract.
|
|
- Validation:
|
|
Cover a prompt larger than 32 KiB. Assert `turn/start.input` preserves it
|
|
exactly and `responsesapiClientMetadata` contains no prompt copy. Run
|
|
`go test ./packages/agent/daemon/runtime`.
|
|
- References:
|
|
[codex_appserver_event_params.go](../../../packages/agent/daemon/runtime/codex_appserver_event_params.go)
|
|
[codex_appserver_adapter_test.go](../../../packages/agent/daemon/runtime/codex_appserver_adapter_test.go)
|
|
[agent-gui-node.md](../../architecture/agent-gui-node.md)
|
|
|
|
### Opening a historical Codex session starts two short provider processes
|
|
|
|
- Symptom:
|
|
Selecting one settled Codex conversation logs two process starts, each with
|
|
`initialize` followed by `thread/read`, even though no turn was resumed.
|
|
- Quick checks:
|
|
Confirm neither sequence contains `thread/resume`. Enable reconcile trace
|
|
diagnostics temporarily and count complete `state_and_messages` commands.
|
|
Two discovery/message/final sequences indicate two reconciles; one sequence
|
|
with two probes indicates that both detail projections resolved capabilities.
|
|
- Root cause:
|
|
There are two independent boundaries to verify. Combined reconciliation
|
|
intentionally reads detail twice to close races while messages load, but its
|
|
discovery read only needs hierarchy and `messageVersion`; using the full
|
|
Session projection there adds an accidental provider-history capability
|
|
probe. Separately, automatic selection restoration can be replayed while the
|
|
first reconcile is running. Treating that ordinary selection as
|
|
`force: true` appends a second complete reconcile, whose final detail read
|
|
launches another probe.
|
|
- Fix:
|
|
Preserve the two-read race fence. Send the first request with the
|
|
`messageHydration` projection and the final request with `full`; only the
|
|
latter resolves provider-backed lifecycle capabilities. Model automatic
|
|
selection as idempotent “ensure hydrated” work, leaving force refresh as an
|
|
explicit intent. Do not mask either symptom with a TTL cache, single-flight
|
|
delay, or by removing the final read.
|
|
- Validation:
|
|
Select a historical root-only Codex Session with no child hierarchy through
|
|
`make dev-gui`. One combined reconcile should issue two detail requests and
|
|
one message-list request, but only the final detail request should emit the short
|
|
`initialize`/`thread/read` capability probe. Replaying the same selection
|
|
while that reconcile is pending must not start another combined reconcile.
|
|
Sessions with child hierarchy or additional message pages may issue more than
|
|
one message-list request; each request must still follow the shared executor's
|
|
Session and cursor policy.
|
|
- References:
|
|
[sessionReconcileExecutor.ts](../../../packages/agent/activity-core/src/sessionReconcileExecutor.ts)
|
|
[useAgentConversationSelection.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentConversationSelection.ts)
|
|
[service_turns.go](../../../services/tuttid/service/agent/service_turns.go)
|
|
[codex_appserver_fork.go](../../../packages/agent/daemon/runtime/codex_appserver_fork.go)
|
|
|
|
### Codex turn stays working before any reply or tool activity
|
|
|
|
- Symptom:
|
|
A submitted Codex turn stays working without assistant text, reasoning, or
|
|
tool activity. Stop may return quickly, but the next prompt on the same
|
|
conversation can stall in the same way. This also occurs when a conversation
|
|
is left offline and its first later submit loses the app-server connection.
|
|
- Quick checks:
|
|
Correlate `agent.submit.trace` records by `client_submit_id`, `turn_id`, and
|
|
`agent_session_id`. A `turn.start.requested` without
|
|
`turn.start.succeeded` means the immediate app-server acknowledgement did
|
|
not arrive. After the bounded failure, confirm
|
|
`agent_session.app_server.turn_start.client_invalidated` appears and the next
|
|
submit starts a new local process with `thread/resume`. An immediate
|
|
`turn.start.failed` with `error=EOF` is an outcome-unknown transport loss and
|
|
must still project one visible failed assistant message.
|
|
- Root cause:
|
|
Codex `turn/start` should acknowledge immediately and stream the actual work
|
|
through notifications, but the adapter previously called it with no client
|
|
deadline. A graceful Stop could acknowledge `turn/interrupt` without proving
|
|
that the unacknowledged `turn/start` connection was healthy, so the adapter
|
|
retained and reused the same bad process. The live-session probe also treated
|
|
a retained client pointer as live after its `Done` channel closed. Finally, a
|
|
pre-acceptance `turn/start` failure was buffered as provider output even
|
|
though it had no provider Turn identity, hiding the canonical terminal from
|
|
the controller and GUI.
|
|
- Fix:
|
|
Bound only the `turn/start` acknowledgement to 30 seconds; do not bound the
|
|
subsequent running turn. Treat deadline, cancellation before acknowledgement,
|
|
or transport disconnect as an unhealthy client, remove exactly that client
|
|
from the live-session registry, and close it. Do not automatically replay the
|
|
prompt because delivery is unknown. The next explicit submit uses the
|
|
existing session recovery path to start a process and resume the provider
|
|
thread. A live-session probe must also reject terminated clients. Publish the
|
|
exact canonical `turn.failed` directly when `turn/start` fails before provider
|
|
acceptance, while dropping unaccepted provider-dependent output; this reuses
|
|
the normal visible-error projection without inventing provider identity.
|
|
Bound `turn/steer` acknowledgement separately to 10 seconds.
|
|
- Validation:
|
|
Run
|
|
`go test ./packages/agent/daemon/runtime -run 'Test(CodexAppServerAdapter(Turn(StartAckTimeoutInvalidatesClient|StartEOFProjectsVisibleFailureBeforeAcceptance|StartCancelBeforeAckInvalidatesClient|StartAckTimeoutDoesNotBoundRunningTurn|SteerTimesOut)|CanResumeAfterTurnStartAckTimeout|HasLiveSessionRejectsClosedClient)|StandardACPAdapterHasLiveSessionRejectsClosedClient)$'`.
|
|
Cover timeout, EOF before acceptance, terminated-client liveness, pre-ack
|
|
cancellation, a long post-ack turn, bounded guidance, and successful
|
|
`thread/resume` followed by a completed turn.
|
|
- References:
|
|
[codex_appserver_turn.go](../../../packages/agent/daemon/runtime/codex_appserver_turn.go)
|
|
[codex_appserver_registry.go](../../../packages/agent/daemon/runtime/codex_appserver_registry.go)
|
|
[codex_appserver_turn_timeout_test.go](../../../packages/agent/daemon/runtime/codex_appserver_turn_timeout_test.go)
|
|
|
|
### AgentGUI turn actions return plain-text route 404s
|
|
|
|
- Symptom:
|
|
A turn-scoped action such as Stop sends the documented OpenAPI URL but gets
|
|
`404 page not found` with `text/plain`, even though nearby daemon APIs work.
|
|
- Quick checks:
|
|
Distinguish the default mux response from a domain 404. A route-level miss is
|
|
plain text; a matched daemon handler returns the structured API error schema.
|
|
Then compare the operation in the OpenAPI document and generated server with
|
|
`services/tuttid/api/routes.go`.
|
|
- Root cause:
|
|
The daemon currently mounts generated handlers through a hand-maintained
|
|
`RegisterRoutes` table. Adding an operation to OpenAPI and regenerating the
|
|
server does not automatically add it to that runtime table, so the handler
|
|
can compile and pass direct tests while remaining unreachable over HTTP.
|
|
- Fix:
|
|
Register every new generated operation in `RegisterRoutes`, including its
|
|
exact method and path pattern. For related protocol operations introduced
|
|
together, audit the whole group rather than only the first reported URL.
|
|
- Validation:
|
|
Add a mux-level test that calls `RegisterRoutes`, sends the real method/path,
|
|
asserts path parameters reach the service, and checks the structured
|
|
response. Rebuild and restart the dev daemon, then verify the live endpoint
|
|
no longer returns the default plain-text 404.
|
|
- References:
|
|
[routes.go](../../../services/tuttid/api/routes.go)
|
|
[daemon_test.go](../../../services/tuttid/api/daemon_test.go)
|
|
[tuttid.v1.yaml](../../../services/tuttid/api/openapi/tuttid.v1.yaml)
|
|
|
|
### Goal banner shows Delete but no Restart action
|
|
|
|
- Symptom:
|
|
A provider supports goal pause/resume and the action is initially visible,
|
|
but a paused or blocked Goal later shows only Delete.
|
|
- Quick checks:
|
|
Compare provider composer capabilities with the selected Session's typed
|
|
capabilities. `capabilities: null` means no authoritative session snapshot
|
|
is available because it is not yet reported or legacy-ambiguous; only then
|
|
may presentation fall back to the provider composer descriptor. A non-null
|
|
object is complete. Inspect the private persistence compatibility carrier
|
|
for `capabilitiesReported` before treating an empty legacy list as
|
|
authoritative.
|
|
- Root cause:
|
|
Legacy persistence could not distinguish an unknown capability list from an
|
|
explicitly reported empty snapshot. Partial `RuntimeContext` reports could
|
|
also replace the full map and erase capability metadata. The API then closed
|
|
the missing list into `false`, which overrode the provider composer value.
|
|
- Fix:
|
|
Carry capabilities as a typed optional snapshot outside `RuntimeContext`.
|
|
Persist explicit report presence, preserve omitted snapshots on partial
|
|
updates, and use typed `set`/`unset` runtime-context patches. In presentation,
|
|
fall back to composer capabilities only for a null session snapshot; preserve
|
|
explicit session `false`. No database schema migration is required because
|
|
the report-presence marker uses the existing metadata JSON column and legacy
|
|
rows are normalized when decoded.
|
|
- Validation:
|
|
Cover legacy empty and non-empty metadata, reported empty snapshots,
|
|
partial state updates, API null/closed projections, and AgentGUI resolution.
|
|
Verify a Codex live snapshot contains `goalPause` after goal pause, cancel,
|
|
and session recovery.
|
|
- References:
|
|
[provider.go](../../../packages/agent/store-sqlite/canonical/provider.go)
|
|
[session_metadata.go](../../../packages/agent/store-sqlite/session_metadata.go)
|
|
[capabilities.ts](../../../packages/agent/activity-core/src/capabilities.ts)
|
|
|
|
### AgentGUI rejects a pasted image as unsupported before send
|
|
|
|
- Symptom:
|
|
Pasting or dropping a supported PNG, JPEG, or WebP into a provider that
|
|
advertises `imageInput` fails with
|
|
`agent prompt image input is unsupported`. Desktop diagnostics may show
|
|
`agent.gui.composer.image_upload.resolved` with `hasPath = true`, followed by
|
|
a daemon failure at `service.send.prompt_validated`; no provider turn starts.
|
|
- Quick checks:
|
|
Confirm the submitted image block is path-backed after the desktop host
|
|
archives the draft. If the block has `path` but no `data`, `url`, or
|
|
`attachmentId`, verify the controller is using preflight validation rather
|
|
than the strict runtime validator before `PersistRequestContent` runs.
|
|
- Root cause:
|
|
A managed desktop path is an ingress staging source. The daemon must accept
|
|
it during capability preflight, then copy and hydrate it before runtime
|
|
execution. Applying the strict provider-content validator during preflight
|
|
rejects the path before the attachment store can canonicalize it.
|
|
- Fix:
|
|
Keep separate preflight and runtime image validators. Preflight accepts and
|
|
preserves the managed path for adapter capability checks. Runtime execution
|
|
remains strict and receives only the hydrated image representation. Do not
|
|
retain base64 in the renderer or move attachment persistence before provider
|
|
capability checks.
|
|
- Validation:
|
|
Cover the full path-backed chain: controller preflight accepts the path,
|
|
service execution receives hydrated data without a path, direct runtime
|
|
execution still rejects path-only content, and unsupported providers create
|
|
no attachment files. Run `go test ./packages/agent/daemon/runtime
|
|
./services/tuttid/service/agent`.
|
|
- References:
|
|
[prompt_content.go](../../../packages/agent/daemon/runtime/prompt_content.go)
|
|
[controller.go](../../../packages/agent/daemon/runtime/controller.go)
|
|
[service_send_input.go](../../../services/tuttid/service/agent/service_send_input.go)
|
|
|
|
### New extension conversation renders a pasted image as a file
|
|
|
|
- Symptom:
|
|
A new Agent Extension conversation renders a pasted PNG, JPEG, or WebP as a
|
|
file mention instead of an image preview, while an existing conversation may
|
|
render the same image correctly. Direct ACP initialization still advertises
|
|
image prompt support, but target-scoped Composer Options returns
|
|
`imageInput = false`.
|
|
- Quick checks:
|
|
Compare the ACP `initialize` response, the daemon runtime
|
|
`SessionStateSnapshot.Capabilities`, the service-facing Session, and the exact
|
|
target-scoped Composer Options response. If ACP advertises
|
|
`promptCapabilities.image = true` but the service-facing Session has an
|
|
unknown capability snapshot, inspect the daemon-to-service runtime adapter
|
|
before changing the renderer paste path.
|
|
- Root cause:
|
|
ACP negotiation correctly stored capabilities in the daemon runtime's typed
|
|
Session state, but the tuttid runtime adapter copied settings and untyped
|
|
runtime context without copying `Capabilities`. New extension conversations
|
|
use that adapter result for hidden Composer discovery, so image support was
|
|
lost before the signed extension capability intersection. Existing
|
|
conversations could appear healthy because their persisted activity
|
|
projection supplied the typed snapshot through a different path.
|
|
- Fix:
|
|
Preserve the daemon runtime's typed capability snapshot at the tuttid runtime
|
|
adapter boundary, then project the exact live Session snapshot into Composer
|
|
runtime evidence before reading or caching it. Treat a non-nil snapshot as
|
|
authoritative even when empty: an explicit empty live or persisted snapshot
|
|
must stop cache fallback, and an empty discovery snapshot is already a ready
|
|
result rather than a reason to poll until timeout. Retain exact Target,
|
|
installation, project, and settings matching and the signed-profile
|
|
intersection.
|
|
- Validation:
|
|
Cover adapter preservation, live and persisted capability projection,
|
|
explicit empty live and persisted snapshots overriding stale untyped/cache
|
|
values, empty discovery returning without polling, and mismatched extension
|
|
identities remaining fail-closed. Confirm repeated `core`,
|
|
`capabilities`, and `full` target-scoped Composer Options requests return
|
|
`imageInput = true` for both Kimi Code and Hermes when their ACP runtimes and
|
|
signed profiles advertise it.
|
|
- References:
|
|
[agent_runtime_adapter.go](../../../services/tuttid/agent_runtime_adapter.go)
|
|
[composer_runtime_context.go](../../../services/tuttid/service/agent/composer_runtime_context.go)
|
|
[composer_live_model_discovery.go](../../../services/tuttid/service/agent/composer_live_model_discovery.go)
|
|
|
|
### Historical Standard ACP session cannot send after its process was released
|
|
|
|
- Symptom:
|
|
Continuing an idle historical Session fails before a new Turn starts. An
|
|
image submission may report `agent prompt image input is unsupported` even
|
|
though the same Session previously advertised image input. A text submission
|
|
may reconnect but then fail a redundant settings RPC, or the following prompt
|
|
may end without assistant, thought, or tool output.
|
|
- Quick checks:
|
|
Confirm the canonical Session still has a provider Session ID while the
|
|
adapter has no live process. For images, look for validation failure without
|
|
a preceding provider process start. For text, inspect `initialize`: an ACP
|
|
agent may advertise legacy `agentCapabilities.loadSession: true` together
|
|
with object-form `sessionCapabilities.resume: {}` (some agents place that
|
|
object under `agentCapabilities.sessionCapabilities`). If Tutti then calls
|
|
`session/load`, historical updates are replayed even though Tutti already has
|
|
the transcript. Also check whether an unchanged persisted permission/Plan
|
|
selection is followed by `session/set_mode`.
|
|
- Root cause:
|
|
Image capability is negotiated by the live ACP handshake, but preflight used
|
|
the released adapter state before reconnecting. Separately, the ACP
|
|
capability parser only recognized boolean/string values. ACP declares newer
|
|
session capabilities by object presence, so `{}` was treated as unsupported
|
|
and the legacy `session/load` path won over `session/resume`. Kimi's current
|
|
response uses the nested placement, which is accepted by the compatibility
|
|
parser. Load replays
|
|
history; resume reattaches without replay. Resume also reasserted an unchanged
|
|
mode even though the provider Session owns restored state, which fails for
|
|
non-idempotent mode implementations.
|
|
- Fix:
|
|
Reconnect a released Standard ACP process before image capability preflight,
|
|
so unsupported images are still rejected before attachment persistence using
|
|
the fresh handshake. Recognize object-form session capabilities and prefer
|
|
`session/resume` when advertised. Use Tutti's persisted permission and Plan
|
|
selection to avoid reasserting the same mode after reattachment, while still
|
|
sending a setting changed while detached. Keep the behavior provider- and
|
|
platform-neutral; do not match a provider ID or error string.
|
|
- Validation:
|
|
Release a resumable Standard ACP Session and prove that image preflight starts
|
|
one replacement process before validation. Given an initialize response with
|
|
both legacy load support and `sessionCapabilities.resume: {}` in standard and
|
|
nested placement, prove Tutti selects `session/resume`. Prove the persisted unchanged mode is not reasserted
|
|
but an actual offline permission or Plan change is. Use a native Windows-shaped
|
|
CWD in the image case and cross-compile the runtime test package for Windows in
|
|
addition to executing the focused tests on POSIX.
|
|
- References:
|
|
[controller_live_sessions.go](../../../packages/agent/daemon/runtime/controller_live_sessions.go)
|
|
[acp_update_events.go](../../../packages/agent/daemon/runtime/acp_update_events.go)
|
|
[standard_acp_settings.go](../../../packages/agent/daemon/runtime/standard_acp_settings.go)
|
|
[standard_acp_session.go](../../../packages/agent/daemon/runtime/standard_acp_session.go)
|
|
|
|
### Remote Agent image reaches the provider as an unsupported URL
|
|
|
|
- Symptom:
|
|
A URL-backed PNG, JPEG, or WebP is accepted and appears in the user activity,
|
|
but Codex rejects the turn with `remote image URLs are not supported; use an
|
|
inline data URL instead`. Claude or standard ACP may instead receive no
|
|
usable image data.
|
|
- Quick checks:
|
|
Confirm the durable prompt block intentionally contains an HTTPS `url`, then
|
|
inspect the final provider payload. Codex must receive a `data:` URL; Claude
|
|
SDK and standard ACP must receive base64 `data`. Search every provider send
|
|
and guidance path for `materializeProviderPromptImagesAtBoundary`; the helper
|
|
and its unit test can remain green even when a refactor removes all production
|
|
callers. If the visible failure detail is
|
|
`download remote prompt image: request failed`, compare the URL with a normal
|
|
system HTTP client and inspect its DNS result. A URL that succeeds normally
|
|
but resolves to a VPN or transparent-proxy synthetic address indicates that
|
|
the runtime is bypassing or second-guessing the system network path.
|
|
- Root cause:
|
|
Remote image URLs are the durable transport representation, while current
|
|
Codex app-server, Claude SDK, and standard ACP provider wires require inline
|
|
data. A provider-adapter refactor can preserve the materialization helper but
|
|
omit its call sites in newly split turn files. Separately, pre-resolving and
|
|
classifying destination IPs at this boundary is incompatible with VPN, TUN,
|
|
split-DNS, and Fake-IP network stacks: their synthetic address is intentionally
|
|
not the public origin address, but the system transport still knows how to
|
|
reach the origin.
|
|
- Fix:
|
|
Materialize only at the final provider boundary. Keep the original URL-backed
|
|
content for activity projection, and convert the provider-only copy after
|
|
local slash/control handling but immediately before Codex `turn/start` or
|
|
`turn/steer`, Claude SDK `exec` or `guide`, and standard ACP
|
|
`session/prompt`. Use the repository proxy-aware HTTP client and leave DNS,
|
|
proxy, VPN, and connect-target selection to the system network stack; do not
|
|
pre-resolve, pin, or reject resolved IP ranges at this provider compatibility
|
|
boundary. Continue accepting only HTTPS URLs, require every redirect hop to
|
|
remain HTTPS, strip redirect `Referer` headers so signed URL query parameters
|
|
cannot cross hops, and keep timeout, response-size, and image MIME checks.
|
|
Admit an asynchronous Codex guidance continuation before publishing its
|
|
provisional provider turn, and settle that exact attempt when preparation
|
|
ends without starting a real provider turn. For standard ACP, publish the
|
|
original user activity and provider-turn start before remote materialization;
|
|
if preparation fails, complete that same provider turn as failed so the
|
|
message remains durable and root settlement cannot strand.
|
|
- Validation:
|
|
Exercise URL-only content through the real adapter request paths and assert
|
|
the captured provider payload contains inline data. Cover Codex send and
|
|
guidance, Claude SDK exec and guide, and standard ACP exec; do not rely only
|
|
on direct helper tests. Verify the production client can materialize through a
|
|
system-routed reserved address, reject a redirect that downgrades to HTTP, and
|
|
assert redirects omit signed-URL referrers. Cover failed and concurrent
|
|
guidance continuation admission so every published provisional attempt either
|
|
yields a real provider lifecycle or receives its matching provider-turn
|
|
completion. Also fail standard ACP materialization and assert the original
|
|
user message, turn start, and failed provider-turn completion are all emitted
|
|
without sending `session/prompt`.
|
|
- References:
|
|
[prompt_content.go](../../../packages/agent/daemon/runtime/prompt_content.go)
|
|
[codex_appserver_turn.go](../../../packages/agent/daemon/runtime/codex_appserver_turn.go)
|
|
[claude_sdk_execution.go](../../../packages/agent/daemon/runtime/claude_sdk_execution.go)
|
|
[standard_acp_turn.go](../../../packages/agent/daemon/runtime/standard_acp_turn.go)
|
|
|
|
### AgentGUI Stop reports no active turn after cancel succeeds
|
|
|
|
- Symptom:
|
|
Pressing Stop settles the AgentGUI turn as canceled, but the renderer also
|
|
logs a `workspace_operation_failed`/502 error whose daemon cause is
|
|
`agent session has no active turn`.
|
|
- Quick checks:
|
|
Compare daemon `agent_session.cancel.adapter_failed` with nearby activity
|
|
state patches. If the same turn reports `turnPhase = settled` and
|
|
`outcome = canceled` at the same timestamp, the cancel result won the event
|
|
race while the synchronous cancel RPC still observed a stale controller turn
|
|
record.
|
|
- Root cause:
|
|
The runtime controller and provider adapter keep separate active-turn views.
|
|
During cancel-after-settle races, the controller can still have a turn record
|
|
while the Codex app-server adapter has already cleared its active turn and
|
|
returns `ErrSessionNoActiveTurn`.
|
|
- Fix:
|
|
Treat `ErrSessionNoActiveTurn` from the controller active-turn cancel path as
|
|
an idempotent settled-turn result: clear the stale controller turn record,
|
|
reconcile any still-blocked view, and return without surfacing a 502.
|
|
- Validation:
|
|
Add controller coverage where `controller.turns` still has a record, the
|
|
stored session is already settled/canceled, and the adapter returns
|
|
`ErrSessionNoActiveTurn`.
|
|
- References:
|
|
[controller.go](../../../packages/agent/daemon/runtime/controller.go)
|
|
[controller_test.go](../../../packages/agent/daemon/runtime/controller_test.go)
|
|
|
|
### AgentGUI send blocked by active_turn after settled snapshot
|
|
|
|
- Symptom:
|
|
AgentGUI shows `turnPhase = settled` and no `activeTurnId`, but a follow-up
|
|
prompt fails with `agent session already has an active turn`, or the runtime
|
|
snapshot still reports `submitAvailabilityState = blocked` with
|
|
`reason = active_turn`.
|
|
- Quick checks:
|
|
Compare renderer state with `tuttid.log` submit traces. If `api.send.failed`
|
|
reports `agent session already has an active turn` after a settled/available
|
|
state patch, inspect whether the controller still has an in-memory `c.turns`
|
|
entry while the adapter lifecycle snapshot has already settled. For Codex
|
|
app-server sessions, also compare `turn/completed` notification timing with
|
|
the triggering `turn/start` RPC result; a stale provider turn id with no
|
|
active turn object indicates that the late result rebound an already-settled
|
|
slot.
|
|
- Root cause:
|
|
The controller's async turn registry is separate from adapter lifecycle
|
|
projection. Async execution must clear `c.turns` when the owning adapter
|
|
publishes a non-live `TurnLifecycleSnapshot`, even if the event type is not a
|
|
terminal `turn.completed`/`turn.failed` event. The settled session and the
|
|
registry cleanup must also become visible together; storing `ready` first
|
|
leaves a follow-up rejection window. Separately, app-server notifications can
|
|
settle a turn before the `turn/start` response is applied, so binding that
|
|
response without checking turn identity can recreate a stale active id.
|
|
- Fix:
|
|
Treat same-turn non-live lifecycle snapshots as async turn completion, in
|
|
addition to terminal event types and steered prompt messages. Clear the
|
|
matching controller turn record before publishing/storing the terminal
|
|
session view. Bind a provider turn id only while the exact active-turn object
|
|
that issued the request still owns the adapter slot.
|
|
- Validation:
|
|
Add controller coverage where an async adapter emits only a settled lifecycle
|
|
snapshot for the turn and no terminal event, then verify a follow-up `Exec`
|
|
no longer returns `ErrSessionActiveTurn`. Also cover a terminal snapshot that
|
|
waits for an open call and assert `ready` is never observable with an active
|
|
controller turn. For Codex, deliver `turn/completed` before the `turn/start`
|
|
result and verify the late result cannot restore the provider turn id. Run
|
|
`go test ./packages/agent/daemon/runtime`.
|
|
- References:
|
|
[controller.go](../../../packages/agent/daemon/runtime/controller.go)
|
|
[controller_test.go](../../../packages/agent/daemon/runtime/controller_test.go)
|
|
[codex_appserver_adapter.go](../../../packages/agent/daemon/runtime/codex_appserver_adapter.go)
|
|
[codex_appserver_adapter_test.go](../../../packages/agent/daemon/runtime/codex_appserver_adapter_test.go)
|
|
|
|
### AgentGUI loading disappears before active turn settles
|
|
|
|
- Symptom:
|
|
AgentGUI loses the in-progress/loading affordance while the app-server turn is
|
|
still active, normal composer sends hit the active-turn guard instead of
|
|
queueing, or a later terminal event arrives for a turn that the runtime
|
|
already cleared.
|
|
- Quick checks:
|
|
First compare the renderer `runtimeSession` and `sessionState` diagnostics.
|
|
If `runtimeSession.turnLifecycle.activeTurnId` is non-empty with a live phase
|
|
but `sessionState.turnLifecycle.phase = settled` and
|
|
`submitAvailabilityState = available`, the bug is in AgentGUI derived state:
|
|
the active composer/projection is trusting stale selected-session control
|
|
state over the runtime snapshot. Separately inspect app-server terminal
|
|
payloads. A `turn/completed` or `turn/failed` notification with an empty
|
|
provider turn id must not settle a bound active turn unless that turn was
|
|
explicitly adopted from a preceding goal-continuation `turn/started`.
|
|
- Root cause:
|
|
There are two distinct failure modes. In AgentGUI, the runtime activity
|
|
snapshot can be live while the selected session view/control state is stale;
|
|
composer loading, projection turn lifecycle, `canSubmit`, and local queue
|
|
decisions must prefer the runtime live lifecycle. In the Codex app-server
|
|
adapter, `settleActiveTurn` is allowed to adopt a mismatched provider turn id
|
|
only for the steer case where `turn/start` returned an unconfirmed stub id and
|
|
codex later completes the running turn with a non-empty provider id. Treating
|
|
an empty terminal id as a wildcard clears active turn state too early and
|
|
removes loading.
|
|
- Fix:
|
|
In AgentGUI, drive active projection, active live state, submit blocking, and
|
|
queue decisions from the live `AgentSessionEngine` returned by
|
|
`AgentGUIRuntime.getSessionEngine(...)` before falling back to
|
|
`activeSessionState`. Ordinary composer sends while busy should
|
|
queue; explicit send-now intents must use capability-selected native guidance
|
|
or exact-turn cancel-then-send. In the daemon,
|
|
keep the steer exception, but require the terminal provider id to be non-empty
|
|
and drop empty-id terminal notifications for bound active turns. Keep the
|
|
narrow exception for goal-adopted turns whose ownership came from
|
|
`turn/started`.
|
|
- Validation:
|
|
Keep tests for both sides: a stale settled `sessionState` plus live runtime
|
|
lifecycle should still render processing/loading, set `canSubmit = false`,
|
|
allow local queueing, and avoid direct `exec`; steered stub turns must settle
|
|
on the running turn's non-empty completion id; empty-id terminal notifications
|
|
must not settle confirmed or unconfirmed active turns; goal continuation must
|
|
still complete its adopted turn.
|
|
- References:
|
|
[useAgentGUINodeController.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUINodeController.ts)
|
|
[useAgentGUINodeController.spec.tsx](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUINodeController.spec.tsx)
|
|
[codex_appserver_turn_machine.go](../../../packages/agent/daemon/runtime/codex_appserver_turn_machine.go)
|
|
[codex_appserver_adapter_test.go](../../../packages/agent/daemon/runtime/codex_appserver_adapter_test.go)
|
|
|
|
### Codex app-server Start/Resume waits after a lifecycle notification
|
|
|
|
- Symptom:
|
|
Session start or resume reaches the provider, but Tutti waits for the
|
|
lifecycle RPC timeout even though Codex sent `thread/started`. A required MCP
|
|
server can show the same symptom when the only failure evidence is
|
|
`mcpServer/startupStatus/updated` with `status = failed` and there is no
|
|
stderr or JSON-RPC response.
|
|
- Quick checks:
|
|
Inspect the app-server wire trace for `thread/started` or
|
|
`mcpServer/startupStatus/updated` before the missing `thread/start` or
|
|
`thread/resume` response. Distinguish this from a completely silent provider:
|
|
without an authoritative lifecycle notification, process termination, or
|
|
transport error, the call must still time out rather than manufacture success.
|
|
The Codex adapter allows 90 seconds for `thread/start`; `thread/resume` and
|
|
the `turn/start` acknowledgement retain their 30-second budgets.
|
|
- Root cause:
|
|
App-server notifications and the response for their triggering RPC have no
|
|
safe application-order guarantee. Waiting only on the response leaves the
|
|
lifecycle call blocked when Codex has already published the authoritative
|
|
thread snapshot. A required MCP failure is terminal only when Codex reports
|
|
it through the lifecycle RPC itself. MCP stderr and startup-status
|
|
notifications are diagnostic state, not independent lifecycle failures:
|
|
optional MCP startup can fail while the Codex session is still usable. Child
|
|
terminal notifications can arrive before `receiverThreadIds` registers their
|
|
thread; dropping those unknown-thread terminal events loses the only
|
|
completion fact.
|
|
- Fix:
|
|
Complete only the active method-matched `thread/start` or `thread/resume` wait
|
|
from a valid `thread/started` snapshot. Do not fail that wait from MCP stderr
|
|
or `mcpServer/startupStatus/updated`; record the warning and keep the session
|
|
alive. A real Codex lifecycle RPC error, process exit, or transport error
|
|
remains terminal and is still propagated. Keep ordinary foreign-thread
|
|
progress dropped, but retain a bounded terminal notification until child
|
|
registration and replay it with child identity. Warnings without a `turnId`
|
|
use a session audit event instead of a turn-scoped message.
|
|
Keep confirmed provider turn-id fences; only the existing unconfirmed steer
|
|
stub and goal-adopted exceptions may settle from a different or empty id.
|
|
- Validation:
|
|
Run the notification-only Start/Resume wire test, the delayed MCP
|
|
failed-status tests, the turnless warning projection test, and
|
|
the child terminal-before-registration replay test.
|
|
Run the focused app-server suite with `-race` and the full
|
|
`go test ./packages/agent/daemon/runtime` package suite.
|
|
- References:
|
|
[codex_appserver_client.go](../../../packages/agent/daemon/runtime/codex_appserver_client.go)
|
|
[codex_appserver_event_routing.go](../../../packages/agent/daemon/runtime/codex_appserver_event_routing.go)
|
|
[codex_appserver_turn_machine.go](../../../packages/agent/daemon/runtime/codex_appserver_turn_machine.go)
|
|
[codex_appserver_lifecycle_test.go](../../../packages/agent/daemon/runtime/codex_appserver_lifecycle_test.go)
|
|
[codex_appserver_events_test.go](../../../packages/agent/daemon/runtime/codex_appserver_events_test.go)
|
|
|
|
### Busy-turn message insertion fails or ends without sending the prompt
|
|
|
|
- Symptom:
|
|
Sending a prompt with the composer guidance shortcut or choosing “send now”
|
|
on a queued prompt fails for ACP-backed agents, reports a turn-scoped
|
|
cancellation/guidance error, or cancels the turn without sending the prompt.
|
|
- Quick checks:
|
|
Inspect the canonical session capabilities and engine commands. Codex and
|
|
Claude sessions should advertise `activeTurnGuidance`; standard ACP sessions
|
|
should advertise `interrupt` without `activeTurnGuidance`. Confirm that no
|
|
renderer branch selects behavior from a provider ID.
|
|
- Root cause:
|
|
Message insertion is one product intent with two transport realizations.
|
|
Treating every provider as native guidance sends a same-turn request that the
|
|
standard ACP protocol does not define. Treating every provider as
|
|
cancel-then-send discards the same-Turn semantics offered by Codex and the
|
|
Claude SDK. Native guidance semantics are provider-specific: Codex steering
|
|
deliberately leaves the current response running, while the Claude SDK must
|
|
interrupt its active Query before it can reliably enqueue guidance.
|
|
- Fix:
|
|
Keep the prompt queue in the workspace `AgentSessionEngine`. Resolve send-now
|
|
from typed runtime capabilities: use native guidance when
|
|
`activeTurnGuidance` is true, with no canonical Turn cancel. The provider
|
|
adapter must preserve its native semantics on the same canonical Turn.
|
|
Claude uses its SDK interrupt before enqueueing the prompt and closes the old
|
|
response projections. Codex/Tutti Agent sends `turn/steer` with the exact
|
|
active provider Turn ID and does not interrupt or start a replacement Turn.
|
|
If no provider response remains but the canonical Turn is still active for
|
|
child work, Codex may start a provider continuation through `turn/start`.
|
|
Otherwise use
|
|
exact-turn cancel when `interrupt` is true, retain the prompt in the frontend
|
|
queue, and send it normally only after validated cancellation or authoritative
|
|
turn settlement. Route both the composer shortcut and queued-item action
|
|
through the same atomic engine transition.
|
|
- Validation:
|
|
Cover both entry points and both capability combinations. Native guidance must
|
|
emit a guidance send with no cancel. ACP fallback must emit cancel with no
|
|
prompt send, then emit one normal prompt send after cancellation settles. At
|
|
the Claude provider boundary, assert that the old response terminal and all
|
|
old thinking-stream terminals precede guided output. For Codex, assert that
|
|
active-response guidance sends exactly one `turn/steer` with the expected
|
|
provider Turn ID and sends neither `turn/interrupt` nor another `turn/start`.
|
|
- References:
|
|
[promptQueue.reducer.ts](../../../packages/agent/activity-core/src/engine/promptQueue.reducer.ts)
|
|
[sessionLifecycle.reducer.ts](../../../packages/agent/activity-core/src/engine/sessionLifecycle.reducer.ts)
|
|
[controller_exec.go](../../../packages/agent/daemon/runtime/controller_exec.go)
|
|
|
|
### Queued AgentGUI prompt stalls after no-active-turn failure
|
|
|
|
- Symptom:
|
|
A prompt submitted while an AgentGUI turn is busy appears in the local queue
|
|
or as an optimistic user row, but does not start after the previous turn
|
|
settles. Submit traces show the same `clientSubmitId` first failing with
|
|
`errorReason = agent.no_active_turn`, then succeeding only after a later
|
|
manual retry or another queue-draining trigger.
|
|
- Quick checks:
|
|
Search desktop and daemon logs for the queued `clientSubmitId`. A local queue
|
|
acceptance has `send_input.requested` with `queued=true` and
|
|
`optimistic_user_message_painted`. The failure pattern is a delayed
|
|
`renderer_adapter.send.failed` with `errorCode=invalid_request` and
|
|
`errorReason=agent.no_active_turn`, plus daemon `runtime_adapter.exec.failed`
|
|
with `agent session has no active turn`.
|
|
- Root cause:
|
|
The daemon exposes the domain-specific reason as the protocol error
|
|
`reason`, while the Agent session engine previously kept only the generic
|
|
`errorCode`. The queue reducer therefore treated the race like a permanent
|
|
send failure, set `failedPromptId`, and stopped automatic drain until
|
|
send-now or another retry path cleared the failure.
|
|
- Fix:
|
|
Preserve protocol `reason` on `EngineCommandResultIntent`. For
|
|
`queue/sendPrompt` failures whose reason is `agent.no_active_turn`, clear the
|
|
in-flight send, request a session reconcile, and skip same-reducer drain so
|
|
the queued prompt retries only after canonical state refresh. Keep ordinary
|
|
send failures blocked until explicit send-now retry.
|
|
- Validation:
|
|
Add reducer coverage where a queued send fails with
|
|
`errorReason = agent.no_active_turn`: it should emit one
|
|
`session/reconcile`, leave the prompt queued without `failedPromptId`, and
|
|
avoid an immediate second `queue/sendPrompt` until a later canonical lifecycle
|
|
update. Keep the existing generic failure test blocked until send-now.
|
|
- References:
|
|
[promptQueue.reducer.ts](../../../packages/agent/activity-core/src/engine/promptQueue.reducer.ts)
|
|
[effectExecutor.ts](../../../packages/agent/activity-core/src/engine/effectExecutor.ts)
|
|
[daemon_agent_submit_handlers.go](../../../services/tuttid/api/daemon_agent_submit_handlers.go)
|
|
|
|
### Cursor or OpenCode turn settles before late ACP activity arrives
|
|
|
|
- Symptom:
|
|
A Cursor or OpenCode turn appears complete, then a delayed tool or permission
|
|
event is projected onto the old turn; the composer may relock, persistence
|
|
may reject a settled-to-running transition, or a synthetic turn may appear.
|
|
A Cursor background Task may also appear launched successfully while its
|
|
detached child later requests permissions that never reach the UI.
|
|
- Quick checks:
|
|
Correlate `session/prompt` response timing with later `session/update` or
|
|
`session/request_permission` messages. If the prompt response arrived first,
|
|
the late event no longer has an active canonical turn owner. Also verify the
|
|
terminal report is `root_provider_turn.completed`, not a direct canonical
|
|
`turn.completed` from the ACP adapter. For Cursor Task/subagent probes,
|
|
correlate `agent_session.cursor.task_tool_update` with
|
|
`agent_session.cursor.task_extension`; the latter records only redacted
|
|
identity, ordering, field-presence, and duration facts. A background Task
|
|
tool result with `isBackground=true` and a very short duration is a launch
|
|
acknowledgement, not child terminal evidence. Permission requests arriving
|
|
after the root prompt result confirm the child is still running out of scope.
|
|
- Root cause:
|
|
Standard ACP has one active prompt handler and a session-level fallback
|
|
handler. Reusing a recent turn ID in the fallback path treats temporal
|
|
proximity as ownership. That can reopen a settled root or fabricate a turn,
|
|
and ordinary tool display fields do not supply the stable child identity and
|
|
terminal lifecycle required for a provider-native child session. Cursor's
|
|
background Task implementation records eventual completion in an internal
|
|
work registry, but Cursor ACP `2026.07.01-41b2de7` does not expose that
|
|
terminal to Tutti.
|
|
- Fix:
|
|
Route every Standard ACP prompt terminal through the daemon-owned root
|
|
provider lifecycle. Drop turn-scoped tool/message updates outside the active
|
|
prompt call and reject out-of-band permission callbacks; never synthesize a
|
|
canonical turn. Keep Cursor/OpenCode root-only until their ACP transports
|
|
expose stable child, parent, and child-terminal facts. Cursor Agent
|
|
`2026.07.01-41b2de7` does not merge `--plugin-dir` hooks into ACP, so the
|
|
dormant `preToolUse` Task guard is deliberately not advertised or
|
|
materialized. Do not treat background Task as supported or blocked, do not
|
|
write hooks into user/project configuration, and do not settle a detached
|
|
child from a guessed timeout.
|
|
- Validation:
|
|
Cover Cursor and OpenCode normal completion with
|
|
`root_provider_turn.started/completed` and no canonical terminal from the
|
|
adapter. Deliver a late tool update and late permission after prompt return
|
|
and verify neither creates a turn, interaction, or child session. Make a
|
|
`session/cancel` write fail and verify the error reaches the caller. Also
|
|
fail an automatic permission-response write and verify the adapter does not
|
|
report a false approval while the provider is still waiting. Verify the
|
|
dormant Cursor hook allows foreground Task inputs, rejects snake-case and
|
|
camel-case background flags, does not match flag-like text inside the Task
|
|
prompt, and fails closed for malformed input; separately verify the current
|
|
ACP plugin manifest does not advertise or materialize that hook.
|
|
- References:
|
|
[standard_acp_turn.go](../../../packages/agent/daemon/runtime/standard_acp_turn.go)
|
|
[standard_acp_stream.go](../../../packages/agent/daemon/runtime/standard_acp_stream.go)
|
|
[provider-native subagents](../../specs/2026-07-15-provider-native-subagents.md)
|
|
|
|
### Standard ACP prompts stay queued after a canceled turn
|
|
|
|
- Symptom:
|
|
A Standard ACP Agent finishes or cancels a visible turn, but every later
|
|
message reports that it was queued for the next turn and is never consumed.
|
|
The provider process remains alive and may still report its previous prompt
|
|
as running even though Tutti already presents the Session as idle.
|
|
- Quick checks:
|
|
Correlate Tutti's `session/cancel` notification, the outstanding
|
|
`session/prompt` response, and the next `session/prompt` request. If the next
|
|
request is dispatched before the canceled prompt returns, the provider may
|
|
queue it behind state that no worker will drain. Distinguish this provider
|
|
queue from AgentGUI's local queue by confirming that the later request
|
|
crossed the ACP transport.
|
|
- Root cause:
|
|
`session/cancel` is only cancellation delivery; it is not proof that the
|
|
outstanding `session/prompt` call has settled. Releasing Tutti's local turn
|
|
fence immediately after writing the notification lets another prompt enter
|
|
the same provider process while its internal running state is still true.
|
|
- Fix:
|
|
Keep a per-Session active-prompt drain barrier. Capture the exact active
|
|
prompt, deliver `session/cancel`, and mark that same prompt canceled only
|
|
after delivery succeeds; a failed notification must leave the prompt's
|
|
eventual result authoritative. Once cancellation is accepted, check it
|
|
before any retriable-error auto-continue branch and wait for the outstanding
|
|
prompt result before accepting another turn. If the provider does not settle
|
|
within the bounded drain window, close only the transport and let the next
|
|
command resume the provider Session in a fresh process. Do not call
|
|
destructive `session/close` during this recovery.
|
|
- Validation:
|
|
Use a deterministic ACP transport fixture whose first prompt returns only
|
|
after `session/cancel`. Assert that `Cancel` cannot return before that prompt
|
|
drains, that the first turn settles as canceled, and that a second prompt on
|
|
the Session completes. Also inject notification failure and prove the active
|
|
prompt remains uncanceled; race cancellation with a retriable tail and prove
|
|
no second prompt is sent. Run the focused test under Go's race detector.
|
|
- References:
|
|
[standard_acp_turn.go](../../../packages/agent/daemon/runtime/standard_acp_turn.go)
|
|
[standard_acp_turn_test.go](../../../packages/agent/daemon/runtime/standard_acp_turn_test.go)
|
|
|
|
### Codex goal stops after a turn while the goal remains active
|
|
|
|
- Symptom:
|
|
A `/goal` completes or fails one turn, remains `active`, but never starts a
|
|
continuation turn. The conversation looks idle even though the goal banner
|
|
still says it is running.
|
|
- Quick checks:
|
|
Enable `TUTTI_TEST_LOGS=1` for a focused runtime reproduction and compare
|
|
`agent_session.app_server.goal.status_changed` with
|
|
`agent_session.app_server.goal.continuation_nudge`. If the first turn settles
|
|
before the initial `thread/goal/set` response records the active goal, an
|
|
eager scheduling check can return without ever creating the continuation
|
|
timer.
|
|
- Root cause:
|
|
App-server notifications and the response for their triggering RPC do not
|
|
have a safe application-order guarantee. A `turn/completed` notification may
|
|
settle the first goal turn while `thread/goal/set` is still in flight, so
|
|
local goal state can still be empty when the settle path schedules its nudge.
|
|
- Fix:
|
|
Before sending a goal-setting RPC that can start a turn, record a local
|
|
`active` goal snapshot with the requested objective. This makes goal
|
|
activation causally visible to terminal notifications that overtake the RPC
|
|
response. Restore the previous local goal if the RPC fails; on success,
|
|
replace the provisional snapshot with the authoritative response. The
|
|
continuation timer must still re-read both active-turn and goal state after
|
|
its grace window before sending a nudge.
|
|
- Validation:
|
|
Keep a scripted protocol test that deliberately delivers goal turn
|
|
notifications before the `thread/goal/set` result, then verify the next turn
|
|
is adopted. Cover both a clean first turn and a failed mid-goal turn with
|
|
repeated focused runs; use event channels rather than polling shared slices.
|
|
- References:
|
|
[codex_appserver_goal.go](../../../packages/agent/daemon/runtime/codex_appserver_goal.go)
|
|
[codex_appserver_adapter_test.go](../../../packages/agent/daemon/runtime/codex_appserver_adapter_test.go)
|
|
|
|
### Codex goal shows active but produces no response
|
|
|
|
- Symptom:
|
|
Sending `/goal …` updates the Goal banner to active, but no assistant text or
|
|
tool activity appears. Logs show `turn/started`, followed shortly by
|
|
`agent_session.app_server.goal.turn_provenance_unproven` and an exact
|
|
`turn/interrupt`.
|
|
- Quick checks:
|
|
Inspect the installed Codex app-server schema and the raw
|
|
`thread/goal/updated` payload. In affected versions, `turnId` is optional and
|
|
the external `thread/goal/set` update omits it. Verify the Goal operation and
|
|
generation fingerprint were persisted successfully before investigating
|
|
transport or model failures.
|
|
- Root cause:
|
|
Tutti required `thread/goal/updated.turnId` as the only Goal-to-Turn
|
|
correlation signal. Codex externally applies the Goal, emits a session-scoped
|
|
update without `turnId`, then starts the continuation Turn. The adapter
|
|
therefore treated valid work as unproven and deliberately interrupted it
|
|
after the provenance grace window.
|
|
- Fix:
|
|
Keep exact turn-scoped Goal evidence as the preferred path. For versions that
|
|
omit `turnId`, use an adapter-local, single-use continuation claim scoped to
|
|
the successful Goal RPC's immutable operation/revision. Chain the next claim
|
|
only from settlement of an adopted Goal Turn, serialize Goal control against
|
|
ordinary submit setup, and invalidate the claim on revision change, inactive
|
|
status, restart, or multiple unowned Turns. Never bind an arbitrary unowned
|
|
Turn from the mutable current Goal snapshot and do not fix this by merely
|
|
extending the provenance timeout.
|
|
- Validation:
|
|
Reproduce the real protocol order
|
|
`goal/set response -> goal/updated without turnId -> turn/started` and verify
|
|
assistant output is persisted without an interrupt. Also cover chained
|
|
continuation Turns without another Goal RPC and a delayed old Turn after a
|
|
newer revision, which must still be interrupted.
|
|
- References:
|
|
[codex_appserver_goal_provenance.go](../../../packages/agent/daemon/runtime/codex_appserver_goal_provenance.go)
|
|
[codex_appserver_goal.go](../../../packages/agent/daemon/runtime/codex_appserver_goal.go)
|
|
[controller_exec.go](../../../packages/agent/daemon/runtime/controller_exec.go)
|
|
[codex_appserver_adapter_test.go](../../../packages/agent/daemon/runtime/codex_appserver_adapter_test.go)
|
|
|
|
### Active Codex goal has no elapsed timer
|
|
|
|
- Symptom:
|
|
The Goal banner shows an active objective but no elapsed value, even though
|
|
Codex continues working and its Goal database reports non-zero usage time.
|
|
- Root cause:
|
|
Codex exposes `timeUsedSeconds` and `tokensUsed`, while Tutti's canonical
|
|
session Goal contract exposes `durationMs` and `tokens`. Passing provider
|
|
fields through runtime context causes the typed durable session boundary to
|
|
discard them. A newly created Goal may also omit canonical `durationMs`
|
|
because zero is an optional counter.
|
|
- Fix:
|
|
Normalize provider counters in the Codex adapter before publishing session
|
|
state or Goal observations. The renderer reads only `durationMs`, hides time
|
|
while the Goal is an optimistic projection, and starts only after canonical
|
|
provider state arrives. If that canonical state omits the optional zero
|
|
baseline, start at zero and advance locally between server updates.
|
|
- Validation:
|
|
Verify provider-to-canonical normalization with non-zero counters, verify an
|
|
optimistic Goal shows no time, verify canonical confirmation adopts the
|
|
provider duration and advances, and verify a paused Goal without a duration
|
|
still omits the timer.
|
|
- References:
|
|
[codex_appserver_goal.go](../../../packages/agent/daemon/runtime/codex_appserver_goal.go)
|
|
[AgentGoalBanner.tsx](../../../packages/agent/gui/agent-gui/agentGuiNode/AgentGoalBanner.tsx)
|
|
|
|
### Codex goal reappears after pause, edit, or clear
|
|
|
|
- Symptom:
|
|
A goal control succeeds, but the banner shortly returns to an older objective
|
|
or status; a cleared goal can reappear as paused.
|
|
- Quick checks:
|
|
Compare the startup background `thread/goal/get` with later goal-control RPCs.
|
|
If the get began before the control and completed afterwards, inspect whether
|
|
its older snapshot was applied unconditionally.
|
|
- Root cause:
|
|
Startup restores the persisted thread goal asynchronously. Its response can
|
|
race with newer user controls or provider goal notifications, so arrival
|
|
order is not a safe freshness signal.
|
|
- Fix:
|
|
Version the session goal state. Capture the session identity and revision
|
|
before the startup fetch, and apply its result only when both are unchanged;
|
|
increment the revision on every update and clear.
|
|
- Validation:
|
|
Capture a startup refresh guard, clear the goal, then attempt to apply the
|
|
older paused snapshot and verify it is rejected.
|
|
- References:
|
|
[codex_appserver_adapter.go](../../../packages/agent/daemon/runtime/codex_appserver_adapter.go)
|
|
[codex_appserver_events.go](../../../packages/agent/daemon/runtime/codex_appserver_events.go)
|
|
[codex_appserver_adapter_test.go](../../../packages/agent/daemon/runtime/codex_appserver_adapter_test.go)
|
|
|
|
### Clearing a goal hides Stop or appends a provider acknowledgement
|
|
|
|
- Symptom:
|
|
Clearing a goal while its current turn is still running leaves the composer
|
|
on a non-clickable send spinner. The transcript treats `/goal clear` as a
|
|
user Turn followed by a new processing row even though no new user work
|
|
started. Claude Code may instead append a native `Goal cleared: …` assistant
|
|
message at the bottom of the transcript after the interrupted turn.
|
|
- Quick checks:
|
|
Inspect the AgentGUI clear handler and the engine pending-submit records. If
|
|
clear calls `executePrompt` with an immediate `/goal clear`, the control has
|
|
entered the normal message pipeline and received a pseudo turn identity. For
|
|
Claude Code, correlate the bottom assistant message's turn ID with the
|
|
adapter-generated turn carrying the native clear command.
|
|
- Root cause:
|
|
Goal clear changes thread metadata rather than submitting user work. For a
|
|
provider such as Codex that leaves the active turn running, submitting clear
|
|
as a prompt creates a pending submit without a real provider turn. That local
|
|
submit owns the send spinner and its visible user message becomes the last
|
|
timeline turn, so canonical processing is projected under the wrong item.
|
|
Claude Code instead interrupts the live goal turn and requires a separate
|
|
native command turn to execute clear; projecting that control turn's provider
|
|
acknowledgement as ordinary assistant content creates an unrelated transcript
|
|
row at the bottom.
|
|
- Fix:
|
|
Route every goal action, including clear, through the dedicated runtime
|
|
goal-control API. Do not create a user Turn message, pending submit, or pseudo
|
|
turn. A surface may project the durable session audit as a dedicated
|
|
`goal-control` row, but that row must carry no Turn ID and must not affect
|
|
processing ownership or Turn counts.
|
|
If the provider needs an internal clear-command turn, register its generated
|
|
turn ID and suppress only that turn's assistant/thinking acknowledgement at
|
|
the runtime-adapter boundary before persistence. Do not filter by localized
|
|
acknowledgement text and do not move the message into the interrupted turn.
|
|
Preserve goal/session updates and terminal cleanup, but do not register the
|
|
internal command as a root provider turn or feed its terminal into canonical
|
|
root settlement.
|
|
Keep Stop and processing derived from the canonical active turn, and report a
|
|
successful clear with a localized transient toast. Render that toast in an
|
|
AgentGUI detail-scoped viewport and use UI System themed surface, foreground,
|
|
and border tokens so it centers within the content area and follows the
|
|
active light or dark theme instead of using the inverted neutral toast style.
|
|
- Validation:
|
|
Clear a goal while a turn is running and verify the goal-control API is called
|
|
without an engine submit dispatch. If the clear control is visible, it must
|
|
be a `goal-control` row with no Turn ID; the original processing row must
|
|
remain in place, and Stop must remain clickable until the active turn settles
|
|
or is interrupted. For Claude Code, verify the native acknowledgement is
|
|
absent both live and after reload, while identical text from an ordinary
|
|
assistant turn remains visible.
|
|
- References:
|
|
[useAgentGUISubmitInteractionActions.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUISubmitInteractionActions.ts)
|
|
[claude_sdk_goal.go](../../../packages/agent/daemon/runtime/claude_sdk_goal.go)
|
|
[claude_sdk_events.go](../../../packages/agent/daemon/runtime/claude_sdk_events.go)
|
|
[agent-gui-node.md](../../architecture/agent-gui-node.md)
|
|
|
|
### Agent session stays loading after a completed turn
|
|
|
|
- Symptom:
|
|
AgentGUI shows the assistant response as completed, but the conversation or
|
|
sidebar remains in a loading/running state. Desktop logs may contain
|
|
`agent.activity.store.session_version_regression` where the previous session
|
|
is `settled`/`available` and the next session is older
|
|
`running`/`active_turn`.
|
|
- Quick checks:
|
|
Compare the desktop `reconcile.state_fetch.resolved` session timestamp with
|
|
the latest inline `state_patch` timestamp. In `tuttid.log`, check whether
|
|
runtime emitted a terminal `turn_phase=settled` event before the fetch
|
|
response was applied.
|
|
- Root cause:
|
|
Activity projection can accept and broadcast a newer completed state while
|
|
`GetWorkspaceAgentSession` still prefers an older live runtime snapshot for
|
|
the same session. The projection store has timestamp regression protection,
|
|
but the service read path can bypass it when a runtime session is present.
|
|
- Fix:
|
|
In service read paths, compare persisted projection freshness against the
|
|
runtime snapshot. If persisted state is newer, return the projected session
|
|
state and synthesize non-live turn lifecycle/submit availability instead of
|
|
exposing the stale runtime active turn.
|
|
- Validation:
|
|
Add service coverage where runtime reports `working/running/active_turn` with
|
|
an older `UpdatedAtUnixMS`, while persisted state reports
|
|
`completed/idle/available` with a newer `LastEventUnixMS`. Validate both
|
|
`Get` and `List` do not return the old active turn. Run
|
|
`go test ./services/tuttid/service/agent`.
|
|
- References:
|
|
[service_session.go](../../../services/tuttid/service/agent/service_session.go)
|
|
[service.go](../../../services/tuttid/service/agent/service.go)
|
|
[service_session_list.go](../../../services/tuttid/service/agent/service_session_list.go)
|
|
|
|
### Remote session stays planning while the provider already replied
|
|
|
|
- Symptom:
|
|
A remote or cloud-backed AgentGUI conversation remains on its planning or
|
|
working placeholder after the provider has already settled the Turn. Reading
|
|
the authoritative Session directly reports `ready` with no active Turn, and
|
|
opening the Session event stream immediately reveals the missing assistant
|
|
response.
|
|
- Quick checks:
|
|
First compare authoritative Session state with the renderer projection. Then
|
|
inspect host access logs for the exact Session event-stream subscription. If
|
|
the provider settled but the AgentGUI surface never requested the stream,
|
|
increasing HTTP or activation timeouts cannot repair the stale projection.
|
|
- Root cause:
|
|
The focused conversation was hydrated once but did not retain the host's
|
|
optional Session synchronization lease. Hosts that use that lease to keep a
|
|
per-Session event stream open therefore receive no terminal state or message
|
|
events until another read happens to reconcile the Session.
|
|
- Fix:
|
|
Keep synchronization ownership in the shared focused-conversation controller.
|
|
Acquire the exact Session lease on focus, keep repeated selection idempotent,
|
|
release the previous lease on switch or clear, and release the final lease on
|
|
disposal. Keep the host responsible for transport and authoritative
|
|
reconciliation; do not add provider-specific polling or longer timeouts.
|
|
- Validation:
|
|
Add controller coverage for acquire, repeated selection, switch, clear, and
|
|
disposal. In the affected host, verify that selecting the conversation opens
|
|
the exact Session stream and that a terminal event updates both state and
|
|
transcript without a manual refresh.
|
|
- References:
|
|
[agentConversationMessageController.ts](../../../packages/agent/gui/agentConversationMessageController.ts)
|
|
[useAgentConversationMessagePaging.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentConversationMessagePaging.ts)
|
|
[agent-activity-packages.md](../../architecture/agent-activity-packages.md)
|
|
|
|
### AgentGUI pin or unpin appears stuck for a live session
|
|
|
|
- Symptom:
|
|
Pinning or unpinning a conversation backed by a live runtime succeeds in the
|
|
daemon, but the rail does not move the row or update its action until a later
|
|
list refresh. Old conversations without a live runtime update immediately. In
|
|
another variant, the pin command completes quickly but the whole rail becomes
|
|
disabled or changes to a skeleton while section membership refreshes. A third
|
|
variant renders the mutation in two steps: unpin first removes the pinned row,
|
|
then removes the pinned section and inserts the ordinary row; delete first
|
|
shrinks the section, then shrinks and refills it.
|
|
- Quick checks:
|
|
Correlate `pin_result` with `agent.activity.store.session_version_regression`.
|
|
Compare the command response's `updatedAtUnixMs` with the engine's current
|
|
session version, then inspect the exported session for a newer
|
|
`pinnedAtUnixMs`. A fast command carrying the new pin value but an older
|
|
`updatedAtUnixMs` identifies a stale runtime projection, not a slow database
|
|
write. If the pin value applies promptly, correlate
|
|
`agent_gui.conversation_rail.first_pages_slow` with
|
|
`workspace.agent_session.sections.list_slow`. The renderer event should report
|
|
`refreshReason=membership_change`; the daemon event separates
|
|
`projects_ms`, `store_ms`, and `hydrate_ms` and reports current/non-empty
|
|
project counts, target-scoped visible sessions, and returned first-page rows.
|
|
- Root cause:
|
|
Durable metadata updates advance the persisted session timestamp. When a live
|
|
runtime session is also present, the service merges persisted metadata such
|
|
as `pinnedAtUnixMs` into the runtime projection. If that merge keeps the older
|
|
runtime timestamp, the frontend's monotonic session reducer correctly rejects
|
|
the whole stale response, including the new pin value.
|
|
When the entity update is accepted, pin membership still requires
|
|
authoritative pinned and ordinary first-page reads. Triggering an aggregate
|
|
section query or workspace activity load turns that targeted revalidation
|
|
into a multi-second blocking reload on large histories. Rendering the updated
|
|
or tombstoned canonical entity against the previous page membership creates
|
|
the two-step variant: entity projection changes before the authoritative page
|
|
replacement knows the final bounded list and refill row.
|
|
- Fix:
|
|
Merge session freshness monotonically across runtime and persistence using
|
|
the newer timestamp. Pin responses that advance the session version must also
|
|
include protocol-v2 active/latest turn state so accepting the metadata update
|
|
cannot clear a running turn. Do not weaken frontend version checks or hide the
|
|
mismatch behind delayed refetches. For an accepted membership update, compare
|
|
canonical before/after membership and request only pinned plus the exact
|
|
ordinary section page. Route pin and delete through engine mutation intents;
|
|
the command result and canonical follow-up must drain before subscribers are
|
|
notified. Make the rail query controller the sole publisher of a composite
|
|
entity-plus-membership snapshot, retain its previous committed value while
|
|
targeted reads run, and publish the complete next value once. The view must
|
|
not subscribe to live engine rows separately or keep stale section snapshots.
|
|
Do not inspect mutation history from the Rail; compare canonical membership
|
|
before and after each engine notification. Keep the committed snapshot on
|
|
targeted failure and lock membership actions until a scoped authoritative
|
|
refresh succeeds. Do not call
|
|
`listSessionSections` or workspace `load` from delete, pin/unpin, or rename.
|
|
Lock scope-sensitive actions only while displayed membership belongs to a
|
|
different or unresolved scope; do not patch daemon-owned membership locally.
|
|
- Validation:
|
|
Cover a live runtime session whose persisted pin update is newer, a newer
|
|
runtime snapshot that must not regress, and a running turn that remains
|
|
attached to the pin response. Run `go test ./services/tuttid/service/agent`
|
|
plus daemon lint, tests, and build when daemon persistence changed. Add
|
|
reducer interleaving coverage proving command success and canonical follow-up
|
|
share one engine notification. Add controller coverage proving same-scope
|
|
pin/unpin calls only the pinned and exact ordinary page endpoints, keeps the
|
|
old composite projection until both resolve, publishes once, and never calls
|
|
the aggregate section endpoint.
|
|
- References:
|
|
[service.go](../../../services/tuttid/service/agent/service.go)
|
|
[service_session.go](../../../services/tuttid/service/agent/service_session.go)
|
|
[sessionEntities.reducer.ts](../../../packages/agent/activity-core/src/engine/sessionEntities.reducer.ts)
|
|
[AgentGUIConversationRailQueryController.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/AgentGUIConversationRailQueryController.ts)
|
|
|
|
### AgentGUI Batch delete sessions does nothing
|
|
|
|
- Symptom:
|
|
The project or Chats overflow menu opens, but choosing **Batch delete
|
|
sessions** neither opens the confirmation dialog nor sends a delete request.
|
|
- Quick checks:
|
|
Look for
|
|
`agent.gui.conversation_batch_delete.capability_incomplete` in the host's
|
|
AgentGUI diagnostics. Its `missingMethods` field identifies whether the host
|
|
omitted `listSessionSectionDeletionCandidates` or `deleteSessionsBatch`. If
|
|
the event is absent and the action is enabled, correlate the candidate-list
|
|
and batch-delete transport requests before investigating the view.
|
|
- Root cause:
|
|
Batch deletion is a two-stage runtime capability: AgentGUI first resolves the
|
|
authoritative session ids for the rail section and only then submits the
|
|
selected ids. A host that manually assembled `AgentGUIRuntime` could
|
|
expose the mutation but omit the candidate query. The old optional-method
|
|
checks then returned an empty candidate list, making a valid click look like
|
|
a no-op.
|
|
- Fix:
|
|
Install the complete runtime cohort from
|
|
`@tutti-os/agent-gui/conversation-rail-runtime` and keep only transport DTO
|
|
mapping in the host adapter. Do not add view-local candidate discovery or
|
|
infer membership from loaded rows. AgentGUI treats a partial two-method
|
|
contract as unavailable, disables the action, and reports the missing method.
|
|
- Validation:
|
|
Cover the host composition with both batch-deletion methods, verify the shared
|
|
factory forwards the exact candidate and delete inputs, and cover a partial
|
|
runtime contract as disabled with one diagnostic. Run the Agent GUI package
|
|
tests and the host activity-runtime composition tests.
|
|
- References:
|
|
[agentConversationRailRuntime.ts](../../../packages/agent/gui/agentConversationRailRuntime.ts)
|
|
[useAgentGUIConversationRailQuery.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUIConversationRailQuery.ts)
|
|
[createDesktopAgentActivityRuntime.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/createDesktopAgentActivityRuntime.ts)
|
|
|
|
### AgentGUI model switch changes defaults but not the active session
|
|
|
|
- Symptom:
|
|
A user selects a different AgentGUI model, but the next provider call still
|
|
uses the previous model. The target-default patch may be acknowledged while
|
|
`workspace_agent_sessions.settings_json`, `runtimeContext.model`, or
|
|
app-server `turn/start` still show the old model. For an Agent Extension, the
|
|
selected model may also change back to Auto as soon as a new session is
|
|
created, even though the durable session row contains the requested model.
|
|
- Quick checks:
|
|
Search desktop and daemon logs for the full settings chain:
|
|
`agent.gui.composer_settings.default_only`,
|
|
`agent.gui.composer_settings.update_requested`,
|
|
`workspace.agent_session.settings.update_requested`,
|
|
`agent_session.settings.update.requested`,
|
|
`agent_session.app_server.settings.applied`, and
|
|
`agent_session.app_server.turn_start.params`. Also distinguish the dedicated
|
|
defaults patch intent from the Session update. If only the defaults ack is
|
|
present, the UI remembered a future target default but did not update the
|
|
active Session. If
|
|
daemon settings update completed but `turn_start.params.model` is old or
|
|
empty, inspect the app-server adapter path. If persistence and the provider
|
|
request both contain the selected model but the daemon session response omits
|
|
`settings.model`, inspect the service projection before debugging the
|
|
renderer selector.
|
|
- Root cause:
|
|
AgentGUI has two distinct composer surfaces. The target home composer writes
|
|
remembered defaults and a sparse local display draft. An active conversation composer must
|
|
additionally call `updateSessionSettings`; Codex app-server providers then
|
|
apply model changes as per-turn overrides on the next `turn/start`, not to an
|
|
already-running turn. If the daemon applies the settings but the update
|
|
response still reports the old model, check the service merge path:
|
|
`serviceSessionWithPersistedFreshness` must not let a newer activity
|
|
projection snapshot overwrite live runtime settings after an explicit
|
|
settings update. For extension-owned open provider IDs, established runtime
|
|
and persisted sessions must use open-provider-aware normalization. Applying
|
|
the closed built-in composer registry to an ID such as `acp:<extension>`
|
|
produces an empty built-in provider, clamps the model, and makes the UI
|
|
correctly render Auto from an already-corrupted session projection.
|
|
- Fix:
|
|
Preserve the dedicated target-default patch path, but make active-session model changes
|
|
observable at every layer. Do not conclude that a provider ignored the model
|
|
until the logs show the active session settings update reached the daemon and
|
|
the following `turn/start` carried the requested model. Keep closed
|
|
normalization for unverified composer requests, but preserve provider-owned
|
|
settings when projecting or resuming a session that was already authorized
|
|
through an Agent Target.
|
|
- Validation:
|
|
Reproduce by switching a model in a running session and sending a follow-up.
|
|
Confirm the logs include the update chain above and that
|
|
`workspace.agent_session.settings.update_completed` reports the requested
|
|
model and the next `turn/start` carries it. If the persisted
|
|
`workspace_agent_sessions.settings_json.model` is older while the runtime is
|
|
live, `Get` responses should still expose live runtime settings instead of
|
|
the stale projection value. Add a service regression with a generic open
|
|
provider ID and assert `serviceSession` retains its model; also assert an
|
|
invalid provider still loses stale settings.
|
|
- References:
|
|
[useAgentGUINodeController.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUINodeController.ts)
|
|
[createDesktopAgentActivityRuntime.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/createDesktopAgentActivityRuntime.ts)
|
|
[workspaceAgentActivityService.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/workspaceAgentActivityService.ts)
|
|
[service_session.go](../../../services/tuttid/service/agent/service_session.go)
|
|
[controller.go](../../../packages/agent/daemon/runtime/controller.go)
|
|
[codex_appserver_adapter.go](../../../packages/agent/daemon/runtime/codex_appserver_adapter.go)
|
|
|
|
### AgentGUI shows the selected settings but a new session does not inherit them
|
|
|
|
- Symptom:
|
|
The home composer continues to show the selected model, permission,
|
|
reasoning effort, or speed, but closing the Agent window, opening another
|
|
window, restarting Tutti, or creating a Session restores an older value. A
|
|
running Session may still use the selected value, which can make the problem
|
|
look provider-specific.
|
|
- Quick checks:
|
|
Start with the exact `agentTargetId`, not only the provider. Confirm the
|
|
`preferences.agent.composer.defaults.patch.requested` intent receives an ack,
|
|
then inspect
|
|
`desktop_preferences.agent_composer_defaults_by_agent_target_json` for that
|
|
target and field. Confirm a
|
|
`preferences.agent.composer.defaults.changed` event carries only the same
|
|
target id. Finally request target-scoped composer options and verify
|
|
`effectiveSettings`, then create a Session without explicit overrides and
|
|
inspect the daemon's resolved create settings. For an Agent Extension model,
|
|
confirm the daemon first observed that value in a live catalog for the exact
|
|
target; a catalog observed only for another target cannot validate the patch.
|
|
- Root cause:
|
|
Target defaults and current Session settings are separate durable concerns.
|
|
The renderer's home draft can display an optimistic selection even when a
|
|
defaults write failed. Conversely, a Session settings update can succeed
|
|
while the future-default patch fails. A stale renderer preferences snapshot
|
|
must never be merged and written back as the defaults map; options snapshots
|
|
must never sanitize a newly selected menu value before persistence.
|
|
- Fix:
|
|
Keep `rememberAgentComposerDefaultsForAgentTarget` on the dedicated patch
|
|
intent. Merge its sparse fields only in the tuttid SQLite transaction, publish
|
|
target invalidation after success, and reread defaults through
|
|
composer-options. Keep Create Session inheritance in `agent.Service.Create`;
|
|
if AgentGUI forwards a presented inherited model or reasoning value, it must
|
|
also carry `modelExplicit=false` / `reasoningEffortExplicit=false` through
|
|
Engine and the HTTP request; explicit overrides carry `true`. Do not repair
|
|
this with debounce,
|
|
localStorage, node/workbench overlays, or another full preferences write.
|
|
Do not add workspace/cwd to the target-default patch. Extension model
|
|
validation uses the daemon-observed last-known-good catalog for the exact
|
|
target. Its evidence survives the workspace/cwd display-cache TTL and is
|
|
cleared by explicit provider invalidation; Create performs the separate
|
|
actual-workspace/cwd validation.
|
|
- Validation:
|
|
Change different fields from two windows and confirm both survive. Repeat the
|
|
same SET, then change the same field again and confirm daemon acceptance order
|
|
determines the result. Force an options refresh with an older permission or
|
|
model list and confirm the explicit selection remains visible and is still
|
|
patched. Exercise A-to-B-to-A on one field and confirm only the exact latest
|
|
generation can leave the optimistic layer. Fail the first options reload
|
|
after a successful patch, then confirm a later successful target invalidation
|
|
read converges the acknowledged draft. Reopen the window and restart the app;
|
|
`effectiveSettings` and a new Session must resolve the remembered values.
|
|
Open a historical Session and confirm its settings do not change future
|
|
defaults.
|
|
- References:
|
|
[service.go](../../../services/tuttid/service/preferences/service.go)
|
|
[sqlite_preferences.go](../../../services/tuttid/data/workspace/sqlite_preferences.go)
|
|
[composer_options.go](../../../services/tuttid/service/agent/composer_options.go)
|
|
[desktopPreferencesService.ts](../../../apps/desktop/src/renderer/src/features/desktop-preferences/services/internal/desktopPreferencesService.ts)
|
|
[useAgentGUIComposerSettingsActions.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUIComposerSettingsActions.ts)
|
|
|
|
### Agent interaction remains waiting after the user already responded
|
|
|
|
- Symptom:
|
|
An approval or question remains at an ask-user stop point after the user
|
|
submitted a response. The session may continue after Cancel or an app
|
|
restart, while retrying by request id alone can target an older Turn or be
|
|
rejected as ambiguous.
|
|
- Quick checks:
|
|
Inspect the canonical Interaction identity without logging its prompt or
|
|
response payload. Compare `workspace_id`, `agent_session_id`, `turn_id`, and
|
|
`request_id` across the UI/CLI command, Interaction row, and durable runtime
|
|
operation. A provider request id reused by two Turns is valid. An operation
|
|
id whose stored tuple differs from the command tuple is an invariant failure.
|
|
- Root cause:
|
|
Provider request ids are transport-local and are not session-wide durable
|
|
identities. Selecting an Interaction or deduplicating a runtime operation by
|
|
request id alone can bind a response to historical Turn state, leaving the
|
|
live Turn waiting.
|
|
- Fix:
|
|
Carry the typed exact tuple
|
|
`(workspaceId, agentSessionId, turnId, requestId)` into Host and select the
|
|
Interaction atomically by that tuple. Scope runtime-operation idempotency to
|
|
the same tuple. Keep the provider request id unchanged. Do not scan, guess,
|
|
auto-deduplicate, or rewrite historical rows. The V4 runtime-operation
|
|
migration removes the lossy `subject_id`, creates partial unique indexes for
|
|
each operation kind, verifies copied row distributions and foreign keys, and
|
|
rolls the whole transaction back when a conflicting durable identity exists.
|
|
- Recovery:
|
|
Existing affected sessions may be canceled and retried; restart settlement
|
|
may also interrupt a stale Turn. This forward fix deliberately does not
|
|
mutate historical Interaction rows.
|
|
- Validation:
|
|
Cover the same provider request id in two Turns, exact CLI response routing,
|
|
same-answer idempotency, different-answer supersession, operation identity
|
|
mismatch failure, lossless V1-to-V4 migration, and V4 preflight rollback.
|
|
- References:
|
|
[host README](../../../packages/agent/host/README.md)
|
|
[runtime_operations.go](../../../packages/agent/host/runtime_operations.go)
|
|
[migrations_runtime_operations_v4.go](../../../packages/agent/store-sqlite/migrations_runtime_operations_v4.go)
|
|
[tutti-cli-contract.md](../tutti-cli-contract.md)
|
|
|
|
### Approval feedback stops the Turn or reports a failed response
|
|
|
|
- Symptom:
|
|
Entering feedback on a denied approval aborts the provider Turn, loses the
|
|
feedback text, or lets the provider continue while the response API reports
|
|
`interactive request is no longer live`.
|
|
- Quick checks:
|
|
Inspect the canonical Interaction options and response. Confirm feedback uses
|
|
the non-terminal deny option, retains `payload.denyMessage`, and targets the
|
|
exact child Session and Turn. For Codex, confirm the approval response is
|
|
`decline` and the following `turn/steer` uses the child's provider thread and
|
|
provider Turn IDs.
|
|
- Root cause:
|
|
Treating abort as an ordinary feedback-capable deny ends the Turn before text
|
|
can be delivered. Dropping the payload at the adapter boundary has the same
|
|
visible result. Blocking Interaction completion on the provider's steer
|
|
acknowledgement can also race request cleanup and falsely fail an already
|
|
answered approval.
|
|
- Fix:
|
|
Keep deny and abort distinct in presentation. Preserve the payload through
|
|
the runtime adapter. Complete the approval response as answered, then deliver
|
|
feedback through provider-native active-turn guidance without replacing it
|
|
with a new root or child `Exec`.
|
|
- Validation:
|
|
Cover option selection, payload preservation, exact child provider
|
|
thread/Turn steering, a delayed steer acknowledgement, and a real
|
|
record-audit-replay cassette whose tool is rejected and whose child reports
|
|
the feedback.
|
|
- References:
|
|
[interactivePromptPresentation.tsx](../../../packages/agent/gui/shared/agentConversation/components/interactivePromptPresentation.tsx)
|
|
[codex_appserver_event_interactive.go](../../../packages/agent/daemon/runtime/codex_appserver_event_interactive.go)
|
|
|
|
### Historical AgentGUI permission changes time out or stop responding
|
|
|
|
- Symptom:
|
|
Changing permission mode in a historical conversation appears to do nothing,
|
|
or the first selection times out and later selections are ignored. Logs may
|
|
show a 30-second settings request with no provider settings application.
|
|
- Quick checks:
|
|
Confirm the selected session is absent from the live runtime but present in
|
|
`workspace_agent_sessions`. Check whether one menu choice emitted two settings
|
|
requests. After a timeout, inspect the engine settings operation: `unknown`
|
|
plus a later request without `retry` explains a silent drop.
|
|
- Root cause:
|
|
Historical settings were routed through runtime preparation, so a metadata
|
|
change could block on provider resume and sidecar startup. The permission
|
|
menu also handled both item pointer-down and Select value-change, duplicating
|
|
one user action. When the first command timed out, the engine correctly kept
|
|
uncertainty but the controller did not mark a later explicit choice as a
|
|
retry.
|
|
- Fix:
|
|
Send one settings intent from Select value-change. Read the current engine
|
|
operation at action time and mark an explicit choice as retry when its status
|
|
is `unknown`. In the daemon, update inactive-session settings through the
|
|
durable activity projection and publish reconciliation; reserve provider
|
|
runtime updates for sessions that are already live. Serialize runtime resume
|
|
with durable settings read-modify-write per session, preventing stale resume
|
|
inputs and lost concurrent partial patches. Do not copy an active session
|
|
setting into target defaults.
|
|
- Validation:
|
|
Cover one pointer selection producing one callback, timeout followed by an
|
|
explicit retry, and a historical Claude Code settings update that persists
|
|
while runtime resume and live adapter update counts remain zero. Run the
|
|
AgentGUI, activity-core, store-sqlite, and agent-service focused tests.
|
|
- References:
|
|
[AgentComposerSettingsMenus.tsx](../../../packages/agent/gui/agent-gui/agentGuiNode/AgentComposerSettingsMenus.tsx)
|
|
[useAgentGUIComposerSettingsActions.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUIComposerSettingsActions.ts)
|
|
[service_settings.go](../../../services/tuttid/service/agent/service_settings.go)
|
|
[agent-gui-node.md](../../architecture/agent-gui-node.md)
|
|
|
|
### Agent GUI provider tab shows fused or stale conversations
|
|
|
|
- Symptom:
|
|
Switching the Agent GUI aggregation rail between All, Cursor, Codex, or Claude
|
|
makes the selected row disappear, collapses a loaded page, or briefly flashes
|
|
missing Show more controls. The same switch may remain visibly blocked even
|
|
when each individual session query takes only tens of milliseconds, because
|
|
the first-page bootstrap repeats count, sort, entity projection, and turn /
|
|
interaction hydration once per current project section. Five page rows plus
|
|
one selected overlay may show
|
|
Show more/Show less even when only six sessions exist, or a nine-session
|
|
section may ignore the first Show more click. Restart can reproduce the same
|
|
selected-row loss. On cold startup, All may also show section headers without
|
|
session rows until selecting Codex and then returning to All.
|
|
- Quick checks:
|
|
Inspect `workspace_agent_sessions.agent_target_id` and `rail_section_key`.
|
|
Confirm section requests carry the selected `agentTargetId` before pagination
|
|
and responses preserve `totalCount`, `hasMore`, and `nextCursor`. In the
|
|
renderer, distinguish daemon-owned section membership ids from engine-owned
|
|
session entities; activating or hydrating one session must not rewrite the
|
|
loaded membership page. For latency, count repository section reads per
|
|
bootstrap: production must issue one `ListSessionSections` batch read, not one
|
|
`ListSessionSection` call per project plus pinned and Chats. Run
|
|
`EXPLAIN QUERY PLAN` and confirm ordinary branches use
|
|
`idx_workspace_agent_sessions_rail_section_page`, pinned branches use
|
|
`idx_workspace_agent_sessions_pinned_page`, and selected page entities load
|
|
by the session primary key after narrow id trimming. A target-filtered rail
|
|
must instead use `idx_workspace_agent_sessions_rail_section_target_page` and
|
|
`idx_workspace_agent_sessions_pinned_target_page` with an exact target
|
|
predicate; an optional `OR` predicate cannot narrow the index range. For a
|
|
rail that shows only the active/new session, inspect `*_failed` events for
|
|
`no such index`, then compare those required indexes with `sqlite_master`
|
|
even when their migration markers exist. Store startup must idempotently
|
|
restore missing rail pagination indexes; do not weaken `INDEXED BY` or fall
|
|
back to a full session scan. For a reported slow switch, correlate
|
|
`agent_gui.conversation_rail.first_pages_slow` with
|
|
`workspace.agent_session.sections.list_slow`: the renderer event separates
|
|
request and controller-apply time, while the daemon event separates current
|
|
projects, store, and hydration time. Corresponding `*_failed` events record
|
|
real failures. Successful requests below 250 ms, aborted requests, and stale
|
|
responses are intentionally silent; these diagnostics must not log project
|
|
paths, section keys, session titles, or prompts. Use `refreshReason` to
|
|
distinguish attach, scope change, and membership invalidation. Interpret
|
|
`rail_visible_session_count` as the target-scoped total across requested
|
|
sections and `returned_session_count` as only the bounded first-page rows;
|
|
neither requires another full-workspace count query.
|
|
For provider switching, inspect `agent_gui.provider_switch.completed` or
|
|
`agent_gui.provider_switch.failed`. They report source/target ids, fresh/stale/
|
|
miss cache status, request time, controller-apply time, and total controller
|
|
readiness time. Correlate target and timestamp with
|
|
`agent.composer_options.load`, which records composer transport duration and
|
|
status. A fresh rail hit with no composer transport indicates both caches were
|
|
reused; a long rail request isolates section loading, while a long composer
|
|
event isolates target option discovery.
|
|
For the cold-start empty-row variant, compare
|
|
`agent.gui.runtime.snapshot_changed` session counts with
|
|
`agent_gui.conversation_rail.first_pages_slow`. If historical reconciliation
|
|
finishes while the first-page request is unresolved, confirm no section-page
|
|
request is emitted with `refreshReason=membership_change`.
|
|
- Root cause:
|
|
A second React summary cache mixed entity data, section membership, active
|
|
selection, and visible-item limits. Effects manually patched section rows
|
|
from changing conversation summaries, so provider/detail reconciliation could
|
|
collapse pages or synthesize membership. Counting the active overlay as a
|
|
pageable row also corrupted Show more decisions. Bounded engine snapshots can
|
|
recreate the loss if omission is treated as deletion. The latency variant is
|
|
an N-section daemon read: current projects are a small requested set, while
|
|
the workspace DB retains history for removed projects. Scanning that full
|
|
history or repeating canonical turn / interaction hydration per section makes
|
|
the rail wait scale with project count even when every leaf query looks fast.
|
|
A database opened by different worktrees or intermediate builds can retain a
|
|
migration marker after a required physical index disappears; treating the
|
|
marker alone as proof of the schema invariant makes every section bootstrap
|
|
fail while an active-session overlay can misleadingly remain visible.
|
|
Historical `session/snapshotReceived` hydration can also be mistaken for a
|
|
live rail membership mutation, causing a targeted section refresh to race the
|
|
unresolved bootstrap request. Caching session payloads or a separate ingestion
|
|
marker makes that race worse by splitting entity ownership from the engine.
|
|
- Fix:
|
|
Keep page sessions in the workspace engine. Cache only ordered membership ids,
|
|
cursor, `hasMore`, and `totalCount` in the controller query, then join ids to
|
|
engine entities with a pure model projection. Keep active, pending, and all
|
|
exact-target in-progress root sessions as display overlays outside pagination
|
|
until canonical membership catches up. Preserve old scope chrome and metadata
|
|
atomically while a provider refetch is pending. Engine snapshots merge
|
|
monotonically; only explicit `session/removed` owns deletion. Keep first-page
|
|
bootstrap as a required narrow repository seam: one requested-section-driven
|
|
batch query, independent pinned and ordinary index branches, count/sort/limit
|
|
on narrow session ids, then one cross-section canonical entity hydration.
|
|
Reassert required rail indexes with idempotent `CREATE INDEX IF NOT EXISTS`
|
|
during every store migration pass, including when the corresponding marker is
|
|
already recorded, so schema drift repairs itself without rewriting session
|
|
data.
|
|
While workspace reconciliation is loading, update only the rail membership
|
|
comparison baseline; do not target-refresh pages for historical snapshot
|
|
hydration. Upsert first-page response entities into the workspace engine before
|
|
publishing their membership ids. Resolved query cache entries must not contain
|
|
session entities or ingestion ownership state.
|
|
Do not add one `UNION ALL` arm per section; that restores section-count scaling
|
|
and inherits SQLite's compound-select term limit.
|
|
- Validation:
|
|
Run `pnpm --filter @tutti-os/agent-gui test`,
|
|
`pnpm --filter @tutti-os/agent-activity-core test`, and
|
|
`pnpm check:agent-activity-runtime-boundaries`. Also run
|
|
`cd packages/agent/store-sqlite && go test ./... -run 'SessionSection|TurnsBackfill'`
|
|
and
|
|
`cd services/tuttid && go test ./service/agent ./api -run 'ListPage|SessionList|SessionSection'`
|
|
so cursor metadata and daemon ordering are covered. Run
|
|
`cd packages/agent/store-sqlite && go test -run '^$' -bench 'BenchmarkStoreListSessionSectionsLargeRemovedProjectHistory' -benchmem`
|
|
to compare the batch reader with the serial reference across sparse and dense
|
|
requested-section histories. Cover Codex -> All -> Codex,
|
|
client restart restore, active row outside first page, five-plus-active totals,
|
|
nine-session Show more, slow provider refetch, and bounded snapshot omission.
|
|
Add a cold-start ordering case where historical workspace hydration arrives
|
|
before the first section response and assert that no targeted membership refresh
|
|
is issued.
|
|
- References:
|
|
[useAgentGUIConversationRailQuery.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUIConversationRailQuery.ts)
|
|
[agentGuiConversationRail.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/model/agentGuiConversationRail.ts)
|
|
[AgentGUIConversationRailSection.tsx](../../../packages/agent/gui/agent-gui/agentGuiNode/view/AgentGUIConversationRailSection.tsx)
|
|
[sessionEntities.reducer.ts](../../../packages/agent/activity-core/src/engine/sessionEntities.reducer.ts)
|
|
[service_session_sections.go](../../../services/tuttid/service/agent/service_session_sections.go)
|
|
|
|
### Agent GUI sessions appear under the wrong user project
|
|
|
|
- Symptom:
|
|
A conversation started with the "No project" selection appears in the Agent
|
|
GUI rail under a parent user-project group such as the user's home directory.
|
|
Imported Codex or Claude Code conversations with `cwd` equal to `$HOME`, and
|
|
claude.ai data-export conversations that have no cwd at all, can show the
|
|
same symptom even though the user never selected a project. A related nested-
|
|
project variant shows an imported conversation under a broad parent folder
|
|
while its explicitly selected child-project section says `No chats yet`.
|
|
Switching provider filters may briefly retain the previous scope's row before
|
|
the exact persisted membership makes the child section empty again.
|
|
Delegated Issue tasks, Agent CLI handoffs, Automation follow-ups, or
|
|
AgentGUI “New conversation” / “Continue in new conversation” can show a
|
|
related variant: the new Session appears in Chats or under a temporary
|
|
worktree instead of the source project, and Git commands no longer operate
|
|
on the intended checkout.
|
|
- Quick checks:
|
|
Inspect the session `cwd` from the activity snapshot. Generated no-project
|
|
sessions should carry `runtimeContext.noProject: true` in the daemon report
|
|
before `cwd` is matched against parent user-project paths. If the create
|
|
request contains that marker but the reported state does not, inspect
|
|
`runtime.Controller.State`: it must preserve the session launch context while
|
|
adding provider adapter state. For imported sessions, inspect `runtimeContext`
|
|
for the daemon-owned `externalImportNoProject` marker. Claude data-export
|
|
sessions should also carry `externalImportResumeSupported: false`. Check both
|
|
the in-memory `rememberNoProjectPath` path and the restart fallback that
|
|
recognizes `Documents/tutti/session-<uuid>`. Codex external history can also
|
|
record its own scratch cwd under
|
|
`Documents/Codex/<yyyy-mm-dd>/<conversation>`.
|
|
For the nested-project variant, compare `cwd`, `rail_project_path`, and
|
|
`rail_section_key` in `workspace_agent_sessions`. If the cwd is inside a
|
|
registered child project but the persisted key names an ancestor, check
|
|
whether that child was registered only after the original import completed.
|
|
For a newly derived Session, compare those three fields with the source
|
|
Session and inspect whether the create adapter supplied both `cwd` and
|
|
`RailPlacement`. If an Issue has `source_session_id` but no Run was created,
|
|
verify that the source Session still resolves; dispatch intentionally waits
|
|
instead of guessing. For project Sessions with an empty cwd, verify that the
|
|
adapter used `rail_project_path` as the runtime fallback.
|
|
- Root cause:
|
|
Rail membership is classified once by the daemon when the session is first
|
|
persisted, using `cwd`, runtime no-project markers, and current user projects.
|
|
If a generated no-project marker is lost before that write, longest-parent
|
|
matching can assign the immutable `railSectionKey` to a broad project such as
|
|
`$HOME`. External import has a similar trap because provider transcripts may
|
|
record `$HOME` or a provider-owned scratch working directory as the cwd when
|
|
no project was selected; that intent must reach initial persistence as session
|
|
metadata. A second loss point is runtime state projection:
|
|
rebuilding `runtimeContext` from only `cwd`, title, permissions, and visibility,
|
|
or replacing it wholesale with `StateAdapter` output, drops launch-scoped
|
|
markers such as `noProject` before durable rail classification runs.
|
|
For project-backed import, registering user projects only after valid session
|
|
writes creates another ordering trap: the store may see an existing parent
|
|
project but not the selected child while assigning the session's first,
|
|
normally immutable rail key.
|
|
Derived-Session entry points add the inverse trap: copying only runtime cwd
|
|
loses logical ownership when the cwd is an isolated worktree, while copying
|
|
only rail placement can leave the Agent without the source checkout. Looking
|
|
up the source twice during one dispatch can also mix two different snapshots,
|
|
and silently accepting a missing source turns an invalid handoff into a
|
|
detached allocator-backed Session.
|
|
- Fix:
|
|
Build runtime state from a clone of the session launch `RuntimeContext`, overlay
|
|
canonical session fields, and merge provider `StateAdapter` context as a patch
|
|
instead of replacing the map. Provider values win on collisions, while
|
|
launch-only markers remain available to the durable classifier.
|
|
Persist Agent GUI rail grouping in daemon-owned
|
|
`workspace_agent_sessions.rail_section_*` fields from the shared
|
|
`services/tuttid/data/workspace` classifier. Migration and session-state
|
|
upsert should both use that classifier, matching exact user projects first,
|
|
then preserving no-project/provider scratch cwd shapes as conversations, then
|
|
applying longest parent-project matches. Project-backed external import must
|
|
pass its exact matched selection into the initial session report, independent
|
|
of whether API-level project registration has completed. The store validates
|
|
that this is an imported, project-backed session whose cwd is contained by the
|
|
selected path. An idempotent re-import may repair only Chats or an ancestor
|
|
assignment to that explicit descendant; it must not infer a migration from
|
|
the current project inventory or move an ordinary runtime session. Apart from
|
|
this evidence-backed import correction, preserve valid existing rail keys
|
|
across later cwd and project-list changes. A successful Create
|
|
response must synchronously read back the persisted session and its nonblank
|
|
key rather than racing the runtime's asynchronous activity reporter. AgentGUI
|
|
must project sessions only by exact key equality and must not retain a cwd-based
|
|
grouping fallback.
|
|
For handoff, Issue, Automation, and AgentGUI new-conversation flows, carry
|
|
runtime cwd and canonical rail placement independently. Resolve project
|
|
identity by exact section key, use the canonical project path when a
|
|
project-backed source cwd is empty, take one source snapshot per dispatch,
|
|
and fail closed when a required source Session cannot be resolved.
|
|
- Validation:
|
|
Run
|
|
`pnpm --filter @tutti-os/agent-gui test -- agent-gui/agentGuiNode/model/agentGuiConversationModel.spec.ts`,
|
|
`cd packages/agent/daemon && go test ./runtime`,
|
|
`cd services/tuttid && go test ./service/agent ./api -run 'ExternalImport|ParseCodex|ParseClaude'`,
|
|
`go test ./packages/agent/store-sqlite -run 'ImportedRail|ClassifiesRail'`,
|
|
`node --import ./test/register-asset-stub.mjs --test --experimental-strip-types ./src/renderer/src/features/workspace-user-project/services/internal/desktopWorkspaceUserProjectService.test.ts`
|
|
from `apps/desktop`, then run `pnpm check:changed`.
|
|
For derived-Session regressions, also run the focused AgentGUI
|
|
new-conversation tests plus
|
|
`go test ./service/workspace ./service/automationrule`.
|
|
- References:
|
|
[controller_state.go](../../../packages/agent/daemon/runtime/controller_state.go)
|
|
[controller_state_test.go](../../../packages/agent/daemon/runtime/controller_state_test.go)
|
|
[external_import_parse.go](../../../services/tuttid/service/agent/external_import_parse.go)
|
|
[external_import_projects.go](../../../services/tuttid/service/agent/external_import_projects.go)
|
|
[external_import.go](../../../services/tuttid/service/agent/external_import.go)
|
|
[rail.go](../../../packages/agent/store-sqlite/rail.go)
|
|
[agentGuiConversationModel.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/model/agentGuiConversationModel.ts)
|
|
[desktopWorkspaceUserProjectService.ts](../../../apps/desktop/src/renderer/src/features/workspace-user-project/services/internal/desktopWorkspaceUserProjectService.ts)
|
|
[agentGuiConversationProjectResolver.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/model/agentGuiConversationProjectResolver.ts)
|
|
[useAgentGuiConversationList.ts](../../../packages/agent/gui/contexts/workspace/presentation/renderer/agentGuiConversationList/useAgentGuiConversationList.ts)
|
|
[issue_sequential_dispatch.go](../../../services/tuttid/service/workspace/issue_sequential_dispatch.go)
|
|
[daemon_executor.go](../../../services/tuttid/service/automationrule/daemon_executor.go)
|
|
|
|
### AgentGUI new conversation does nothing after leaving a Chats session
|
|
|
|
- Symptom:
|
|
After using an ordinary Chats Session, the toolbar new-conversation action
|
|
opens the home composer but submitting by Enter or the send button creates no
|
|
visible Session and produces no `session/activate` command. Enabling Tutti
|
|
Mode before submitting can make the problem look mode-specific even though
|
|
activation is blocked before provider or Tutti Mode execution starts.
|
|
- Quick checks:
|
|
Inspect the active conversation projection before the new-conversation
|
|
request. If `railSectionKey` is `conversations` while `cwd` is a generated
|
|
runtime directory, confirm that directory is not copied into the home
|
|
composer's selected project. A subsequent activation that resolves the path
|
|
against user projects and returns no placement explains the missing
|
|
`session/activate`.
|
|
- Root cause:
|
|
Composer presentation intentionally exposes the active Session `cwd` for
|
|
file mentions, Git operations, and missing-directory checks. Treating that
|
|
presentation field as the user's home project selection conflates runtime
|
|
working directory with rail placement. The generated Chats directory then
|
|
enters project resolution and fails closed because it has no canonical
|
|
project section.
|
|
- Fix:
|
|
Normalize default project selection at the AgentGUI controller's
|
|
new-conversation command. Explicit section actions remain authoritative; an
|
|
active Chats Session replaces the home selection with no project, an active
|
|
project Session resolves its immutable section key back to the canonical
|
|
registered project path, and an action already on Home preserves the user's
|
|
explicit selection. The continuation action shares this resolver before it
|
|
moves the source mention draft to Home. Views forward the intent without
|
|
interpreting composer presentation fields.
|
|
- Validation:
|
|
Cover the command through final `session/activate` for three P0 scenarios:
|
|
active Chats clears a generated cwd, active Project with a nested/worktree
|
|
cwd preserves its canonical placement, Continue uses that same project, and
|
|
Home preserves an explicit project selection.
|
|
- References:
|
|
[agentGuiNewConversationRequest.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/agentGuiNewConversationRequest.ts)
|
|
[useAgentGUIOperationActions.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUIOperationActions.ts)
|
|
[agentGuiNewConversationRequest.spec.tsx](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/agentGuiNewConversationRequest.spec.tsx)
|
|
|
|
### Extension history becomes non-resumable after daemon restart
|
|
|
|
- Symptom:
|
|
An Agent Extension conversation works until `tuttid` restarts. Its history
|
|
remains visible, but AgentGUI says it cannot resume on this device and only
|
|
offers continuing through an `@` mention. In an affected live extension
|
|
session, the `tutti` shim can be present on `PATH` while many public commands,
|
|
including `tutti agent list`, return `command_not_found`. After restart,
|
|
sending to the same session can fail with
|
|
`session runtime snapshot is unavailable: launch identity is incomplete`.
|
|
- Quick checks:
|
|
Confirm the persisted session still has `provider_session_id` and
|
|
`agent_target_id`. If the Target remains enabled and names a fixed extension
|
|
installation, compare list-time `resumable` calculation with the actual
|
|
Resume path. An empty process-local adapter registry after restart is not
|
|
evidence that the session cannot be restored.
|
|
For the CLI variant, first distinguish discovery from routing: look for
|
|
`tutti cli shim ready` in Desktop logs and `path_contains_tutti_bin=true` in
|
|
the provider process diagnostics. If both are present but the provider tool
|
|
reports `unknown command`, compare
|
|
`workspace_agent_sessions.provider` with
|
|
`internal_runtime_context_json.$.sessionRuntimeSnapshot.provider`. An
|
|
extension provider such as `acp:<name>` beside an empty snapshot provider,
|
|
followed by `launch identity is incomplete`, identifies the durable snapshot
|
|
path rather than a PATH or listener failure. A second historical shape keeps
|
|
`acp:<name>` in both locations but has a provider-native fingerprint computed
|
|
from an empty provider; it fails later with
|
|
`provider-native fingerprint does not match`.
|
|
- Root cause:
|
|
Dynamic Agent Extension adapters are created on demand and cached only for
|
|
the daemon lifetime. Computing `resumable` from that cache maps restart state
|
|
to a false domain result before Resume can re-resolve the persisted Target.
|
|
The same false result occurs when an adapter rebuilds the runtime resume input
|
|
but drops `agentTargetId`: the fixed Target ref then fails the controller's
|
|
complete binding check even though persistence and Target resolution are
|
|
correct. A separate snapshot variant occurs when provider-neutral metadata
|
|
uses the closed built-in-provider normalizer for an open extension identity.
|
|
The writer then persists an empty provider and fingerprints the
|
|
provider-native configuration with that empty value. Session-scoped CLI
|
|
capability projection validates the snapshot before returning the command
|
|
catalog; validation failure collapses discovery to an empty catalog, so
|
|
otherwise valid commands appear unknown. Runtime resume rejects the same
|
|
incomplete identity after daemon restart.
|
|
- Fix:
|
|
At the service boundary, re-derive `ProviderTargetRef` from the persisted
|
|
session's enabled `agentTargetId`. At the runtime boundary, validate the
|
|
provider, Target, and fixed installation binding; when a dynamic resolver is
|
|
configured, treat that authorized binding as eligible for a Resume attempt.
|
|
Keep installation validation and ACP `session/load` in the actual Resume
|
|
path. Never use an open provider id or adapter-cache presence as launch
|
|
authority.
|
|
Every bridge from the Host resume input to the runtime resume input must
|
|
preserve `agentTargetId` and `ProviderTargetRef` together. Cover the complete
|
|
service-to-adapter-to-controller path instead of testing each endpoint with
|
|
independently constructed valid inputs.
|
|
Persist and compare snapshot provider metadata with the open provider
|
|
normalizer; launch authority still comes exclusively from the exact enabled
|
|
Agent Target. For already-written empty-provider extension snapshots, recover
|
|
only when the canonical session provider is a valid unregistered open
|
|
identity, the snapshot declares provider-native configuration, and its
|
|
fingerprint exactly matches the historical empty-provider payload. Apply the
|
|
same narrow compatibility check to the transitional shape that already
|
|
retained the open provider in the snapshot: the canonical and snapshot
|
|
providers must match exactly before accepting the historical fingerprint.
|
|
Keep every other malformed or mismatched snapshot fail-closed, and do not
|
|
rewrite the database merely to make discovery succeed.
|
|
- Validation:
|
|
Start from a controller with no cached extension adapter. Assert a persisted
|
|
Target-bound session is resumable, malformed or mismatched bindings fail
|
|
closed, and the eligibility check does not launch the provider. Then run
|
|
`go test ./packages/agent/daemon/runtime ./services/tuttid/service/agent`.
|
|
Also cover new extension snapshots preserving `acp:*`, verified legacy
|
|
empty-provider recovery, transitional open-provider/legacy-fingerprint
|
|
recovery, registered or mismatched provider fallback rejection, CLI command
|
|
projection for the recovered session, and runtime preparation after restart.
|
|
- References:
|
|
[controller_session_registry.go](../../../packages/agent/daemon/runtime/controller_session_registry.go)
|
|
[agent_runtime_adapter.go](../../../services/tuttid/agent_runtime_adapter.go)
|
|
[service_session.go](../../../services/tuttid/service/agent/service_session.go)
|
|
[session_runtime_snapshot.go](../../../services/tuttid/service/agent/session_runtime_snapshot.go)
|
|
[agent-extensions.md](../../architecture/agent-extensions.md)
|
|
|
|
### An authorized observer loops unavailable while a session is resuming
|
|
|
|
- Symptom:
|
|
A remote or local observer has a durable Agent session ID and passes
|
|
authorization, but immediately receives `session not found` or
|
|
`Unavailable`. Its caller reconnects rapidly even though Agent Host restores
|
|
the provider session shortly afterward.
|
|
- Quick checks:
|
|
Separate durable identity from the Controller registry. Confirm the session
|
|
exists in canonical persistence while `Controller.Session` is temporarily
|
|
absent, then correlate the observer attempt with the later Host
|
|
`EnsureRuntimeSession`, runtime `Start`, or runtime `Resume`. If observation
|
|
itself launches the provider, lifecycle authority is inverted.
|
|
- Root cause:
|
|
A durable session record proves that work may be resumed; it does not prove a
|
|
live provider or runtime registry entry exists after daemon restart. A
|
|
one-shot `Controller.Subscribe` therefore turns a normal restore race into a
|
|
transport failure, and caller reconnect loops cannot know when the local
|
|
lifecycle owner has finished restoring ACP.
|
|
- Fix:
|
|
Use `Controller.SubscribeWhenAvailable` when the consumer already has an
|
|
authorized durable session identity and must observe the next live runtime.
|
|
It waits on a per-session Controller notification and atomically subscribes
|
|
with the initial state snapshot once `Start` or `Resume` registers the
|
|
session. Keep provider launch and resume in Agent Host; the observation
|
|
method must never call those lifecycle operations.
|
|
- Validation:
|
|
Start the observation against a missing runtime, assert that it neither
|
|
returns nor creates a session, then perform a real Controller `Resume` and
|
|
verify the observer receives the initial state snapshot. Also cancel a
|
|
missing-session wait and verify no waiter or runtime session remains, and
|
|
cover concurrent observers under the race detector.
|
|
- References:
|
|
[controller_stream.go](../../../packages/agent/daemon/runtime/controller_stream.go)
|
|
[controller_stream_wait_test.go](../../../packages/agent/daemon/runtime/controller_stream_wait_test.go)
|
|
|
|
### Agent session restore breaks when durable snapshot ownership is split
|
|
|
|
- Symptom:
|
|
Workspace agent sessions still appear recoverable after a renderer refresh,
|
|
but after a `tuttid` restart the session list is empty, the detail pane
|
|
falls back to an unavailable state, or a newly reported session overwrites
|
|
older history for the same workspace.
|
|
- Quick checks:
|
|
Confirm `services/tuttid/data/workspace` has a durable snapshot row for the
|
|
workspace and that `services/tuttid/wiring.go` hydrates the in-memory agent
|
|
activity store from it before new runtime reports are applied.
|
|
If restore reads use a different source than write-time projection, verify
|
|
both `List/Get` and message-history queries are reading the same durable
|
|
snapshot shape.
|
|
If the durable row has `provider_session_id` but ACP returns
|
|
`Resource not found`, confirm the restore path re-runs the agent sidecar
|
|
preparer and passes the prepared runtime environment, such as the per-session
|
|
`CODEX_HOME`, into runtime resume.
|
|
- Root cause:
|
|
Agent runtime reports are projected into an in-memory activity store, but
|
|
restore paths survive daemon restarts only if the projected snapshot is also
|
|
written to daemon-owned local state and reloaded before the next activity
|
|
report. If only the renderer cache or only the daemon process memory holds
|
|
the projection, session metadata and message history diverge after restart.
|
|
- Fix:
|
|
Make `tuttid` own a durable agent snapshot in `data/workspace`, persist it
|
|
from the activity-store update listener, and hydrate the in-memory activity
|
|
store from that snapshot on first room tracking. Service-level
|
|
session/message restore should read from the same durable snapshot source,
|
|
and runtime mutations should on-demand resume a persisted session before
|
|
accepting new input. Provider-session resume must use the same prepared
|
|
sidecar runtime root and env as the original session, because provider ids
|
|
are often scoped to provider-local state under that root.
|
|
- Validation:
|
|
Add store round-trip coverage for the snapshot row, service tests that fall
|
|
back to persisted sessions and resume them into runtime, then run
|
|
`pnpm lint:go` plus `cd services/tuttid && go test ./... && go build ./...`.
|
|
- References:
|
|
[service.go](../../../services/tuttid/service/agent/service.go)
|
|
[wiring.go](../../../services/tuttid/wiring.go)
|
|
|
|
### Agent activity live updates fail after event schema changes
|
|
|
|
- Symptom:
|
|
AgentGUI stays busy after a turn has finished, while durable
|
|
`workspace_agent_sessions` state is already idle and daemon logs show
|
|
`publish workspace agent activity update failed` with a
|
|
`decode ... data: json: unknown field` error.
|
|
- Quick checks:
|
|
Compare the new field in
|
|
`packages/events/protocol/definitions/agent/activity.updated.event.json`,
|
|
generated event protocol outputs, and the hand-written strict validators in
|
|
`services/tuttid/service/eventstream/catalog.go`.
|
|
- Root cause:
|
|
The shared business event schema and generated Go/TypeScript protocol files
|
|
can be current while the daemon event-stream catalog still rejects the same
|
|
payload through `DisallowUnknownFields` on a hand-written validation struct.
|
|
The activity projection may persist the correct session state, but the live
|
|
`agent.activity.updated` publish is rejected before the renderer runtime sees
|
|
the settling patch.
|
|
- Fix:
|
|
Keep `catalog.go` validation DTOs in sync with new event fields, especially
|
|
for `agent.activity.updated` top-level, `session_update`, and `state_patch`
|
|
payloads. Add a positive validator test for the new field, not only generated
|
|
protocol checks.
|
|
- Validation:
|
|
Run `go test ./services/tuttid/service/eventstream` and
|
|
`pnpm check:event-protocol-generated` when event protocol sources changed.
|
|
- References:
|
|
[catalog.go](../../../services/tuttid/service/eventstream/catalog.go)
|
|
[activity.updated.event.json](../../../packages/events/protocol/definitions/agent/activity.updated.event.json)
|
|
|
|
### AgentGUI file-change undo reports a generic failure
|
|
|
|
- Symptom:
|
|
Clicking Undo on a changed-files summary shows a failure even though the
|
|
target directory is a Git repository and the file appears unchanged since
|
|
the agent edit. The same loss can appear earlier as a missing changed-files
|
|
card, missing or incorrect line counts, or a "missing reversible patch data"
|
|
error after a provider overwrites a file.
|
|
- Quick checks:
|
|
Search desktop and daemon logs for the `agent-git-patch` diagnostic family.
|
|
Inspect `errorCode`, Git `stderr`, the diff byte count and hash, and the
|
|
affected paths. For `invalid-patch`, inspect the durable tool output for
|
|
malformed unified-diff control markers. A no-newline marker must begin with
|
|
`\`, not a leading context-space followed by `\`. For
|
|
a synthesized whole-file edit, verify that an empty old or new side has a
|
|
hunk count of `0`; `@@ -1,1 +1,N @@` with no old or context line is corrupt.
|
|
If only one side of a modified file was persisted, Undo must be unavailable
|
|
instead of guessing that the file was created. For
|
|
`patch-does-not-apply`, compare the recorded after-state with the current
|
|
file rather than assuming the original turn is still the latest writer. For
|
|
missing patch data, check whether Standard ACP logged an unsupported
|
|
`fs/write_text_file` request, whether Codex emitted
|
|
`item/fileChange/patchUpdated`, and whether a modified canonical file entry
|
|
contains only one of `oldString` and `newString`.
|
|
- Root cause:
|
|
Provider display diffs can contain syntax that a viewer tolerates but
|
|
`git apply` rejects. Treating that display payload as executable patch data
|
|
produces corrupt hunks. A separate failure occurs when the patch is valid
|
|
but later edits changed its context. On Windows, leaving an absolute drive
|
|
path in either a synthesized patch or an existing unified-diff header also
|
|
violates Git's cwd-relative patch contract and can report that an untracked
|
|
created file does not exist in the index.
|
|
Historical raw tool payloads can also contradict the canonical
|
|
`fileChanges` classification. Giving that raw payload priority can turn a
|
|
created file into a one-sided modified edit; forcing an empty side to count
|
|
as one then produces a syntactically corrupt patch.
|
|
created file does not exist in the index. Patch data is also lost when a
|
|
Standard ACP host advertises file writes as unsupported and therefore cannot
|
|
snapshot the old content, or when an app-server adapter ignores the
|
|
authoritative incremental file-change patch notification. Synthesizing the
|
|
missing side of a modified file as empty then produces a plausible-looking
|
|
but non-reversible patch.
|
|
- Fix:
|
|
Canonicalize provider file-change metadata at the runtime adapter boundary
|
|
before persistence, and canonicalize historical no-newline markers on read.
|
|
AgentGUI must make synthesized paths and existing unified-diff headers
|
|
relative to the patch cwd, using case-insensitive path identity for Windows.
|
|
Executable batches prefer canonical per-call `fileChanges`; incomplete raw
|
|
modified changes fail closed. Undo/Reapply is atomic for the settled Turn:
|
|
every canonical visible path and every source-batch change must have an
|
|
executable patch, otherwise the whole action stays unavailable. When both
|
|
modified-file sides are known, hunk counts use the actual line counts,
|
|
including zero, and text synthesis rejects NUL-containing binary content.
|
|
Standard ACP hosts that advertise `writeTextFile` must implement the matching
|
|
`fs/write_text_file` request, capture the exact old and new content, and emit
|
|
canonical turn file changes. App-server adapters must preserve authoritative
|
|
incremental patch notifications in the same canonical turn state. AgentGUI
|
|
must fail closed instead of synthesizing a modified-file patch when either
|
|
side is absent.
|
|
The daemon must preflight with `git apply --check` using the same execution
|
|
options, return `invalid-patch` for syntax failures and
|
|
`patch-does-not-apply` for state mismatch, and avoid mutating the worktree on
|
|
either result.
|
|
- Validation:
|
|
Cover leading-whitespace no-newline markers, historical activity projection,
|
|
corrupt-patch preflight without mutation, worktree divergence, reverse
|
|
application, cwd-relative Windows drive paths for synthesized and complete
|
|
diffs, zero-sided whole-file edits with LF and CRLF input, binary fail-closed
|
|
behavior, canonical-versus-raw conflicts, and the existing
|
|
diffs, Standard ACP overwrite capture and relative-path rejection, Codex
|
|
incremental patch projection, one-sided modified metadata, and the existing
|
|
untracked-created-file behavior.
|
|
- References:
|
|
[claude_sdk_activity.go](../../../packages/agent/daemon/runtime/claude_sdk_activity.go)
|
|
[standard_acp_filesystem.go](../../../packages/agent/daemon/runtime/standard_acp_filesystem.go)
|
|
[codex_appserver_reducer.go](../../../packages/agent/daemon/runtime/codex_appserver_reducer.go)
|
|
[agentTurnSummaryPatchDiff.ts](../../../packages/agent/gui/shared/agentConversation/rules/agentTurnSummaryPatchDiff.ts)
|
|
[agentPatchMetadata.ts](../../../packages/agent/gui/shared/agentConversation/rules/agentPatchMetadata.ts)
|
|
[agentTurnSummaryProjection.ts](../../../packages/agent/gui/shared/agentConversation/projection/agentTurnSummaryProjection.ts)
|
|
[agentTurnSummaryPatchDiff.ts](../../../packages/agent/gui/shared/agentConversation/rules/agentTurnSummaryPatchDiff.ts)
|
|
[git_patch.go](../../../services/tuttid/service/agent/git_patch.go)
|
|
|
|
### AgentGUI changed-files summary shows negative lines for a new file
|
|
|
|
- Symptom:
|
|
The file-edit row reports additions, but the settled changed-files summary
|
|
reports deletions, often matching the number of Markdown list items that
|
|
begin with `-`. The worktree file itself is present and complete.
|
|
- Quick checks:
|
|
Compare the final file's line count with the tool row, then inspect the
|
|
canonical `turn.fileChanges.files[]` entry. A real unified diff has a hunk
|
|
header such as `@@ -0,0 +1,13 @@`; a file body stored in `unifiedDiff` is
|
|
malformed metadata, not a diff.
|
|
- Root cause:
|
|
The summary projection used any non-empty `unifiedDiff` as a valid patch and
|
|
counted every line beginning with `-` as a removal. Its content fallback also
|
|
discarded blank lines, so the summary and tool row could use different line
|
|
totals for the same write.
|
|
- Fix:
|
|
Normalize `fileChanges` at the runtime and durable-payload boundaries: a
|
|
created file's raw body becomes `newString`, while `unifiedDiff` is retained
|
|
only when it has a real hunk. AgentGUI also requires a hunk before parsing
|
|
historical payloads and shares the line-count helper with tool render data,
|
|
so blank lines are counted consistently.
|
|
- Validation:
|
|
Cover a created Markdown body containing list items and blank lines at both
|
|
canonicalization boundaries, then run the Agent runtime and AgentGUI tests
|
|
and typechecks.
|
|
- References:
|
|
[tool_file_changes.go](../../../packages/agent/daemon/runtime/tool_file_changes.go)
|
|
[tool_payload.go](../../../packages/agent/store-sqlite/canonical/tool_payload.go)
|
|
[agentUnifiedDiff.ts](../../../packages/agent/gui/shared/agentConversation/components/tool-renderers/file-diff/agentUnifiedDiff.ts)
|
|
[AgentTurnSummaryRow.tsx](../../../packages/agent/gui/shared/agentConversation/components/AgentTurnSummaryRow.tsx)
|
|
|
|
### Cursor deleted files appear as created or modified
|
|
|
|
- Symptom:
|
|
After Cursor deletes a file, the settled turn's changed-files summary or tool
|
|
detail labels it as created or modified instead of deleted.
|
|
- Quick checks:
|
|
Inspect the original completed ACP tool payload, its following `turn.updated`
|
|
event, and the durable turn row. Cursor reports deletion with ACP
|
|
`kind = delete`, but its canonical tool name is still `Write`. The
|
|
`turn.updated.metadata.fileChanges.files[]` entry and durable
|
|
`file_changes_json` must both say `change = deleted`.
|
|
- Root cause:
|
|
The canonical tool name describes the file-writing tool family, not the
|
|
change direction. Inferring every `Write` as creation discards Cursor's
|
|
explicit ACP delete semantic. A second failure mode is normalizing the tool
|
|
call correctly but omitting canonical `fileChanges` from the subsequent turn
|
|
state patch, leaving AgentGUI without its authoritative response-tail input.
|
|
- Fix:
|
|
Normalize recognized provider change kinds inside the shared runtime
|
|
file-change projector, accumulate the result per turn, and persist it through
|
|
`turn.updated`. The session-detail response must return all durable root turns,
|
|
and the desktop reconcile bridge must insert them into the existing activity
|
|
engine turn store. AgentGUI reads only the matching canonical turn's
|
|
`fileChanges`; do not add Cursor/ACP/Codex/Claude field inference to
|
|
conversation projection. Keep tool `changes` payloads independently for
|
|
Undo/Reapply patch batches. Do not backfill sessions whose historical turns
|
|
have no canonical file changes.
|
|
- Validation:
|
|
Cover the Cursor completed-call to `turn.updated` path, durable turn state,
|
|
and canonical AgentGUI projection. Also cover Codex `changes[].kind.type` and
|
|
Claude Code started/completed tool merging because all three providers share
|
|
the same turn-level contract.
|
|
- References:
|
|
[tool_file_changes.go](../../../packages/agent/daemon/runtime/tool_file_changes.go)
|
|
[acp_turn_normalizer.go](../../../packages/agent/daemon/runtime/acp_turn_normalizer.go)
|
|
[reporter_message.go](../../../packages/agent/daemon/runtime/reporter_message.go)
|
|
[reporter_state.go](../../../packages/agent/daemon/runtime/reporter_state.go)
|
|
[agentTurnSummaryProjection.ts](../../../packages/agent/gui/shared/agentConversation/projection/agentTurnSummaryProjection.ts)
|
|
|
|
### Remote agent cancel does not stop the local turn
|
|
|
|
- Symptom:
|
|
A cancel request returns successfully and the provider adapter logs a remote
|
|
cancel notification, but the session remains `running` and continues to emit
|
|
model output.
|
|
- Quick checks:
|
|
Inspect the runtime controller path for the active turn's local
|
|
`context.CancelFunc`. A provider-level cancel or ACP notification is not
|
|
enough if the local `Exec` goroutine is still waiting on the original turn
|
|
context.
|
|
- Root cause:
|
|
Some providers treat cancel as a notification and may return no immediate
|
|
terminal events. If the controller does not also cancel the local active turn
|
|
context, `runExecTurn` cannot converge through its context-canceled path.
|
|
- Fix:
|
|
Once an active turn is found, cancel its local context as part of the
|
|
controller cancel flow, then call the provider adapter cancel hook so both
|
|
local and remote paths are interrupted.
|
|
- Validation:
|
|
Add a controller test with an adapter that returns no cancel events and only
|
|
exits when its `Exec` context is canceled. A direct API smoke should return
|
|
HTTP 200 and final session status `canceled`.
|
|
- References:
|
|
[controller.go](../../../packages/agent/daemon/runtime/controller.go)
|
|
[controller_test.go](../../../packages/agent/daemon/runtime/controller_test.go)
|
|
|
|
### Claude Code completes but the Turn has no Fork entry
|
|
|
|
- Symptom:
|
|
A Claude Code Turn is settled and has assistant output, but AgentGUI does not
|
|
offer Fork. The session capability reports `forkThroughTurn=false`, or its
|
|
supported provider Turn list is empty even though the Claude transcript is
|
|
readable. On a newly created Session, the same missing binding can make the
|
|
completed reply roll back to the new-conversation screen with
|
|
`provider turn was not durably accepted`.
|
|
- Quick checks:
|
|
Compare the canonical Turn's `root_provider_turn_id` with the UUID of the
|
|
matching root user message returned by the official Claude SDK
|
|
`getSessionMessages` API. If they differ, inspect the live sidecar event
|
|
sequence for `provider_turn_identity_resolved` and the canonical activity
|
|
sequence for `root_provider_turn.started`. Do not infer identity from
|
|
transcript position or substitute the canonical Tutti Turn ID.
|
|
- Root cause:
|
|
The outbound user-message UUID is only a prompt correlation value. Claude
|
|
Code may rewrite that UUID before persisting the transcript. Publishing the
|
|
caller-generated value as canonical provider identity makes strict prefix
|
|
verification correctly reject the Turn, which removes the Fork capability.
|
|
The SDK can also omit the persisted root user echo from a successful live
|
|
query. Activating on assistant output while waiting exclusively for that echo
|
|
can deadlock when approval or user input occurs before result: the Provider
|
|
waits for the user, the sidecar waits for result-time recovery, and the Host
|
|
waits for durable acceptance.
|
|
- Fix:
|
|
Mark the next root prompt echo as causally expected before submitting it.
|
|
Bind provider Turn identity from that observed root user-message UUID. Route
|
|
every root assistant, stream, tool, approval, user-input, and result path
|
|
through the shared single-flight identity barrier. Without an echo, resolve
|
|
exactly one root user UUID and checkpoint through the official
|
|
`getSessionMessages` transcript read with bounded cancellable retries.
|
|
Emit `provider_turn_identity_resolved`, synchronously persist the acceptance
|
|
binding in the Host, then publish canonical `root_provider_turn.started`
|
|
before any interaction or streaming event. Never substitute the outbound
|
|
correlation UUID.
|
|
Keep historical Turns without observed provider identity fail-closed rather
|
|
than guessing or backfilling them.
|
|
- Validation:
|
|
Cover a query whose root user echo rewrites the outbound UUID. Assert
|
|
`provider_turn_identity_resolved`, checkpoint, and terminal events all carry
|
|
the persisted UUID. Cover successful assistant/result, approval,
|
|
`AskUserQuestion`, and `ExitPlanMode` sequences with no root user echo.
|
|
Assert canonical `root_provider_turn.started` is durable and ordered before
|
|
every interaction, then verify Fork accepts the same identity. Restart the
|
|
daemon/sidecar with an accepted incomplete Turn and confirm it is recovered
|
|
without re-dispatch. Historical affected Turns remain intentionally
|
|
non-forkable.
|
|
- References:
|
|
[sessionRuntime.ts](../../../packages/agent/claude-sdk-sidecar/src/sessionRuntime.ts)
|
|
[turnLifecycle.ts](../../../packages/agent/claude-sdk-sidecar/src/turnLifecycle.ts)
|
|
[claude_sdk_execution.go](../../../packages/agent/daemon/runtime/claude_sdk_execution.go)
|
|
[claude_sdk_events.go](../../../packages/agent/daemon/runtime/claude_sdk_events.go)
|
|
|
|
### A completed Turn loses its Fork entry after a rail refresh
|
|
|
|
- Symptom:
|
|
A selected Session detail initially offers Fork on its latest completed Turn,
|
|
but switching provider scope, refreshing the conversation rail, or receiving
|
|
a section page removes the action. The canonical Turn still has
|
|
`root_provider_turn_id` and `provider_turn_binding_json`; a full Session
|
|
detail reports `providerForkBindingState=bound`, while the list projection
|
|
reports `recovery_required` for the same Turn and timestamp.
|
|
- Quick checks:
|
|
Compare the list and full-detail payloads for the exact Session and Turn.
|
|
Then inspect the activity-engine Turn before and after
|
|
`session/snapshotReceived`. An image attachment is unrelated unless the
|
|
provider binding itself is absent.
|
|
- Root cause:
|
|
Session list and section responses batch-project the latest Turn. Treating a
|
|
non-persisted provider-binding availability flag as canonical without
|
|
resolving it returns a fail-closed value. The rail then merges that
|
|
lightweight latest-Turn entity into the same canonical Turn as the
|
|
authoritative detail. Equal timestamps and outcomes permit the whole Turn
|
|
replacement, so the fail-closed list value overwrites `bound`.
|
|
- Fix:
|
|
Resolve settled latest/active Turn forkability in the shared batch response
|
|
projector and cache duplicate probes within the request. Keep list Session
|
|
capabilities lightweight, but mark full capability projections explicitly.
|
|
In Activity Core, a lightweight Session snapshot may upgrade a provider Turn
|
|
binding but must not downgrade an already bound Turn. Keep message and
|
|
TuttiMode revisions monotonic for the same reason; Goal synchronization needs
|
|
its own update version because one Goal revision has multiple sync states.
|
|
- Validation:
|
|
Cover the list entry point and duplicate active/latest projections through
|
|
the shared batch projector used by list and section reads, asserting one
|
|
cached provider probe per exact Turn and fail-closed provider errors.
|
|
Apply a lightweight `recovery_required` snapshot after an authoritative
|
|
`bound` snapshot and assert the canonical Turn remains bound. Also verify an
|
|
actual full projection can still supply authoritative lifecycle state.
|
|
- References:
|
|
[service_turns.go](../../../services/tuttid/service/agent/service_turns.go)
|
|
[sessionEntities.reducer.ts](../../../packages/agent/activity-core/src/engine/sessionEntities.reducer.ts)
|
|
|
|
### Claude Code Fork fails after the action is clicked
|
|
|
|
- Symptom:
|
|
Fork is available for a completed Claude Code Turn, but clicking it produces
|
|
a failed operation. The durable operation becomes `unknown`, no target
|
|
canonical Session is committed, and the target provider Session is absent.
|
|
- Quick checks:
|
|
Inspect the latest `workspace_agent_session_fork_operations` row and query the
|
|
target UUID with the official SDK `getSessionInfo` and
|
|
`getSessionMessages`. If both are empty, check whether the sidecar used
|
|
`query({forkSession: true})` with an empty prompt and treated
|
|
`initializationResult()` as provider mutation completion.
|
|
- Root cause:
|
|
Query initialization only completes the SDK/CLI handshake. Without a prompt,
|
|
it does not durably create the requested child transcript. Unit tests can
|
|
hide the defect if their fake marks the child created inside
|
|
`initializationResult()`. A catch that replaces the original exception with
|
|
a generic Fork error also erases whether failure occurred during source
|
|
validation, provider mutation, or child verification.
|
|
- Fix:
|
|
Use the official `forkSession(source, {upToMessageId, title})` transcript
|
|
mutation. Accept its provider-generated child UUID, advertise the driver as
|
|
non-deterministic, and let Host keep only the canonical target Session ID
|
|
deterministic. Preserve the failure stage and original SDK reason.
|
|
After provider mutation starts, any failure is `unknown` and must never be
|
|
replayed.
|
|
- Validation:
|
|
Exercise the official SDK with an in-memory SessionStore, including a Turn
|
|
whose inclusive checkpoint ends in a system message. Verify the provider
|
|
child is independently readable, root user UUIDs are remapped, the canonical
|
|
child resumes by the returned provider Session ID, and a second Fork from
|
|
that child succeeds. Cover pre-mutation `not_started`, post-mutation
|
|
`unknown`, and Host's no-replay behavior for non-deterministic drivers.
|
|
- References:
|
|
[sessionFork.ts](../../../packages/agent/claude-sdk-sidecar/src/sessionFork.ts)
|
|
[sessionFork.test.ts](../../../packages/agent/claude-sdk-sidecar/src/sessionFork.test.ts)
|
|
[claude_sdk_fork.go](../../../packages/agent/daemon/runtime/claude_sdk_fork.go)
|
|
[session_fork.go](../../../packages/agent/host/session_fork.go)
|
|
|
|
### A provider-accepted Session Fork prevents Tutti from starting
|
|
|
|
- Symptom:
|
|
Tutti exits during cold startup with `materialize accepted session fork` and
|
|
a provider-owned binding count that differs from the frozen canonical
|
|
provider-bound Turn count. The source Session and its history still exist,
|
|
while the operation remains `provider_accepted`.
|
|
- Root cause:
|
|
Claude transcripts represent tool results as top-level `user` messages even
|
|
though they continue the current canonical Turn. Treating every top-level
|
|
Claude `user` message as a root Turn creates extra provider bindings. More
|
|
generally, after any provider accepts a Fork, recovery correctly refuses to
|
|
dispatch it again. Canonical materialization also correctly refuses to invent
|
|
missing provider Turn bindings, but the deterministic mismatch was returned
|
|
as an ordinary recovery error. One unrecoverable operation therefore poisoned
|
|
Host recovery and daemon construction on every restart.
|
|
- Fix:
|
|
Keep Claude `tool_result` messages inside the preceding root Turn checkpoint
|
|
instead of creating independent bindings. As a separate final safety net,
|
|
classify only deterministic provider-owned materialization evidence
|
|
mismatches as permanent. Atomically fail that accepted operation, retain its
|
|
provider acceptance evidence and the complete source history, remove any
|
|
incomplete target, and release its reservation and boundary barrier. Do not
|
|
re-dispatch the provider. Keep transient SQLite errors and failures to persist
|
|
the quarantine startup-fatal. After quarantine persists, emit a content-free
|
|
`agent_session.fork.materialization_quarantined` warning to `tuttid.log` with
|
|
operation identities, driver kind, returned binding count, and the mismatch.
|
|
- Validation:
|
|
Feed the Claude Fork adapter a real transcript shape containing root prompts,
|
|
assistant tool calls, and top-level tool-result messages. Require one binding
|
|
per root prompt and require the tool result to extend the preceding Turn's
|
|
checkpoint. Separately, seed a real temporary SQLite database with 29
|
|
provider-bound canonical Turns, three returned bindings, and a
|
|
`provider_accepted` operation. Recover through
|
|
`Host.Recover`, verify the source still has 29 Turns, the target is absent,
|
|
provider evidence remains, isolation rows are gone, no provider Fork call was
|
|
made, and a later valid operation commits. Inject quarantine persistence
|
|
failure as the negative control. Start the real `tuttidd` binary from a
|
|
poisoned state directory and require listener publication plus healthy
|
|
`/v1/health`.
|
|
- References:
|
|
[session_fork.go](../../../packages/agent/host/session_fork.go)
|
|
[session_fork_operations.go](../../../packages/agent/store-sqlite/session_fork_operations.go)
|
|
[session_fork_conformance_test.go](../../../packages/agent/host/session_fork_conformance_test.go)
|
|
[blackbox_test.go](../../../services/tuttid/integration/blackbox_test.go)
|
|
|
|
### Fork reports only `agent_session_fork_conflict`
|
|
|
|
- Symptom:
|
|
A through-Turn Fork returns HTTP 409 before any provider `thread/fork`
|
|
request. Desktop diagnostics contain only
|
|
`reason=agent_session_fork_conflict` or a developer-message length, so
|
|
provenance, attachment, descendant-lane, and provider-Turn failures are
|
|
indistinguishable.
|
|
- Quick checks:
|
|
Read `error.params.forkBoundaryReason` from the 409 response or the promoted
|
|
Desktop diagnostic `reason`. For example,
|
|
`agent_session_fork_turn_sequence_unverified` means the selected Turn has
|
|
unverified sequence provenance, while
|
|
`agent_session_fork_prefix_sequence_unverified` identifies an earlier Turn
|
|
in the inclusive prefix. The developer message carries the observed phase,
|
|
provenance, sequence, or identity condition without message content.
|
|
- Root cause:
|
|
`CheckSessionForkThroughTurn` collapsed every fail-closed boundary branch to
|
|
`supported=false`; Host replaced it with one generic Turn-state error, and
|
|
the service formatted the nested error with `%v`, which discarded the error
|
|
chain before transport classification.
|
|
- Fix:
|
|
Preserve one stable boundary rejection reason from the Store through Host
|
|
and Service. Keep the public 409 reason
|
|
`agent_session_fork_conflict` for compatibility, add the exact stable code
|
|
as `error.params.forkBoundaryReason`, and promote that code only for Desktop
|
|
diagnostics. Do not log transcript payloads or attachment contents.
|
|
- Validation:
|
|
Cover unverified selected/prefix sequences, duplicate provider Turn IDs,
|
|
descendant lanes, and session-local attachments at the Store. Verify Service
|
|
wrapping preserves both the generic conflict and typed boundary error, the
|
|
API includes the structured parameter, and Desktop promotes it to the
|
|
diagnostic reason.
|
|
- References:
|
|
[session_fork.go](../../../packages/agent/store-sqlite/session_fork.go)
|
|
[session_fork_types.go](../../../packages/agent/store-sqlite/session_fork_types.go)
|
|
[daemon_agent_session_fork.go](../../../services/tuttid/api/daemon_agent_session_fork.go)
|
|
[desktopAgentActivityAdapter.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/desktopAgentActivityAdapter.ts)
|
|
|
|
### Claude Code cancel leaves Write/tool cards or thinking stuck in progress
|
|
|
|
- Symptom:
|
|
User stops a Claude Code turn while a tool such as Write is running, or while
|
|
the assistant is still in a thinking disclosure. The turn settles as
|
|
canceled/interrupted, but the transcript still shows the tool as in progress
|
|
or thinking as forever-"thinking".
|
|
- Quick checks:
|
|
Compare durable tool-call / `assistant_thinking` message status with turn
|
|
outcome. If the turn is interrupted/canceled and an open `tool_call` is still
|
|
`running`, or thinking is still `streaming`/`working`, the Claude SDK turn
|
|
lifecycle did not finish dangling normalizer-owned rows. Confirm Codex/ACP
|
|
cancel of the same shape closes open tools and thinking via
|
|
`acpTurnNormalizer.FinishInterrupted`.
|
|
- Root cause:
|
|
Claude Code SDK first projected tool events without owning the shared turn
|
|
event lifecycle (`acpTurnNormalizer`), so cancel settled the turn without
|
|
`Finish*` and open tools never received terminal `call.failed`. A follow-on
|
|
gap kept thinking/assistant snapshots off that same normalizer: only tools
|
|
were tracked, so Stop could fail open Write cards while leaving an in-flight
|
|
thinking row at `streamState=streaming`.
|
|
- Fix:
|
|
Attach per-turn `acpTurnNormalizer` on the Claude SDK session. Track
|
|
`call.started/completed/failed` against that normalizer, route thinking and
|
|
assistant snapshots through the same normalizer, and call
|
|
`FinishInterrupted` / `FinishFailed` / `FinishCompleted` as part of turn
|
|
terminalization (`Cancel`, sidecar `turn_*`, reader failure). Drop late tool
|
|
events after the turn is already settled.
|
|
Also: controller Cancel cancels the Exec context before `adapter.Cancel`.
|
|
Claude Exec unregisters its waiter on that context cancel, so Cancel must
|
|
finish open tools/streams from the turn-normalizer map (not only live
|
|
waiters). The controller Exec context-canceled path must retain those
|
|
adapter-produced close events via `retainTurnCallLifecycleEvents` — not only
|
|
`call.failed`, but also failed/completed assistant/thinking message
|
|
snapshots — instead of replacing the whole event slice with a bare
|
|
turn.canceled. Otherwise FinishInterrupted runs in Exec, then the controller
|
|
drops the thinking settlement and the durable row stays `streaming`.
|
|
- Validation:
|
|
`go test ./packages/agent/daemon/runtime -run 'TestClaudeCodeSDKAdapter(CancelFailsOpenToolCalls|CancelFailsOpenToolsAfterWaiterUnregistered|TurnCanceledFailsOpenToolCalls|CancelFailsOpenThinking|MapsThinkingEvents)|TestRetainTurnCallLifecycleEvents'`.
|
|
Manually: rebuild/restart desktop so `tuttid` includes the fix, then start
|
|
Claude Code, stop during thinking and during a long Write; confirm thinking
|
|
leaves the active state and the tool card leaves "in progress". In
|
|
`~/.tutti-dev/tuttid.db`, the reasoning message status should leave
|
|
`streaming` after cancel.
|
|
- References:
|
|
[claude_sdk_turn.go](../../../packages/agent/daemon/runtime/claude_sdk_turn.go)
|
|
[claude_sdk_events.go](../../../packages/agent/daemon/runtime/claude_sdk_events.go)
|
|
[claude_sdk_execution.go](../../../packages/agent/daemon/runtime/claude_sdk_execution.go)
|
|
[controller_turn_exec.go](../../../packages/agent/daemon/runtime/controller_turn_exec.go)
|
|
[controller_turn_state.go](../../../packages/agent/daemon/runtime/controller_turn_state.go)
|
|
[acp_turn_normalizer.go](../../../packages/agent/daemon/runtime/acp_turn_normalizer.go)
|
|
[acp_turn_normalizer_snapshots.go](../../../packages/agent/daemon/runtime/acp_turn_normalizer_snapshots.go)
|
|
|
|
### Claude Code starts another command after Stop
|
|
|
|
- Symptom:
|
|
User stops a Claude Code turn after a Bash command becomes a background task.
|
|
The turn settles as canceled, but background completion is followed by a new
|
|
assistant continuation, approval, or Bash command such as `ls`. The new
|
|
provider id may be synthetic even though no sub-agent exists.
|
|
- Quick checks:
|
|
Correlate provider session id, canonical root turn id, and sidecar events. If
|
|
a background `task_notification` arrives after `turn_canceled`, followed by
|
|
synthetic `turn_started` or `approval_requested`, inspect SDK Query lifetime.
|
|
Confirm the exported session has no child-session relation before classifying
|
|
the continuation as a sub-agent.
|
|
- Root cause:
|
|
Claude Agent SDK `interrupt()` stops current query execution but does not
|
|
terminate the Query or all background resources. Reusing that Query lets a
|
|
late background completion trigger another root inference. Filtering the
|
|
synthetic event in AgentGUI or daemon persistence is too late: provider code
|
|
may already have requested or executed the tool, especially under
|
|
`bypassPermissions`.
|
|
- Fix:
|
|
Treat each SDK Query as an execution generation. Cancel revokes the generation
|
|
before calling the SDK, rejects its pending interactions, awaits the interrupt
|
|
acknowledgment, then closes it in cleanup. Fence messages, hooks, and
|
|
`canUseTool` by generation identity. Give the next real user prompt a fresh
|
|
prompt queue and Query using `resume: providerSessionId`. Consume a canceled
|
|
generation's replayed terminal task notification and paired result before
|
|
they can settle the new canonical Turn. Keep normal non-canceled child
|
|
completion continuations enabled. As defense in depth, reject a new pending
|
|
Interaction whose canonical owning Turn is already settled.
|
|
- Validation:
|
|
Run `pnpm --filter @tutti-os/claude-sdk-sidecar test` and
|
|
`go test ./packages/agent/store-sqlite -run 'TestUpsertInteractionRejectsNewPendingRequestOnSettledTurn'`.
|
|
Cover default and `bypassPermissions`, assert no synthetic turn, approval, or
|
|
tool permission after cancel, then assert a new real prompt resumes the same
|
|
provider session.
|
|
- References:
|
|
[sessionRuntime.ts](../../../packages/agent/claude-sdk-sidecar/src/sessionRuntime.ts)
|
|
[queryGeneration.ts](../../../packages/agent/claude-sdk-sidecar/src/queryGeneration.ts)
|
|
[activity_turns.go](../../../packages/agent/store-sqlite/activity_turns.go)
|
|
|
|
### Claude Code follow-up, settings, or cancel-resend becomes silent
|
|
|
|
- Symptom:
|
|
A completed Claude Code conversation accepts a follow-up but emits no SDK
|
|
frames. Changing model, reasoning, or speed can time out and make later sends
|
|
appear disabled. Canceling immediately after submit can leave the next send
|
|
rejected as `provider_session_not_established` even though the provider
|
|
Session exists.
|
|
- Quick checks:
|
|
Compare the sidecar request with the SDK Query generation. A follow-up on the
|
|
same quiet post-result generation, or `apply_settings` waiting inside that
|
|
generation, identifies the idle-query path. For cancel-resend, compare the
|
|
canonical Turn settlement with its durable provider-Turn binding; settlement
|
|
before the binding explains `HasSettledTurn && !Established`.
|
|
- Root cause:
|
|
A naturally completed SDK iterator was treated as a terminal Session, while
|
|
follow-up and live settings reused or mutated the now-quiet Query. Separately,
|
|
cancel could settle the canonical Turn before provider acceptance crossed the
|
|
durable identity barrier. The frontend could then race another send against
|
|
an unsettled settings write.
|
|
- Fix:
|
|
Keep Session lifetime separate from Query-generation lifetime. Retire an idle
|
|
post-turn Query and resume a fresh generation for the next prompt; serialize
|
|
settings flag application with turn dispatch and retire the idle generation
|
|
before a live settings mutation. The workspace Engine and composer gate both
|
|
block send while the settings operation is unsettled. For cancel, carry the
|
|
exact Turn ID to the sidecar and dispatch the native cancel before consulting
|
|
acceptance state. The sidecar returns `pre_accept`, `provider_active`,
|
|
`absent`, or `mismatch`: locally remove only an undispatched queue item; after
|
|
dispatch, publish the canceled terminal only after the bounded Query shutdown
|
|
protocol either receives the SDK interrupt acknowledgment or closes the owned
|
|
transport and drains its consumer. Wait for the exact Turn's durable
|
|
provider-acceptance outcome only for `provider_active`. Treat only `absent` as
|
|
authoritative not-found; mismatch, unknown disposition, drain failure, and
|
|
acceptance failure stay fail-closed.
|
|
- Validation:
|
|
Cover follow-up resume, settings timeout/retry gating, exact targeted cancel,
|
|
same-tick Goal cancellation, cancel before dispatch, interrupt acknowledgment,
|
|
missing acknowledgment, interrupt failure, consumer-drain timeout, durable
|
|
acceptance success and failure, mismatched/absent targets, and cancel followed
|
|
by a new send. Native guidance must interrupt active tool work before enqueueing
|
|
the steering prompt.
|
|
- References:
|
|
[sessionRuntime.ts](../../../packages/agent/claude-sdk-sidecar/src/sessionRuntime.ts)
|
|
[claude_sdk_execution.go](../../../packages/agent/daemon/runtime/claude_sdk_execution.go)
|
|
[controller_exec.go](../../../packages/agent/daemon/runtime/controller_exec.go)
|
|
[promptQueue.reducer.ts](../../../packages/agent/activity-core/src/engine/promptQueue.reducer.ts)
|
|
[agentGuiComposerGate.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/model/agentGuiComposerGate.ts)
|
|
|
|
### AgentGUI freezes when session history is large
|
|
|
|
- Symptom:
|
|
The workspace renderer freezes, tears visually in screen recordings, or feels
|
|
stuck while opening AgentGUI or submitting an agent prompt in a workspace with
|
|
a long agent history.
|
|
- Quick checks:
|
|
Inspect developer logs for `agent.gui.runtime.snapshot_changed` diagnostics.
|
|
If `sessionCount` is in the hundreds or thousands, check whether the desktop
|
|
adapter is calling `listWorkspaceAgentSessions` without a `limit`.
|
|
- Root cause:
|
|
Unbounded session-list loads push every historical agent session into
|
|
`AgentGUIRuntime`, and each live event can make AgentGuiNode rebuild
|
|
conversation projections for history the visible rail does not need.
|
|
- Fix:
|
|
Keep broad runtime session-list requests bounded at the desktop adapter or
|
|
daemon API boundary. Use targeted message/session fetches for the selected
|
|
detail rather than widening the runtime snapshot.
|
|
- Validation:
|
|
Reproduce with a large session table and confirm runtime diagnostics report a
|
|
bounded `sessionCount`. Run the desktop adapter tests and `pnpm check:changed`
|
|
for mixed AgentGUI/desktop changes.
|
|
- References:
|
|
[desktopAgentActivityAdapter.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/desktopAgentActivityAdapter.ts)
|
|
[createDesktopAgentActivityRuntime.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/createDesktopAgentActivityRuntime.ts)
|
|
|
|
### AgentGUI @ Sessions tab is empty
|
|
|
|
- Symptom:
|
|
Opening the composer `@` palette (default Sessions tab) shows no session
|
|
rows, even though the workspace has agent history.
|
|
- Quick checks:
|
|
In `tuttid.log`, look for `event=workspace.agent_session.api.list_completed`
|
|
from `GET /v1/workspaces/{workspaceID}/agent-sessions` (the Sessions-tab
|
|
source). Check `session_count`. Do not confuse it with
|
|
`workspace.agent_session.messages.api.list_*` or
|
|
`workspace.agent_session.section.list_failed`.
|
|
- Root cause:
|
|
The Sessions tab loads through `listWorkspaceAgentSessions`. Successful calls
|
|
previously left no durable log, so empty palettes could not be distinguished
|
|
from "API never ran" or "API returned zero sessions" in exported logs.
|
|
- Fix:
|
|
Successful list responses now emit
|
|
`workspace.agent_session.api.list_completed` with `session_count`. If the
|
|
event is missing, the client never hit the endpoint; if `session_count=0`,
|
|
the daemon truly returned an empty list.
|
|
- Validation:
|
|
Click the composer `@` button, confirm a
|
|
`workspace.agent_session.api.list_completed` line appears with a non-zero
|
|
`session_count` when sessions exist.
|
|
- References:
|
|
[daemon_agent_session_list.go](../../../services/tuttid/api/daemon_agent_session_list.go)
|
|
[desktopRichTextAtAgentContributors.ts](../../../apps/desktop/src/renderer/src/features/rich-text-at/services/internal/desktopRichTextAtAgentContributors.ts)
|
|
[agent-gui-node.md](../../architecture/agent-gui-node.md)
|
|
|
|
### AgentGUI @ browse fails after reopening
|
|
|
|
- Symptom:
|
|
The AgentGUI `@` palette opens once, then shows a search failure after the
|
|
palette is closed and reopened, even though the workspace request itself is
|
|
healthy.
|
|
- Quick checks:
|
|
Inspect the AgentGUI lifecycle diagnostics for a `browse.fetch.start` entry
|
|
followed by `AbortError` on the next open. The failing request usually has
|
|
the same browse key as the request canceled when the first palette closed.
|
|
- Root cause:
|
|
A shared browse request was kept in its deduplication map until its promise's
|
|
asynchronous `finally` cleanup ran. A new consumer could arrive in that
|
|
window and attach to the already-aborted request, turning expected palette
|
|
cancellation into a visible search failure.
|
|
- Fix:
|
|
Use `AbortableSingleFlight` for shared abortable requests. Evict the entry
|
|
synchronously when its final consumer leaves, and make late cleanup delete
|
|
only the exact request instance. An operation owner may also abort a stuck
|
|
request immediately without leaving a retryable stale entry.
|
|
- Validation:
|
|
Cover concurrent deduplication, one-consumer cancellation, immediate retry
|
|
after final-consumer cancellation, operation-owner timeout cancellation, and
|
|
late completion of the old request after a replacement starts.
|
|
- References:
|
|
[abortableSingleFlight.ts](../../../packages/agent/gui/shared/query/abortableSingleFlight.ts)
|
|
[AgentMentionSearchCache.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/AgentMentionSearchCache.ts)
|
|
|
|
### Agent diagnostics flood while a turn is streaming
|
|
|
|
- Symptom:
|
|
Exported developer logs are dominated by agent diagnostics and the app feels
|
|
sluggish while a streaming turn is active or while switching AgentGUI sessions.
|
|
- Quick checks:
|
|
Count repeated `agent submit trace`, `agent.activity.reconcile.trace`, and
|
|
`agent.gui.node.render_state_changed` lines before blaming one visible click.
|
|
Runtime event emissions should appear as
|
|
`runtime.events_emitted.summary`/`runtime.async_events_emitted.summary`;
|
|
the old `runtime.events_emitted`/`runtime.async_events_emitted` names must not
|
|
appear at the default level. Successful reconcile steps, message-page reads,
|
|
ACP transport frames, and unchanged CuaDriver polls should appear only when
|
|
debug logging is enabled. A streaming message-version change alone must not
|
|
emit `agent.gui.node.render_state_changed` or
|
|
`agent.gui.runtime.snapshot_changed`.
|
|
- Root cause:
|
|
Per-token runtime events and renderer inline reconcile commits can produce
|
|
thousands of diagnostic writes. Those writes compete with rendering and also
|
|
inflate trace/log exports enough to obscure the actual session-switch work.
|
|
A later AgentGUI refactor can reintroduce this problem by replacing turn
|
|
summaries with per-batch logs or by adding message cursors to diagnostic
|
|
change keys.
|
|
- Fix:
|
|
Aggregate runtime emissions once per turn. Keep successful reconcile,
|
|
message-page, and ACP frame diagnostics at debug. Build renderer snapshot and
|
|
render-state keys from semantic lifecycle/interaction state rather than
|
|
streaming cursors. Log unchanged permission-poll results at debug, and keep
|
|
desktop log writes ordered through the asynchronous file writer.
|
|
- Validation:
|
|
Reproduce a streaming turn at the default info level. Confirm each turn has
|
|
at most one runtime emission summary, semantic render/snapshot diagnostics do
|
|
not advance for token-only updates, and reconcile/ACP frame diagnostics are
|
|
absent. Repeat with debug enabled when per-frame evidence is needed. Submit,
|
|
reconcile, and protocol failures must remain visible.
|
|
- References:
|
|
[controller_turn_exec.go](../../../packages/agent/daemon/runtime/controller_turn_exec.go)
|
|
[workspaceAgentActivityReconcileBridge.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/workspaceAgentActivityReconcileBridge.ts)
|
|
[desktopAgentRuntimeStateDiagnostics.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/desktopAgentRuntimeStateDiagnostics.ts)
|
|
[useAgentGUISessionPresentation.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUISessionPresentation.ts)
|
|
|
|
### Claude export leaks hidden data, flattens branches, or resumes as Claude Code
|
|
|
|
- Symptom:
|
|
Imported claude.ai history shows internal thinking/tool payload text, mixes
|
|
mutually exclusive edited/retried messages into one timeline, or the composer
|
|
tries to resume an imported web conversation as if its UUID were a local
|
|
Claude Code session id.
|
|
- Quick checks:
|
|
Inspect the source message shape without logging its content. Visible text
|
|
must come from ordered `content[type=text]` blocks; legacy human messages
|
|
with no content blocks may fall back to top-level `message.text`, but
|
|
assistant messages never may (that field mixes in hidden thinking and tool
|
|
material). Inspect the persisted runtime context for
|
|
`externalImportResumeSupported: false`, and confirm the source path is not
|
|
forwarded as an external Claude Code rollout path. For a branched fixture,
|
|
confirm every imported message belongs to the selected latest leaf's ancestor
|
|
path and carries the same `sourceBranchLeafId`.
|
|
- Root cause:
|
|
Claude data exports use top-level `message.text` as a convenience aggregate
|
|
that can include hidden thinking and tool material. Their conversation UUIDs
|
|
belong to claude.ai, not the local Claude Code runtime, and referenced files
|
|
in `conversations.json` do not imply that file payloads exist in the ZIP.
|
|
`chat_messages` is a parent graph rather than a guaranteed linear list, so
|
|
timestamp-sorting every node can combine incompatible sibling branches.
|
|
- Fix:
|
|
Parse only the exact root `conversations.json` entry without extracting the
|
|
archive. Project visible text from text blocks, keep file-only messages as
|
|
unavailable references, select one deterministic latest root-to-leaf branch,
|
|
seed persisted message ids from source UUIDs, and include the selected sibling
|
|
choices in imported session identity so a future retry-branch change cannot
|
|
append into the old branch. Place sessions in the no-project Chats section and
|
|
mark them non-resumable while preserving the normal continue-in-new-chat
|
|
recovery action.
|
|
- Validation:
|
|
Run `cd services/tuttid && go test ./service/agent ./api -run
|
|
'ExternalImport|ClaudeExport|ExternalRollout'`, then run
|
|
`pnpm --filter @tutti-os/desktop test` and `pnpm check:i18n`.
|
|
- References:
|
|
[external_import_claude_export.go](../../../services/tuttid/service/agent/external_import_claude_export.go)
|
|
[ExternalAgentSessionImportWizard.tsx](../../../apps/desktop/src/renderer/src/features/workspace-workbench/ui/ExternalAgentSessionImportWizard.tsx)
|
|
[service_session.go](../../../services/tuttid/service/agent/service_session.go)
|
|
|
|
### Imported long Turns show one disclosure per tool call
|
|
|
|
- Symptom:
|
|
A long imported Codex or Claude Code execution shows many adjacent
|
|
`1 tool call` disclosures instead of one grouped Turn, especially after
|
|
opening or paging into the middle of the conversation.
|
|
- Quick checks:
|
|
Inspect the affected `workspace_agent_messages` rows without printing
|
|
payload text. If the user/tool/assistant rows all have `turn_id = NULL`, then
|
|
compare the loaded page with the source transcript. A page that starts after
|
|
the initiating user message cannot recover the Turn boundary locally.
|
|
- Root cause:
|
|
The external importer historically used the turnless compatibility path for
|
|
every message. AgentGUI can make a temporary presentation group when the
|
|
leading user row is loaded, but that grouping cannot survive a page boundary;
|
|
assigning each orphan row from its current page position would manufacture
|
|
lifecycle identity in the renderer.
|
|
- Fix:
|
|
At the provider transcript boundary, start a stable historical Turn from each
|
|
retained real user message and carry it through following assistant and tool
|
|
messages. Persist the messages with settled backfilled Turns atomically.
|
|
The forward-only store migration repairs existing imported rows, while a
|
|
later re-import applies the same identity to any still-turnless rows. Content
|
|
before the first trustworthy user boundary remains turnless.
|
|
- Validation:
|
|
Run `go test ./packages/agent/store-sqlite -run HistoricalImport`, then
|
|
`cd services/tuttid && go test ./service/agent -run
|
|
'ServiceImportsExternalAgentSessionsByProject|ServiceReimportRepairsLegacyTurnlessExternalMessages'`.
|
|
- References:
|
|
[external_import.go](../../../services/tuttid/service/agent/external_import.go)
|
|
[activity_historical_import_turns.go](../../../packages/agent/store-sqlite/activity_historical_import_turns.go)
|
|
[agent-gui-node.md](../../architecture/agent-gui-node.md)
|
|
|
|
### Imported sessions trigger fresh-completion indicators
|
|
|
|
- Symptom:
|
|
After importing Codex or Claude Code history, Agent GUI conversation rows show
|
|
unread-completion lamps, or Message Center's priority view briefly shows many
|
|
items under the recently-completed group, even though those sessions are
|
|
historical imports rather than newly finished local runs.
|
|
- Quick checks:
|
|
Inspect the session `runtimeContext`. Imported sessions should carry
|
|
`imported: true`. Conversation summaries and Message Center items derived from
|
|
them should preserve that marker before unread-completion or priority grouping
|
|
is derived.
|
|
- Root cause:
|
|
Agent GUI unread-completion lamps and Message Center's recently-completed
|
|
group are notification-style surfaces. Imported history is persisted as
|
|
completed agent activity, so if projection models treat imported sessions the
|
|
same as live runtime completions, a bulk import can look like a burst of fresh
|
|
completed work.
|
|
- Fix:
|
|
Keep imported sessions visible in Agent GUI, Message Center, and completed
|
|
filters, but exclude `runtimeContext.imported` items from unread-completion
|
|
lamps and recently-completed groups.
|
|
- Validation:
|
|
For Agent GUI rail projection changes, run
|
|
`pnpm --dir packages/agent/gui exec vitest run --environment jsdom contexts/workspace/presentation/renderer/agentGuiConversationList/useAgentGuiConversationList.spec.tsx agent-gui/agentGuiNode/model/agentGuiConversationModel.spec.ts`.
|
|
For Message Center grouping changes, run
|
|
`pnpm --dir packages/agent/gui exec vitest run --environment jsdom agent-message-center/workspaceAgentMessageCenterModel.spec.ts agent-message-center/workspaceAgentMessageCenterViewModel.spec.ts`.
|
|
- References:
|
|
[useAgentGuiConversationList.ts](../../../packages/agent/gui/contexts/workspace/presentation/renderer/agentGuiConversationList/useAgentGuiConversationList.ts)
|
|
[workspaceAgentMessageCenterModel.ts](../../../packages/agent/gui/agent-message-center/workspaceAgentMessageCenterModel.ts)
|
|
[workspaceAgentMessageCenterViewModel.ts](../../../packages/agent/gui/agent-message-center/workspaceAgentMessageCenterViewModel.ts)
|
|
|
|
### Realtime agent completion does not show unread attention
|
|
|
|
- Symptom:
|
|
A turn settles while Agent GUI is open, but its conversation row never shows
|
|
unread-completion attention. Historical sessions may behave correctly and
|
|
must not acquire attention merely because their snapshot was loaded.
|
|
- Quick checks:
|
|
Trace the event path into the activity engine. A realtime `turn_update`
|
|
should enter as one atomic Turn projection. When the Session is cached, the
|
|
same Engine transition must update the Turn and update or clear
|
|
`Session.activeTurnId` without rewriting Session timestamps from the event
|
|
envelope. Attention must observe the post-lifecycle canonical Turn, not the
|
|
raw event. A delayed settled event may clear only the matching active Turn.
|
|
The event should also request a state-only session reconcile with realtime
|
|
provenance. Initial, restored, and imported history should enter through
|
|
`session/snapshotReceived` only. Also confirm the projected desktop session
|
|
carries the shared local Agent GUI user id used by read-state actions.
|
|
- Root cause:
|
|
Realtime and historical data lost their provenance when both were folded into
|
|
a mutable controller snapshot and re-emitted as `session/snapshotReceived`.
|
|
The attention reducer correctly treats snapshots as non-live, so it recorded
|
|
the settled completion without producing unread attention; a later live
|
|
update with the same completion key could no longer recover the transition.
|
|
- Fix:
|
|
Keep the activity engine as the single mutable owner. Feed pull/bootstrap
|
|
results through `session/snapshotReceived`, and feed each realtime
|
|
`turn_update` through one intent that atomically projects the Turn and cached
|
|
Session reference. Reject a projection whose live/settled phase disagrees
|
|
with `activeTurnId`; reconcile instead of applying half of the wire fact.
|
|
Fence stale Session snapshots with canonical Turn versions rather than
|
|
event-envelope time, and let attention consume only a Turn that the lifecycle
|
|
reducer accepted.
|
|
Keep the realtime marker on the follow-up reconcile so an uncached Session
|
|
can hydrate and replay the latest Turn with live attention semantics. Use
|
|
inline message events only for message deltas and one shared local identity
|
|
for session projection and read-state commands.
|
|
- Validation:
|
|
Run
|
|
`pnpm --filter @tutti-os/desktop test -- workspaceAgentActivityService.test.ts`
|
|
and verify the service integration coverage proves that realtime completion
|
|
becomes unread while a settled historical load creates no attention record.
|
|
- References:
|
|
[workspaceAgentActivityReconcileBridge.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/workspaceAgentActivityReconcileBridge.ts)
|
|
[workspaceAgentActivityService.test.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/workspaceAgentActivityService.test.ts)
|
|
[attentionReadState.reducer.ts](../../../packages/agent/activity-core/src/engine/attentionReadState.reducer.ts)
|
|
|
|
### Unfocused AgentGUI Session completion becomes read
|
|
|
|
- Symptom:
|
|
A running Session settles after the user focuses another AgentGUI node or
|
|
window, but the original Session immediately loses its unread-completion lamp.
|
|
- Quick checks:
|
|
Inspect `agent.gui.attention_read.decision`. The diagnostic identifies the
|
|
node, completion key, host focus, document exposure, visibility, and whether
|
|
the completion was read or preserved. A retained node with
|
|
`isSurfaceActive=false`, `isSurfaceVisible=false`, or
|
|
`isSurfaceDocumentExposed=false` must report `decision=preserve_unread`; its
|
|
local `activeConversationId` is not evidence that the user is reading it.
|
|
- Root cause:
|
|
AgentGUI selection is local to each mounted controller, while attention/read
|
|
state is shared by the workspace engine. Treating every controller's active
|
|
Session as visually selected allowed a hidden or unfocused retained node to
|
|
dispatch the shared `attention/read` intent. Separately, historical Turns
|
|
without a durable marker were assigned `isUnread=false` and persisted into
|
|
`readIds`; a stale list/detail response could therefore turn an unseen live
|
|
completion into durable read state.
|
|
- Fix:
|
|
Use unread-only attention state. Only a live completion transition or an
|
|
explicit unread request creates an unread record; `attention/read` removes
|
|
that record, and historical snapshots never mutate attention. Hydration
|
|
ignores legacy `readIds` and the next write clears those buckets. Pass
|
|
host-projected focus, host visibility, and renderer document focus/visibility
|
|
into the selection controller, and require all three before dispatching
|
|
`attention/read`. Keep the existing manual unread provenance rule so a
|
|
user-marked unread completion stays unread until the Session is selected
|
|
again.
|
|
- Validation:
|
|
Keep two AgentGUI nodes mounted, run a Turn in the unfocused node's active
|
|
Session, and verify its completion key remains in `completed.unreadIds`.
|
|
Focusing and exposing that node should remove the exact key while leaving
|
|
`completed.readIds` empty.
|
|
- References:
|
|
[AgentGUINode.tsx](../../../packages/agent/gui/agent-gui/agentGuiNode/AgentGUINode.tsx)
|
|
[useAgentGUIConversationSelectionController.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUIConversationSelectionController.ts)
|
|
|
|
### Completed agent session stays activating and disables the composer
|
|
|
|
- Symptom:
|
|
A new conversation visibly completes and its assistant reply is present, but
|
|
opening it leaves the composer disabled. Roughly one activation-expiry window
|
|
later, AgentGUI reports that the agent session could not be started.
|
|
- Quick checks:
|
|
Correlate activation diagnostics with the authoritative session updates. If
|
|
the session create and turn both succeeded while the presentation remains
|
|
`activating` until `engine/intentExpired`, inspect which session intent
|
|
reached the pending-activation reducer. Also check that a failed realtime
|
|
session fetch preserves `live: true` on the Engine's pending reconcile
|
|
record before a retry.
|
|
- Root cause:
|
|
An engine migration introduced `session/upserted` for authoritative mutation
|
|
and realtime results, while pending activation still confirmed only from the
|
|
historical `session/snapshotReceived` path. The canonical session therefore
|
|
existed and could render, but the independent activation intent expired and
|
|
overrode the composer with a false failure. Consuming realtime provenance
|
|
before a fallible fetch can produce a related retry-only mismatch.
|
|
- Fix:
|
|
Confirm activation from both authoritative session intents. Preserve the
|
|
semantic distinction only where it matters: historical snapshots remain
|
|
neutral for unread attention, while realtime reconciliation additionally
|
|
emits the live turn update. Carry realtime provenance on the Engine-owned
|
|
reconcile command, merge it into in-flight demand, and restore it to pending
|
|
demand after fetch failure until a live session is applied or the session is
|
|
deleted. Hosts must not keep a parallel marker set.
|
|
- Validation:
|
|
Cover the reducer with a pending activation followed by `session/upserted`.
|
|
At the desktop service boundary, run a real engine activation through the
|
|
create result, manually expire its old deadline, and verify the presentation
|
|
remains active. Also fail the first realtime reconciliation, retry it, and
|
|
verify the settled turn still gains unread attention.
|
|
- References:
|
|
[pendingIntents.reducer.ts](../../../packages/agent/activity-core/src/engine/pendingIntents.reducer.ts)
|
|
[workspaceAgentActivityReconcileBridge.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/workspaceAgentActivityReconcileBridge.ts)
|
|
[workspaceAgentActivityService.test.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/workspaceAgentActivityService.test.ts)
|
|
|
|
### Forked conversation disappears from the Rail after completion
|
|
|
|
- Symptom:
|
|
A newly forked conversation is visible and selected while its first Turn is
|
|
running, but disappears from the Rail as soon as that Turn settles. Opening
|
|
the exact Session still works, and a later full Rail reload may restore it.
|
|
- Quick checks:
|
|
Correlate `session/forkThroughTurnRequested`, the canonical child
|
|
`session/upserted`, and the bounded `listSessionSectionPage` request. If the
|
|
child becomes canonical while selected but no exact request is made for its
|
|
inherited `railSectionKey`, inspect pending-creation membership rather than
|
|
the fork persistence path. Also verify that the child was initially visible
|
|
only through the active or running overlay.
|
|
- Root cause:
|
|
Fork selected the child immediately, while membership reconciliation skipped
|
|
every newly observed active Session to avoid refreshing for historical detail
|
|
hydration. The fork child therefore never entered its bounded authoritative
|
|
section page. Its active/running overlay temporarily hid that omission and
|
|
settlement exposed it by removing the overlay row.
|
|
- Fix:
|
|
Project in-flight fork targets and pending activations as pending-creation
|
|
reconciliation evidence. Keep that semantic marker separate from render
|
|
provenance. When the same identity becomes canonical, refresh its exact
|
|
persisted Rail section even when selected; continue skipping ordinary
|
|
selected historical hydration when no pending-creation evidence exists.
|
|
- Validation:
|
|
Drive a real `AgentSessionEngine` and
|
|
`AgentGUIConversationRailQueryController` through a held fork command,
|
|
select the target before canonical upsert, then verify the controller requests
|
|
the inherited section and publishes the child in authoritative membership.
|
|
Retain the existing selected historical-Session no-refresh coverage.
|
|
- References:
|
|
[agentGuiConversationRailMembershipRecords.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/model/agentGuiConversationRailMembershipRecords.ts)
|
|
[agentGuiConversationRailMembershipRefresh.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/model/agentGuiConversationRailMembershipRefresh.ts)
|
|
[AgentGUIConversationRailQueryController.spec.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/AgentGUIConversationRailQueryController.spec.ts)
|
|
|
|
### Shared Agent composer stays disabled after the target connects
|
|
|
|
- Symptom:
|
|
A shared Agent target reaches `connected`, and diagnostics show submission
|
|
readiness has recovered, but the entire composer remains disabled and cannot
|
|
receive focus or input. This differs from an empty draft disabling only the
|
|
send button.
|
|
- Quick checks:
|
|
Inspect one rendered Composer gate snapshot. Its runtime, editor, and
|
|
submission branches must agree: a ready submission cannot coexist with a
|
|
target-connection runtime block. If logs instead compare fields from separate
|
|
Composer and readiness projections, inspect view-model memoization before
|
|
debugging P2P transport.
|
|
- Root cause:
|
|
Connection and submission facts were projected into independent memoized
|
|
slices. A missing dependency could retain the old target-connection block
|
|
while publishing the new submission-ready value, and a downstream detail
|
|
model then recombined those two render-time generations into a torn
|
|
`composerDisabled` decision.
|
|
- Fix:
|
|
Derive the canonical Composer gate once at the Session-presentation boundary.
|
|
Keep editor editability, submission readiness/queue/blocking, and
|
|
runtime-command availability in that one object, then pass it through one
|
|
view-model slice to the editor, send button, shortcuts, Stop control, and
|
|
Interaction paths. Keep draft-empty and upload conditions submission-local.
|
|
- Validation:
|
|
Drive an exact shared target from `connecting` to `connected` and assert the
|
|
same resulting snapshot reports runtime ready, editor editable, and
|
|
submission ready. Also cover busy queue behavior, collaborator read-only
|
|
behavior, and the invariant that submission ready never retains a runtime
|
|
connection block.
|
|
- References:
|
|
[agentGuiComposerGate.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/model/agentGuiComposerGate.ts)
|
|
[useAgentGUISessionPresentation.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUISessionPresentation.ts)
|
|
[useAgentGUIViewModel.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/model/useAgentGUIViewModel.ts)
|
|
|
|
### AgentGUI submit clears the composer but creates no session or turn
|
|
|
|
- Symptom:
|
|
Sending from AgentGUI clears or switches the composer, but the conversation
|
|
rail and transcript do not change. Renderer diagnostics stop at
|
|
`renderer_adapter.create.http_requested` or
|
|
`renderer_adapter.send.http_requested`, and the daemon has no matching
|
|
`clientSubmitId`.
|
|
- Quick checks:
|
|
Correlate `clientSubmitId` across the desktop and daemon logs. If the engine
|
|
records an immediate failed activation while the daemon has no create/send
|
|
business log, compare the adapter's exact JSON body with the generated
|
|
request type and the OpenAPI schema, including `additionalProperties`.
|
|
- Root cause:
|
|
A conditional object spread can add a stale property to an otherwise typed
|
|
request without triggering excess-property checking. Strict OpenAPI request
|
|
validation then rejects the body before the business handler, while eager
|
|
composer clearing makes the failed request look successful for an instant.
|
|
- Fix:
|
|
Keep `clientSubmitId` as the top-level idempotency field and carry optional
|
|
evidence through the typed `submitDiagnostics` contract from AgentGUI through
|
|
the session engine to the desktop adapter. Assign the final body to the
|
|
generated request type before sending. Clear a draft only after the engine
|
|
queues, accepts, or confirms the exact submitted content; failed sends retain
|
|
the draft.
|
|
- Validation:
|
|
Assert the adapter's complete create/send body with generated request types,
|
|
verify the generated client serializes `submitDiagnostics`, and cover that
|
|
Composer does not clear before its parent applies engine acknowledgment.
|
|
- References:
|
|
[agent-gui-node.md](../architecture/agent-gui-node.md)
|
|
[desktopAgentActivityAdapter.ts](../../apps/desktop/src/renderer/src/features/workspace-agent/services/desktopAgentActivityAdapter.ts)
|
|
[useAgentGUISubmitInteractionActions.ts](../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUISubmitInteractionActions.ts)
|
|
|
|
### AgentGUI send reports `workspace_agent.send_response_turn_required`
|
|
|
|
- Symptom:
|
|
Sending or continuing a conversation reaches the provider, but the renderer
|
|
rejects the HTTP result with `workspace_agent.send_response_turn_required`.
|
|
A realtime reconcile may make the conversation recover moments later, so the
|
|
failure can appear release- or timing-dependent.
|
|
- Quick checks:
|
|
Correlate one `clientSubmitId` across `runtime.submitted`,
|
|
`api.send.completed`, and the following `turn_update`. If the API completes
|
|
with a submitted runtime Turn while its session projection is still settled,
|
|
then the response raced the asynchronous activity report. Inspect the raw
|
|
response: a Turn-producing branch missing either `turnId` or `turn` violates
|
|
the protocol even if realtime state arrives afterward.
|
|
- Root cause:
|
|
`RuntimeController.Exec` queued the submitted Turn report asynchronously,
|
|
started provider work, and returned. The service immediately refreshed the
|
|
older durable Session, while the API conditionally copied `session.activeTurn`.
|
|
The exact Turn therefore disappeared from an otherwise successful response.
|
|
- Fix:
|
|
Make the submitted Turn report a synchronous durable-acceptance barrier.
|
|
Commit the session pointer and Turn atomically before publishing or starting
|
|
provider work; roll runtime state back on report failure. Read the exact Turn
|
|
by the runtime-returned `turnId` in the service, and model send output as an
|
|
OpenAPI discriminated union whose Turn branch requires both fields. Keep Goal
|
|
control Turn-less. Do not add renderer polling, sleeps, or synthetic Turns.
|
|
- Validation:
|
|
Block the activity reporter and prove both `Exec` and provider startup wait;
|
|
fail it and prove no provider execution or active Turn survives. Cover stale
|
|
Session plus exact-Turn service reads, required API Turn output, and the
|
|
Turn-less Goal-control branch. Run API generation drift checks and the Agent
|
|
session engine suite.
|
|
- References:
|
|
[agent-gui-node.md](../../architecture/agent-gui-node.md)
|
|
[controller_exec.go](../../../packages/agent/daemon/runtime/controller_exec.go)
|
|
[service_send_input.go](../../../services/tuttid/service/agent/service_send_input.go)
|
|
[daemon_agent_submit_handlers.go](../../../services/tuttid/api/daemon_agent_submit_handlers.go)
|
|
|
|
### AgentGUI new session times out and appears completed without a reply
|
|
|
|
- Symptom:
|
|
A new conversation shows the optimistic user prompt, then becomes idle or
|
|
completed with no assistant reply. The session has no canonical turn or
|
|
messages.
|
|
- Quick checks:
|
|
Correlate the create `clientSubmitId`. A characteristic sequence is desktop
|
|
`renderer_adapter.create.failed errorCode=ETIMEDOUT` at 30 seconds, followed
|
|
by daemon `provider_runtime_status=failed`, `agent session is not connected`,
|
|
and a rejected turnless `visible-error` message.
|
|
- Root cause:
|
|
Runtime command-guide construction asked the CLI registry to run live
|
|
capability filters. The agent-context filter probes provider availability,
|
|
so static guide construction could consume the entire create-request budget.
|
|
Cancellation then reached provider startup. The failed startup was also
|
|
published as a durable session plus a message without a turn, creating a
|
|
phantom session that the UI could only project as idle/completed.
|
|
- Fix:
|
|
Build runtime command guides from static capability registration with live
|
|
capability filters skipped. Treat provider startup as transactional: return a
|
|
typed runtime error when the provider fails to start; for create-with-prompt,
|
|
keep the runtime Session provisional until the first Turn is accepted. A
|
|
failed or rolled-back attempt must not publish or store a canonical Session,
|
|
Turn, command/config snapshot, or turnless message.
|
|
- Validation:
|
|
Cover the non-blocking command-catalog context, typed failed-start mapping,
|
|
provisional provider callback isolation and rollback, and controller behavior
|
|
proving that startup failure returns diagnostics but creates no canonical
|
|
session or activity report.
|
|
- References:
|
|
[command_catalog.go](../../services/tuttid/service/agentsidecar/command_catalog.go)
|
|
[controller_session_lifecycle.go](../../packages/agent/daemon/runtime/controller_session_lifecycle.go)
|
|
[agent_runtime_adapter.go](../../services/tuttid/agent_runtime_adapter.go)
|
|
|
|
### AgentGUI cannot stop Cursor immediately after the first message
|
|
|
|
- Symptom:
|
|
The first Cursor message enters an activating state, but Stop is absent or
|
|
does nothing until a canonical session and Turn appear. Codex and Claude Code
|
|
often appear unaffected because their startup completes before a user can hit
|
|
the same window.
|
|
- Quick checks:
|
|
Correlate the activation request with process spawn, ACP `initialize`,
|
|
`session/new`, and the first Turn. If Stop becomes available only after the
|
|
Turn id is known, the UI and engine are treating cancellation as turn-only.
|
|
Also inspect failed-create cleanup after request cancellation for provisional
|
|
runtime state left under the session id.
|
|
- Root cause:
|
|
New-session activation is an abortable engine command before it becomes a
|
|
canonical Turn. A turn-only Stop gate cannot target that command, and a cancel
|
|
operation stored only under existing session entities is lost on an empty
|
|
reconciliation snapshot. Cleanup that reuses the canceled request context
|
|
can then fail to close the provisional runtime.
|
|
- Fix:
|
|
Dispatch one provider-neutral session Stop intent. Abort the exact activation
|
|
command by command id, preserve a detached workspace/session-scoped
|
|
`awaitingTurn` cancel through snapshots, and convert it to exact-turn cancel
|
|
when the first Turn arrives. Expire the detached operation after a bounded
|
|
window. Run failed-create close and cleanup with a bounded context detached
|
|
from request cancellation.
|
|
- Validation:
|
|
Cover immediate activation abort, empty-snapshot preservation, late first-Turn
|
|
cancel, expiry before an unrelated future Turn, and identical lifecycle
|
|
behavior for Cursor, Codex, and Claude Code. Verify AgentGUI exposes Stop
|
|
while new-conversation activation is still submitting.
|
|
- References:
|
|
[createAgentSessionEngine.ts](../../../packages/agent/activity-core/src/engine/createAgentSessionEngine.ts)
|
|
[sessionLifecycle.reducer.ts](../../../packages/agent/activity-core/src/engine/sessionLifecycle.reducer.ts)
|
|
[pendingIntents.reducer.ts](../../../packages/agent/activity-core/src/engine/pendingIntents.reducer.ts)
|
|
[useAgentGUIDetailModel.tsx](../../../packages/agent/gui/agent-gui/agentGuiNode/view/useAgentGUIDetailModel.tsx)
|
|
[service.go](../../../services/tuttid/service/agent/service.go)
|
|
|
|
### Cursor session/new is canceled before its 30-second timeout
|
|
|
|
- Symptom:
|
|
Sending the first message to Cursor leaves AgentGUI activating until Tutti
|
|
reports a timeout. Cursor logs show `session/new` started, but it is canceled
|
|
substantially before the ACP runtime's configured 30-second deadline.
|
|
- Quick checks:
|
|
Correlate `service.create.entered`, provider-status completion,
|
|
`runtime_start_requested`, ACP `initialize`, and `session/new`. If a full
|
|
provider-status probe runs inside `Service.Create`, or the renderer activation
|
|
expires 30 seconds after the click rather than 30 seconds after `session/new`
|
|
starts, the protocol call is receiving only a leftover budget.
|
|
- Root cause:
|
|
Provider readiness was treated as a per-session precondition even though it
|
|
performs application-scoped binary, version, and auth detection. Cursor auth
|
|
fallback commands can consume several seconds before ACP starts. An outer
|
|
30-second activation timer then propagates cancellation into the daemon and
|
|
truncates the independent 30-second `session/new` timeout.
|
|
- Fix:
|
|
Cache provider readiness in `tuttid` per provider with completion-time TTL and
|
|
single-flight refresh. Explicit setup refreshes bypass that cache; ordinary
|
|
session creation does not run status, version, auth, network, or hidden model
|
|
discovery. Let the actual process spawn and ACP handshake be authoritative.
|
|
Give new-session activation a larger outer safety deadline while preserving
|
|
the ACP runtime's own 30-second `session/new` deadline.
|
|
- Validation:
|
|
Cover cache reuse across all-provider and single-provider requests, forced
|
|
refresh, concurrent single-flight reads, absence of availability checks from
|
|
`Service.Create`, and a new-session activation timeout larger than 30 seconds.
|
|
- References:
|
|
[status_cache.go](../../../services/tuttid/service/agentstatus/status_cache.go)
|
|
[service.go](../../../services/tuttid/service/agent/service.go)
|
|
[pendingIntents.reducer.ts](../../../packages/agent/activity-core/src/engine/pendingIntents.reducer.ts)
|
|
[acp_shared.go](../../../packages/agent/daemon/runtime/acp_shared.go)
|
|
|
|
### Claude Code keeps returning ConnectionRefused after network recovery
|
|
|
|
- Symptom:
|
|
A Claude Code Turn loses network access while sending and eventually reports
|
|
`API Error: Unable to connect to API (ConnectionRefused)`. After the machine
|
|
network recovers, later sends in the same Session keep failing, while a newly
|
|
created Claude Session succeeds.
|
|
- Quick checks:
|
|
Correlate `agent_session.claude_sdk.lifecycle_event` by Agent Session and
|
|
provider Session id. An SDK `system/api_retry` event now records
|
|
`sdk_connection_error`, `sdk_retry_attempt`, `sdk_max_retries`, and
|
|
`sdk_retry_delay_ms`. A terminal result should carry
|
|
`sdk_result_is_error=true`; a numeric `sdk_api_error_status` instead points to
|
|
an HTTP/auth/provider response rather than a connection failure.
|
|
- Root cause:
|
|
The Claude Agent SDK owns one long-lived Query and Claude Code subprocess for
|
|
streaming multi-Turn input. A connection failure can terminate the Turn while
|
|
leaving that Query unsuitable for later requests. Reusing it preserves the
|
|
bad per-process state even though host and VM networking have recovered.
|
|
Older sidecars also treated every `result/subtype=success` as a completed
|
|
Turn, ignoring the SDK's independent `is_error` flag.
|
|
- Fix:
|
|
Preserve the failed Turn and its provider error. When the SDK terminal result
|
|
explicitly reports a connection failure, revoke and close that Query. The
|
|
next user send creates a fresh Query with `resume` set to the same provider
|
|
Session id. Do not automatically resend the failed prompt because delivery
|
|
may be ambiguous. Do not recycle the Query for a transient retry that
|
|
eventually succeeds or for a terminal HTTP/authentication error.
|
|
This SDK-owned `api_retry` behavior is separate from AgentGUI presentation.
|
|
AgentGUI does not expose manual activation retry for a failed or canceled
|
|
Session. If authentication rejects the first submit, Host preserves the
|
|
failed history and discards that startup runtime; the auth notice offers
|
|
login, after which the user starts a new conversation.
|
|
Runtime discard keeps cleanup cancellation-independent but re-applies the
|
|
caller's deadline. If the Claude sidecar does not acknowledge close in time,
|
|
the adapter forcibly unregisters and closes the connection so a rejected
|
|
startup cannot remain live in the runtime registry.
|
|
- Validation:
|
|
Simulate `api_retry/error_status=null` followed by
|
|
`result/is_error=true/api_error_status=null`, then send a second Turn. Require
|
|
two Query generations, closure of the first, and `resume` on the second.
|
|
Also prove that retry-then-success and HTTP 401 failures retain the existing
|
|
Query. For rejected startup cleanup, block the sidecar close response and
|
|
require teardown at the caller deadline with no retained runtime Session.
|
|
- References:
|
|
[messageRouter.ts](../../../packages/agent/claude-sdk-sidecar/src/messageRouter.ts)
|
|
[sessionRuntime.ts](../../../packages/agent/claude-sdk-sidecar/src/sessionRuntime.ts)
|
|
[sessionRuntime.recovery.test.ts](../../../packages/agent/claude-sdk-sidecar/src/sessionRuntime.recovery.test.ts)
|
|
|
|
### Cursor auto-continue invents interrupted work after a network drop
|
|
|
|
- Symptom:
|
|
After a Cursor `RetriableError` / TLS drop and Tutti's automatic
|
|
`transport_retry`, the agent does not answer the user's last message
|
|
(for example a simple greeting). Instead it talks about recovering prior
|
|
context, reading transcripts, or continuing an interrupted task that never
|
|
started.
|
|
- Quick checks:
|
|
In the session transcript, confirm the failed attempt produced only the
|
|
`Error: RetriableError:` / `Error: ConnectError:` tail (no useful assistant
|
|
text and no tool calls) before the retry notice. Check
|
|
`agent_session.acp.exec.auto_continue` in `tuttid` logs for
|
|
`has_useful_progress=false`.
|
|
- Root cause:
|
|
Cursor keeps conversation history on its backend; Tutti can mainly control the
|
|
synthetic auto-continue `session/prompt`. Mid-task wording
|
|
("Continue exactly where you left off") misleads the model when the attempt
|
|
died before any useful output.
|
|
- Fix:
|
|
Branch the auto-continue prompt by useful progress: zero-progress retries ask
|
|
the model to answer the user's most recent message normally and not invent
|
|
interrupted work; mid-task retries keep the continue wording. Progress is
|
|
assistant text after stripping the retriable error tail, or any observed tool
|
|
call.
|
|
- Validation:
|
|
`cd packages/agent/daemon && go test ./runtime/ -run
|
|
'TestACPAutoContinueHasUsefulProgress|TestACPAutoContinuePromptContentBranches|TestCursorAdapterAutoContinuesAfterRetriableTurnError|TestCursorAdapterAutoContinueMidTaskUsesContinuePrompt'`.
|
|
Live: send a short Cursor message that fails before any reply, confirm the
|
|
retry answers the user instead of recovering a phantom task, and that
|
|
mid-task drops still resume in place.
|
|
- References:
|
|
[acp_auto_continue.go](../../../packages/agent/daemon/runtime/acp_auto_continue.go)
|
|
[standard_acp_turn.go](../../../packages/agent/daemon/runtime/standard_acp_turn.go)
|
|
[acp_auto_continue_test.go](../../../packages/agent/daemon/runtime/acp_auto_continue_test.go)
|
|
|
|
### Canceling an old AgentGUI turn stops a newer turn
|
|
|
|
- Symptom:
|
|
A `cancelTurn(turnId)` request targets an older turn, but a newer turn in the
|
|
same session stops, or the requested turn is reported canceled even though
|
|
the runtime continued with a different active turn.
|
|
- Quick checks:
|
|
Compare `requested_turn_id` and `active_turn_id` in
|
|
`agent_session.cancel.turn_mismatch`. Also check whether a new `Exec` entered
|
|
between the exact-turn lookup and the provider cancel call.
|
|
- Root cause:
|
|
Session-level provider cancel APIs do not carry a turn id. Validating the id
|
|
before calling the adapter is insufficient unless validation and cancel are
|
|
protected by the same session lifecycle lock used to start turns.
|
|
- Fix:
|
|
Carry the requested turn id through HTTP service, runtime adapter, and
|
|
controller. Return an idempotent no-op on mismatch, and hold the per-session
|
|
lifecycle lock across the active-turn comparison and `adapter.Cancel` so a
|
|
new turn cannot enter the gap.
|
|
- Validation:
|
|
Cover both a mismatched active turn (the adapter must not be called) and a
|
|
blocking adapter cancel (a second lifecycle operation must remain blocked
|
|
until cancel returns).
|
|
- References:
|
|
[service_turns.go](../../services/tuttid/service/agent/service_turns.go)
|
|
[controller_cancel.go](../../packages/agent/daemon/runtime/controller_cancel.go)
|
|
[controller_test.go](../../packages/agent/daemon/runtime/controller_test.go)
|
|
|
|
### Historical Agent completions notify again when a workspace opens
|
|
|
|
- Symptom:
|
|
Opening a workspace produces completion or failure notifications for turns
|
|
that settled before the window was opened.
|
|
- Quick checks:
|
|
Compare notification-controller creation with the first agent activity
|
|
`load`. If the engine was empty at subscription time and the first populated
|
|
snapshot contains settled turns, verify those turns were treated as initial
|
|
hydration rather than live transitions.
|
|
- Root cause:
|
|
Taking an empty engine snapshot as the history baseline is insufficient when
|
|
durable loading happens asynchronously after subscribers are registered.
|
|
The first hydrated settled turn then looks indistinguishable from a newly
|
|
settled turn.
|
|
- Fix:
|
|
Keep outcome notifications behind an explicit hydration boundary. Record all
|
|
settled turns observed before the initial durable load resolves as baseline
|
|
history. After hydration, notify only the first observation of each
|
|
session-scoped turn key; a live non-settled turn can also establish readiness
|
|
when the initial load is unavailable.
|
|
- Validation:
|
|
Cover the real startup order: subscribe against an empty engine, hydrate a
|
|
historical settled turn without notifying, finish hydration, then verify a
|
|
later running-to-settled turn notifies exactly once.
|
|
- References:
|
|
[workspaceAgentOutcomeNotification.ts](../../apps/desktop/src/renderer/src/features/workspace-workbench/services/workspaceAgentOutcomeNotification.ts)
|
|
[workspaceAgentOutcomeNotification.test.ts](../../apps/desktop/src/renderer/src/features/workspace-workbench/services/workspaceAgentOutcomeNotification.test.ts)
|
|
|
|
### Agent GUI context usage is absent or has the wrong total
|
|
|
|
- Symptom:
|
|
The Agent GUI composer never shows context usage even though provider usage
|
|
logs are present. Alternatively, Claude Code GUI usage shows a 200k context
|
|
window for a model that should have 1M context, or a 200k model keeps showing
|
|
the prior 1M total after a model switch. After one Claude Code request runs
|
|
several tools, the used-token count may also jump from the latest iteration's
|
|
value to nearly the sum of every iteration in that turn. A related lifecycle
|
|
symptom is that Claude has rendered its final answer but Agent GUI remains in
|
|
the working state until a delayed context-usage control request returns.
|
|
- Quick checks:
|
|
Trace the provider update first: use
|
|
`agent_session.claude_sdk.usage_update` for Claude SDK and
|
|
`agent_session.acp.usage_update` for ACP providers. Then inspect the daemon
|
|
session response for its typed `usage.contextWindow` field and confirm the
|
|
desktop canonical session preserves it. If provider logs contain nonzero
|
|
used and total tokens but the API field is null, inspect the runtime-context
|
|
split into typed session metadata. If the API field is populated but the
|
|
footer is absent, inspect the desktop adapter and the active canonical
|
|
session passed to the composer capability projection. For Claude, if the
|
|
payload keys include
|
|
`modelUsage` but `raw_total_tokens` is `0`, the daemon did not parse the
|
|
model-usage context window. If `previous_context_model` and
|
|
`current_context_model` differ but `current_total_tokens` equals
|
|
`previous_total_tokens`, daemon usage normalization reused a stale context
|
|
window across models. For a tool-loop spike, compare the streamed
|
|
`normalized_used_tokens` values with the final Result update. If the final
|
|
value equals their sum, the cumulative Result usage was mistaken for current
|
|
context occupancy. For a stuck working indicator, compare the Claude SDK
|
|
`result` lifecycle timestamp with the later `context_window` usage update and
|
|
`turn_completed`; a long result-to-completion gap isolates telemetry on the
|
|
terminal path rather than unfinished provider work.
|
|
- Root cause:
|
|
Protocol v2 intentionally removed raw `runtimeContext` from the public
|
|
session model. If the refactor removes that legacy field without adding a
|
|
typed `usage` field across persistence, API generation, the desktop adapter,
|
|
and the canonical session, the provider still records correct telemetry but
|
|
Agent GUI has no public data to render. A wrong Claude total is a separate
|
|
normalization failure: Claude SDK result messages expose model usage as a map
|
|
keyed by model id, for example
|
|
`modelUsage["claude-sonnet-5"].contextWindow`. If either sidecar or daemon
|
|
only parses array-shaped `modelUsage`, the context-window total is missing and
|
|
daemon normalization falls back to 200k. Claude SDK Result `usage` is
|
|
cumulative across the model iterations in a client-side tool loop, whereas
|
|
each streamed usage update describes one iteration. The authoritative
|
|
`Query.getContextUsage()` snapshot must therefore win at Result time. Calling
|
|
that method after detaching it from the Query object loses its `this` binding;
|
|
if the best-effort error is swallowed, the cumulative fallback remains
|
|
visible as a false context spike. Conversely, awaiting a correctly bound
|
|
`getContextUsage()` call before emitting `turn_completed` makes optional
|
|
telemetry a lifecycle dependency and leaves the GUI working for the duration
|
|
of a slow control response.
|
|
- Fix:
|
|
Define usage in the protocol-v2 OpenAPI contract and carry it as typed durable
|
|
session metadata through the generated client, desktop adapter, canonical
|
|
activity session, and composer projection. Keep raw runtime context private
|
|
to provider recovery; do not restore a GUI runtime-context dependency.
|
|
Parse `modelUsage` recursively as both arrays and maps before using fallback
|
|
context-window values. Track the model associated with a cached context
|
|
window, and only reuse the previous total for the same model or when the model
|
|
is unknown. Do not hard-code alias-to-model mappings in Tutti. Invoke
|
|
`getContextUsage()` through its owning Query object after the Result has
|
|
terminalized the turn. Keep the authoritative snapshot and cumulative Result
|
|
fallback in an asynchronous telemetry path, and invalidate a delayed snapshot
|
|
when a newer snapshot request starts or a later root user prompt begins so
|
|
older usage cannot overwrite newer work.
|
|
- Validation:
|
|
Cover runtime-context splitting and metadata persistence, generated API
|
|
projection, desktop canonical-session adaptation, activity-core usage
|
|
resolution, and the composer hook.
|
|
Add sidecar and daemon coverage with map-shaped `modelUsage` carrying
|
|
`contextWindow: 1_000_000`, plus daemon coverage for Haiku -> Sonnet5 -> Haiku
|
|
usage updates where the last payload lacks `totalTokens`. Add a sidecar query
|
|
fixture whose `getContextUsage()` depends on `this`, and give its Result a
|
|
multi-iteration cumulative usage total; assert that only the authoritative
|
|
context snapshot is emitted. Also cover a context query that remains pending:
|
|
`turn_completed` must be emitted first, and a delayed snapshot must be dropped
|
|
after a newer snapshot request or the next root user prompt. Then run the
|
|
Claude SDK sidecar tests, daemon Go tests, AgentGUI tests, and typechecks.
|
|
- References:
|
|
[agent-activity-packages.md](../architecture/agent-activity-packages.md)
|
|
[session_metadata.go](../../packages/agent/store-sqlite/session_metadata.go)
|
|
[desktopAgentActivityAdapter.ts](../../apps/desktop/src/renderer/src/features/workspace-agent/services/desktopAgentActivityAdapter.ts)
|
|
[main.ts](../../packages/agent/claude-sdk-sidecar/src/main.ts)
|
|
[main.test.ts](../../packages/agent/claude-sdk-sidecar/src/main.test.ts)
|
|
[claude_sdk_adapter.go](../../packages/agent/daemon/runtime/claude_sdk_adapter.go)
|
|
[compaction.ts](../../../packages/agent/claude-sdk-sidecar/src/compaction.ts)
|
|
[messageRouter.ts](../../../packages/agent/claude-sdk-sidecar/src/messageRouter.ts)
|
|
[sessionRuntime.session.test.ts](../../../packages/agent/claude-sdk-sidecar/src/sessionRuntime.session.test.ts)
|
|
|
|
### Claude `/compact` finishes without a compaction divider
|
|
|
|
- Symptom:
|
|
After Claude Code `/compact`, AgentGUI shows only the turn duration footer
|
|
(for example `总用时 22 秒`) and never renders the
|
|
`Compacting context` / `Context compacted.` divider. Context usage may still
|
|
drop correctly.
|
|
- Quick checks:
|
|
Confirm the provider is Claude Code SDK. Inspect the Claude transcript under
|
|
`~/.claude/projects/.../<provider-session-id>.jsonl` for `compact_boundary`
|
|
or `<local-command-stdout>Compacted`. Check tuttíd for a
|
|
`claude-sdk:compact:<turnId>` system notice; if tokens fell but that notice is
|
|
missing, the sidecar never published `compact_started`.
|
|
- Root cause:
|
|
Claude Code 2.1.x often completes manual compaction without streaming
|
|
`status:compacting` or `compact_boundary` to the query iterator (the boundary
|
|
may exist only on disk, or arrive as `local_command` /
|
|
camelCase `compactMetadata`). Compaction banners were driven only by those
|
|
signals, so a silent success left no notice for AgentGUI to project. Even when
|
|
the sidecar emits `compact_started` immediately on `/compact`, that assistant
|
|
system notice arrives before provider-turn acceptance; the Claude acceptance
|
|
gate previously treated it as premature provider output and dropped it. The
|
|
daemon already settles an active compact notice when the turn closes; it needs
|
|
a held `compact_started` (or an adapter-emitted running notice) first. Flushing
|
|
those held events after acceptance must not re-observe their earlier provider
|
|
input units: that either regresses the Replay cursor or coalesces conflicting
|
|
`compaction.status` readiness onto the durable `turn.working` checkpoint.
|
|
- Fix:
|
|
Emit a running compact notice from the Claude adapter when `/compact` is
|
|
selected, allow compact system notices to precede provider-turn acceptance,
|
|
accept `local_command` / `local_command_output` and camelCase boundary
|
|
metadata in the sidecar. For the pinned Claude Code 2.1.220 contract, treat
|
|
`status.compact_result=failed` plus `compact_error` as the canonical compact
|
|
failure signal; assistant and local-command text are compatibility fallbacks
|
|
for streams that omit that status. Map the normalized
|
|
`conversation could not be reduced below the context limit` failure to
|
|
`compact_failed` before the successful SDK result settles the Turn. When the
|
|
acceptance barrier later flushes held events, strip their
|
|
`ProviderInputUnit` so they publish transcript/state only. If the compact
|
|
failure specifically reports that the hard context limit was exceeded,
|
|
project a typed `context_handoff_required` error. Do not replace the provider
|
|
session or automatically dispatch the next message. Tell the user to create
|
|
a new conversation and add an `agent-session` mention for this conversation,
|
|
making the handoff explicit while retaining Claude Code's normalized failure
|
|
detail. For restored Claude sessions, use `rawMaxTokens` as the fallback hard
|
|
window and log the SDK maximum, raw maximum, native
|
|
auto-compact threshold, and effective auto-compact flag separately.
|
|
- Validation:
|
|
Add daemon coverage that `/compact` banners stay held until durable
|
|
acceptance, then flush without provider-input units; add sidecar coverage for
|
|
silent `/compact` (result only), local_command failure, and camelCase
|
|
`compactMetadata`. Also cover exact overflow classification, raw hard-window
|
|
diagnostics, the typed handoff error projection, and localized Desktop and
|
|
Native guidance. Re-run L04-CLAUDE recording and confirm the progress divider
|
|
appears, then becomes `Context compacted.` (or the interrupted divider with
|
|
the failure detail), and that record+replay both pass.
|
|
- References:
|
|
[compaction.ts](../../../packages/agent/claude-sdk-sidecar/src/compaction.ts)
|
|
[claude_sdk_execution.go](../../../packages/agent/daemon/runtime/claude_sdk_execution.go)
|
|
[claude_sdk_turn.go](../../../packages/agent/daemon/runtime/claude_sdk_turn.go)
|
|
[AgentMessageBlock.tsx](../../../packages/agent/gui/shared/agentConversation/components/AgentMessageBlock.tsx)
|
|
|
|
### Inactive Claude resume times out then later sends stay queued
|
|
|
|
- Symptom:
|
|
On a large inactive Claude session, `/compact` or any send shows working then
|
|
fails after ~30s with `engine command timed out` / tuttíd `context canceled`.
|
|
Compact never runs. A later send shows 排队中 and never reaches tuttíd
|
|
(`api.send.received` missing).
|
|
- Quick checks:
|
|
Confirm `activeLiveState=inactive` and `resumable=true` before send. Order
|
|
desktop `renderer_adapter.send.http_requested` → tuttíd
|
|
`process_start.env_diagnostics` → `api.send.failed` (~30s, no
|
|
`runtime.exec`). Check Claude transcript size under
|
|
`~/.claude/projects/.../<provider-session-id>.jsonl`. After the timeout,
|
|
inspect whether a second submit stamps `queued:true` without a daemon send.
|
|
- Root cause:
|
|
`SendInput` blocks on `ensureRuntimeSession` / Claude `Resume`→`Start` until
|
|
the sidecar emits `session_started`. On restore, the sidecar previously
|
|
awaited `query.getContextUsage()` before that event; the SDK call has no
|
|
timeout and has hung for minutes. The Engine then treated the client send
|
|
timeout as `uncertainDelivery`, which blocks all queue drain and prevents
|
|
remove/send-now of the timed-out head, so later prompts stay visible as
|
|
排队中 forever even after reconcile finds no delivery proof.
|
|
- Fix:
|
|
Emit `session_started` before the restore usage snapshot, and refresh usage
|
|
in the background (same as turn completion). Keep queue send timeout at 90s
|
|
inside the 120s confirmation window as defense in depth. When an owned
|
|
`queue:reconcile:*` completes without exact turn proof, drop the timed-out
|
|
prompt (definitive non-delivery) or release uncertainty into a retryable
|
|
failed head so later queued work can drain.
|
|
- Validation:
|
|
Sidecar coverage that restore `start()` reaches `session_started` while
|
|
`getContextUsage` never resolves, then still emits `usage_updated` after
|
|
resolve. `promptQueue.reducer` coverage for owned reconcile without turn
|
|
proof. Manually: open a large inactive Claude session, send `/compact`,
|
|
confirm Resume completes without waiting on usage and later messages are not
|
|
stuck behind uncertain delivery.
|
|
- References:
|
|
[sessionRuntime.ts](../../../packages/agent/claude-sdk-sidecar/src/sessionRuntime.ts)
|
|
[compaction.ts](../../../packages/agent/claude-sdk-sidecar/src/compaction.ts)
|
|
[promptQueue.reducer.ts](../../../packages/agent/activity-core/src/engine/promptQueue.reducer.ts)
|
|
[promptQueue.ownedReconcile.ts](../../../packages/agent/activity-core/src/engine/promptQueue.ownedReconcile.ts)
|
|
[promptQueue.drainDecision.ts](../../../packages/agent/activity-core/src/engine/promptQueue.drainDecision.ts)
|
|
[lifecycle.go](../../../packages/agent/host/lifecycle.go)
|
|
[claude_sdk_lifecycle.go](../../../packages/agent/daemon/runtime/claude_sdk_lifecycle.go)
|
|
|
|
### AgentGUI compaction timer keeps running after compaction completed
|
|
|
|
- Symptom:
|
|
AgentGUI continues to show an increasing `Compacting context` duration after
|
|
the provider finished compaction. The durable compaction message and Turn are
|
|
already terminal, but the mounted renderer still projects the earlier
|
|
`noticeCommandStatus=running` snapshot.
|
|
- Quick checks:
|
|
Compare the message-list requests in desktop reconcile diagnostics with the
|
|
durable message versions. If the renderer pulled a running compaction at
|
|
version N, missed its terminal update at N+1, then next requested
|
|
`afterVersion` at a much higher value, the local cache advanced across a
|
|
version hole. Confirm that the terminal row uses the same `messageId`; a new
|
|
compaction row or a timer-specific state bug is a different failure.
|
|
- Root cause:
|
|
The realtime bridge treated the maximum version of materialized message rows
|
|
as a contiguous acknowledged change cursor. After an event-stream loss, it
|
|
applied a later inline message and advanced that maximum past the missed
|
|
terminal mutation. Every later `afterVersion` pull then started beyond the
|
|
mutation, so the authoritative completed snapshot could never repair the
|
|
cached running snapshot. The timer correctly kept rendering the stale running
|
|
lifecycle.
|
|
- Fix:
|
|
Before folding realtime messages inline, compare only their unseen versions
|
|
with the cached high-water boundary. If the first unseen version is not the
|
|
next cursor, do not apply any of that event's messages; retain the old cursor
|
|
and request an authoritative incremental reconcile. After a disconnected
|
|
event stream reconnects, also incrementally reconcile every session whose
|
|
messages are already cached; otherwise a missed final mutation has no later
|
|
event that can reveal its gap. Do not require the materialized cache itself to
|
|
contain every historical cursor value, because mutable message rows replace
|
|
older versions.
|
|
- Validation:
|
|
Cache a user message and a running compaction, omit the next terminal
|
|
compaction mutation, then deliver a later assistant message. Assert that the
|
|
later message is not applied inline, reconciliation requests from the
|
|
pre-gap cursor, and the stable compaction `messageId` becomes completed from
|
|
the authoritative response. Also cover valid snapshot gaps already present in
|
|
the cache plus duplicate and stale event delivery. Finally, omit the terminal
|
|
mutation as the last event, disconnect and reconnect without another activity
|
|
event, and verify the reconnect reconcile retrieves it from the pre-disconnect
|
|
cursor.
|
|
- References:
|
|
[workspaceAgentActivityReconcileBridge.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/workspaceAgentActivityReconcileBridge.ts)
|
|
[sessionReconcileExecutor.ts](../../../packages/agent/activity-core/src/sessionReconcileExecutor.ts)
|
|
[agent-gui-node.md](../../architecture/agent-gui-node.md)
|
|
|
|
### Root detail reconciliation repeatedly reloads unchanged child transcripts
|
|
|
|
- Symptom:
|
|
Opening or reconciling one root conversation repeatedly issues message-list
|
|
requests for every known child Session, including assistant-only and
|
|
tool-only children whose durable messages have not changed.
|
|
- Quick checks:
|
|
Compare each child Session's required `messageVersion` from both root-detail
|
|
reads with the largest cached message version that also has a durable
|
|
`sequence`. If the cache is current but the request still starts at zero,
|
|
inspect whether child reconciliation reused the root conversation's
|
|
user-message boundary heuristic.
|
|
- Root cause:
|
|
The root heuristic intentionally returns zero when cached history has no user
|
|
message, so it can repair an incomplete root conversation. Applying that
|
|
heuristic to provider-native child Sessions makes ordinary assistant/tool
|
|
histories look permanently unhydrated and forces the same reads forever.
|
|
- Fix:
|
|
Keep root and child cursor policies separate. For a child, derive the cursor
|
|
only from durable sequenced messages and skip its message request when that
|
|
cursor has reached the Session `messageVersion`. Preserve the bounded
|
|
newest-first initial read, but treat an existing empty child window as the
|
|
authoritative zero cursor and drain later messages from `afterVersion=0`.
|
|
After the first message pass, read root detail again and incrementally fetch
|
|
newly discovered children plus existing children whose `messageVersion`
|
|
advanced during the pass. Do not let optimistic/transient rows advance the
|
|
durable cursor, and do not add polling; later changes arrive through the
|
|
existing push-and-reconcile path.
|
|
- Validation:
|
|
Cover assistant-only and tool-only child caches, a transient row with a higher
|
|
synthetic version, an unchanged child that performs no request, initial
|
|
newest-first hydration, an empty known child that gains more than one page,
|
|
and a child advancing between the two detail reads. Keep a root assistant-only
|
|
case proving its existing repair still reads from zero.
|
|
- References:
|
|
[workspaceAgentActivityReconcileBridge.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/workspaceAgentActivityReconcileBridge.ts)
|
|
[sessionReconcileExecutor.ts](../../../packages/agent/activity-core/src/sessionReconcileExecutor.ts)
|
|
|
|
### Completed agent output appears only after switching Sessions
|
|
|
|
- Symptom:
|
|
A Turn finishes in the daemon, but the active AgentGUI timeline does not show
|
|
the final assistant output until the user selects another Session and returns.
|
|
This is especially visible after an edit-retry replacement Turn.
|
|
- Quick checks:
|
|
Follow one exact `(workspaceId, agentSessionId, turnId)` through durable
|
|
`turn_update` publication, the business-event WebSocket, and
|
|
`WorkspaceAgentActivityReconcileBridge`. Distinguish a daemon publication
|
|
gap from a disconnected/stale subscription and from a renderer event that
|
|
updated Turn state without requesting authoritative messages. Compare the
|
|
cached message cursor with `listWorkspaceAgentSessionMessages`.
|
|
- Root cause:
|
|
A terminal Turn event can be delivered while its final message event is
|
|
missed, reordered, or not folded into the active cache. Session navigation
|
|
performs a detail/message hydration and therefore hides the missing
|
|
reconcile trigger.
|
|
- Fix:
|
|
Treat realtime events as hints. Every terminal `turn_update` requests one
|
|
combined state-and-message reconcile through the existing workspace engine.
|
|
An accepted edit-retry completion or recovery command remains `reconciling`
|
|
and blocks another edit until the same authoritative detail reconciliation
|
|
confirms its revision and recovery state. Never replace canonical
|
|
availability with recovery actions inferred by Desktop. Do not add a second
|
|
WebSocket, component store, or edit-retry-specific polling loop.
|
|
- Validation:
|
|
Deliver a settled `turn_update` without the corresponding final
|
|
`message_update`; assert that the bridge fetches messages and updates the
|
|
mounted timeline without Session navigation. Repeat after edit retry and
|
|
after a transient reconcile failure.
|
|
- References:
|
|
[workspaceAgentActivityReconcileBridge.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/workspaceAgentActivityReconcileBridge.ts)
|
|
[workspaceAgentEditRetry.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/workspaceAgentEditRetry.ts)
|
|
[agent-gui-node.md](../../architecture/agent-gui-node.md)
|
|
|
|
### AgentActivity replication repeatedly rejects message batches as invalid
|
|
|
|
- Symptom:
|
|
A downstream AgentActivity replica repeatedly returns `INVALID_ARGUMENT` for
|
|
the same message batch. The source session has a higher `messageVersion`, but
|
|
the destination has no messages or stops at an earlier version.
|
|
- Quick checks:
|
|
Compare the session watermark with the current message rows. Values such as
|
|
`1,3` or `1,5` are valid when an intermediate snapshot of the same
|
|
`messageId` was overwritten. Check whether the destination requires
|
|
`incomingVersion == maxStoredVersion + 1` or treats a message version as
|
|
immutable identity.
|
|
- Root cause:
|
|
`Message.Version` is a per-session change cursor on a mutable snapshot. Each
|
|
accepted update advances the cursor, and updating the same `messageId`
|
|
replaces its prior row. Current rows therefore need not contain every cursor
|
|
value, and the same message identity legitimately moves to a higher version.
|
|
- Fix:
|
|
Replicas must accept any positive version for a new message, accept a higher
|
|
version for an existing `messageId`, and ignore or reject only stale lower
|
|
versions. Use a version-guarded atomic upsert so concurrent stale snapshots
|
|
cannot overwrite newer state. Do not add an event-history table merely to
|
|
make the current snapshot appear contiguous.
|
|
- Validation:
|
|
Record message A at v1, message B at v2, then update B to v3. Verify the
|
|
current snapshot is `A@1,B@3`, `afterVersion=1` returns B at v3, and a
|
|
rejected projection does not consume another cursor. Downstream replication
|
|
coverage should also accept an initial v3, update it to v5, and preserve v5
|
|
when v4 arrives later.
|
|
- References:
|
|
[activity_messages.go](../../../packages/agent/store-sqlite/activity_messages.go)
|
|
[activity_message_read.go](../../../packages/agent/store-sqlite/activity_message_read.go)
|
|
[repository.go](../../../packages/agent/store-sqlite/repository.go)
|
|
|
|
### Goal clear stays planning and leaves the session running
|
|
|
|
- Symptom:
|
|
Immediately clearing a newly set Goal leaves the first response in thinking,
|
|
`/goal clear` in planning, or the conversation permanently running.
|
|
- Quick checks:
|
|
Inspect the Goal control response: its Turn ID must be empty. List persisted
|
|
Turns and verify no Turn was created solely for the clear action. Inspect the
|
|
Goal state endpoint for `desired`, `observed`, `revision`, `syncStatus`, and
|
|
`pendingOperationId`. For any real Goal Turn, verify `origin` and source Goal
|
|
operation/revision are present.
|
|
- Root cause:
|
|
The prompt path allocated a Turn ID before classifying `/goal`. Provider Goal
|
|
control is session-level and did not start that Turn, but message persistence
|
|
manufactured a row for the unknown ID. With no real `turn_started` or
|
|
terminal event, loading and session-running projections never settled.
|
|
- Fix:
|
|
Route Goal controls through the typed Goal API before Turn allocation. Persist
|
|
desired/observed Goal state and an independent operation, reject messages that
|
|
reference unknown Turns, and adopt only provider-started continuation Turns.
|
|
Use durable origin and revision correlation so clear does not cancel unrelated
|
|
user work and stale continuation timers cannot revive an older Goal.
|
|
- Validation:
|
|
Set then immediately clear a Goal and assert that no control Turn exists, the
|
|
operation reaches a terminal state, and the session has no phantom active
|
|
Turn. Cover unknown-Turn message rejection, Claude goal-arm quiescing, Codex
|
|
adopted continuation provenance, stale observation protection, and revision-
|
|
guarded continuation nudges.
|
|
- References:
|
|
[Agent Goal Control Design](../../specs/2026-07-15-agent-goal-control-design.md)
|
|
[controller_exec.go](../../../packages/agent/daemon/runtime/controller_exec.go)
|
|
[goal_state.go](../../../packages/agent/store-sqlite/goal_state.go)
|
|
|
|
### Claude Goal completes but the UI remains active
|
|
|
|
- Symptom:
|
|
Claude finishes the requested work and its session JSONL contains an
|
|
`attachment.type=goal_status` row with `met=true`, but Tutti emits no
|
|
`goal_observed`; durable desired and observed Goal state remain active.
|
|
- Quick checks:
|
|
Correlate the Tutti Session with the Claude provider Session ID. Inspect the
|
|
provider transcript for the final `goal_status` attachment, then check dev
|
|
logs for a matching sidecar `goal_observed`. Do not infer Goal completion
|
|
from a successful root result or from a successful Stop hook process.
|
|
- Root cause:
|
|
Local Claude Agent SDK `SDKMessage` streams through `0.3.222` omit transcript
|
|
attachments. `SDKActiveGoalMessage` is also absent from the public
|
|
`SDKMessage` union, and local Claude Code only publishes `active_goal` on its
|
|
remote path. A test that manually yielded `type=attachment` therefore proved
|
|
the projection but not the real SDK boundary.
|
|
- Fix:
|
|
Locate the provider transcript from root `system/init.session_id + cwd`
|
|
using the pinned SDK's project-key algorithm. Start restored Sessions from
|
|
their already known provider Session ID. Keep
|
|
`SessionStart.transcript_path` as an exact-path auxiliary signal, not the
|
|
required trigger. Read only newly appended complete JSONL rows, forward only
|
|
`goal_status` attachments through the existing Goal projection, drain before
|
|
root Turn completion, and keep following after the SDK result because the
|
|
native evaluator can flush its terminal row slightly later. Late evidence
|
|
retains the latest settled root Turn identity. Do not parse evaluator hook
|
|
stdout and do not configure an alpha `SessionStore` solely for this signal;
|
|
both change a larger and less reliable boundary.
|
|
- Validation:
|
|
Run a query fixture whose SDK iterator emits root `system/init`, the root
|
|
user message, and result. Reproduce the observed ordering where transcript
|
|
`met=false` precedes `turn_completed` and `met=true` lands afterward; require
|
|
the late completion to retain the original Turn identity. Also cover resume
|
|
without another init, delegated init exclusion, watcher cleanup,
|
|
historical-row skipping, partial JSONL rows, and repeated-drain
|
|
deduplication.
|
|
- References:
|
|
[goalTranscript.ts](../../../packages/agent/claude-sdk-sidecar/src/goalTranscript.ts)
|
|
[messageRouter.ts](../../../packages/agent/claude-sdk-sidecar/src/messageRouter.ts)
|
|
[Anthropic SDK issue #336](https://github.com/anthropics/claude-agent-sdk-typescript/issues/336)
|
|
|
|
### Accepted Goal clear is sent repeatedly until convergence times out
|
|
|
|
- Symptom:
|
|
One `/goal clear` produces repeated provider control requests at the Goal
|
|
worker interval. Each request is accepted, but the durable operation remains
|
|
`dispatched` with provider phase `accepted`, its attempt count keeps rising,
|
|
and it eventually fails with `accepted goal operation exceeded its
|
|
convergence deadline`.
|
|
- Quick checks:
|
|
Correlate logs by `agent_session_id`, operation ID, and Goal revision. Verify
|
|
there is one user control audit but multiple provider `action=clear` calls.
|
|
Inspect `workspace_agent_goal_control_operations`: an accepted operation with
|
|
increasing attempts and unchanged accepted evidence identifies Host replay,
|
|
not repeated user input.
|
|
- Root cause:
|
|
The steady-state Goal worker treated clear's provider idempotence as
|
|
permission to resubmit an already accepted command. Acceptance had already
|
|
crossed the delivery boundary, so every later worker claim queued another
|
|
native clear command while the Host was supposed to wait for applied
|
|
lifecycle evidence.
|
|
- Fix:
|
|
For every accepted Goal mutation, make the steady-state worker defer without
|
|
calling the provider again. Keep the convergence deadline so missing applied
|
|
evidence still terminates. Reserve replay for startup crash recovery and
|
|
gate it with the adapter recovery policy; query-incapable adapters may replay
|
|
an idempotent clear once, while unsafe set replay remains rejected.
|
|
- Validation:
|
|
Return `accepted` from a clear, advance the worker clock past its next claim,
|
|
and step the steady-state worker. The provider call count must remain one,
|
|
while the operation stays pending and `applying`. Existing deadline coverage
|
|
must still fail a lost-evidence operation without another provider call.
|
|
- References:
|
|
[goal_operation_worker.go](../../../packages/agent/host/goal_operation_worker.go)
|
|
[goal_scenarios.go](../../../packages/agent/host/conformance/goal_scenarios.go)
|
|
|
|
### Replaced Goal banner keeps the previous objective
|
|
|
|
- Symptom:
|
|
A second `/goal <objective>` is accepted and runs, but AgentGUI continues to
|
|
show the first objective. The Goal state table contains the second objective
|
|
at a newer revision while `workspace_agent_sessions.session_metadata_json`
|
|
or a runtime Session snapshot still contains the first.
|
|
- Quick checks:
|
|
Compare `workspace_agent_session_goals.desired_json`, `revision`, and
|
|
`updated_at_unix_ms` with the Session metadata Goal. Confirm that the second
|
|
Goal operation completed before attributing the mismatch to React rendering.
|
|
- Root cause:
|
|
Durable Goal state and provider Session metadata update on different
|
|
schedules. Session reads previously exposed the provider metadata Goal and
|
|
attached only `goalSyncState`, so a later Session reload could overwrite the
|
|
correct Goal Control response with an older objective.
|
|
- Fix:
|
|
Project Host-owned durable Goal state and its update timestamp onto every
|
|
single and batch Session read. Use `desired` while convergence is unresolved,
|
|
use `observed` after synchronization, and honor the durable tombstone.
|
|
- Validation:
|
|
Read a Session whose metadata contains objective A beside durable Goal
|
|
revision N+1 containing objective B. Single and batch projections must return
|
|
B with a Session timestamp at least as new as the Goal state. A durable
|
|
tombstone must return no Session Goal; a synchronized terminal observation
|
|
must remain terminal instead of reverting to active `desired` state.
|
|
- References:
|
|
[service_turns.go](../../../services/tuttid/service/agent/service_turns.go)
|
|
[goal_state.go](../../../packages/agent/store-sqlite/goal_state.go)
|
|
|
|
### Cleared Goal reappears as a newer provider-authored Goal
|
|
|
|
- Symptom:
|
|
Goal clear returns success and briefly shows “目标已移除”, but the active Goal
|
|
banner returns. The Goal table contains a completed clear followed by a new
|
|
provider-adoption `set` revision for the same objective. In a projection-only
|
|
variant, the table remains tombstoned with no later `set`, while the banner
|
|
still shows the pre-clear Goal.
|
|
- Quick checks:
|
|
Correlate `workspace_agent_session.goal_control.completed action=clear` with
|
|
`agent_session.app_server.goal.provider_adopted`. If adoption was scheduled
|
|
before clear but completed immediately after it, inspect the adoption's
|
|
expected revision and the canonical revision committed by clear.
|
|
If no later adoption exists, inspect the Goal Control response: `goal` must
|
|
be present as `null`, and its embedded `session.goal` must also be null.
|
|
- Root cause:
|
|
Provider Goal adoption runs off the app-server read loop. An observation
|
|
captured before clear could wait behind the serialized Goal actor; the old
|
|
implementation read the revision only after acquiring that actor, so the
|
|
delayed observation was mistaken for a new provider-authored Goal and
|
|
cleared the tombstone.
|
|
The projection-only variant made nullable `goal` optional on the wire. Go
|
|
therefore omitted a successful clear, the client fell back to a stale
|
|
runtime `session.goal`, and the Engine promoted that stale field after the
|
|
operation settled.
|
|
- Fix:
|
|
Capture the canonical Goal revision while scheduling provider adoption and
|
|
carry it through the runtime, Host, and store boundary. The store compares
|
|
it with the current revision inside the same transaction that would advance
|
|
the Goal. Mutable progress snapshots from an already owned provider Goal
|
|
bind to that Goal's current operation identity instead of entering adoption.
|
|
Goal Control responses require a nullable `goal`, project the Host result
|
|
onto `session.goal`, and let the Engine normalize a synced Session from that
|
|
same authoritative field.
|
|
- Validation:
|
|
Block provider adoption after it captures revision N, complete clear at
|
|
revision N+1, then release adoption. It must fail as superseded, leave the
|
|
Goal tombstoned at N+1, create no later `set` operation, and keep the runtime
|
|
Goal banner empty. Also return a synced clear beside a deliberately stale
|
|
Session Goal; JSON must contain `"goal": null`, and the Engine presentation
|
|
must remain empty.
|
|
- References:
|
|
[goal_provider_adoption.go](../../../packages/agent/host/goal_provider_adoption.go)
|
|
[codex_appserver_provider_goal_adoption.go](../../../packages/agent/daemon/runtime/codex_appserver_provider_goal_adoption.go)
|
|
[daemon_agent_sessions_goal.go](../../../services/tuttid/api/daemon_agent_sessions_goal.go)
|
|
[sessionGoalControl.validation.ts](../../../packages/agent/activity-core/src/engine/sessionGoalControl.validation.ts)
|
|
|
|
### Goal loops after pause/resume and never reaches complete
|
|
|
|
- Symptom:
|
|
Pause and resume succeed, the Goal keeps producing continuation Turns, and the
|
|
model may even emit the final marker text, but `thread/goal/updated` never
|
|
reaches `status=complete`. Logs show a `continuation_nudge` after a brief
|
|
inter-turn idle, then an unbounded stream of adopted Goal Turns.
|
|
- Quick checks:
|
|
Compare the nudge `thread/goal/set` params with the original set. If the nudge
|
|
re-sends `objective`, Codex may restart Goal work instead of letting the
|
|
current generation settle. Confirm pause/resume ops completed and the durable
|
|
generation fingerprint is still owned by the set/adoption operation.
|
|
- Root cause:
|
|
Pause/resume intentionally keep generation ownership on the set-time
|
|
fingerprint while advancing the live operation identity. Provider
|
|
`status=complete` for that fingerprint was classified as superseded
|
|
(`binding.identity != current`), so local goal state stayed `active`. The
|
|
per-turn continuation nudge then re-sent `thread/goal/set` with
|
|
`status=active` and revived the finished Goal.
|
|
- Fix:
|
|
Treat provider progress for the session's current generation fingerprint as
|
|
known-current after pause/resume (while the Goal is still present). Keep
|
|
nudges status-only, do not re-bind generation on pause/resume/nudge, and do
|
|
not schedule a delayed nudge from Resume itself.
|
|
- Validation:
|
|
Record a short one-digit-per-turn Goal, pause after the first digit, resume,
|
|
and verify the provider reaches `complete` with the final marker and no
|
|
post-complete continuation storm.
|
|
- References:
|
|
[codex_appserver_goal.go](../../../packages/agent/daemon/runtime/codex_appserver_goal.go)
|
|
[codex_appserver_adapter_goal_lifecycle_test.go](../../../packages/agent/daemon/runtime/codex_appserver_adapter_goal_lifecycle_test.go)
|
|
|
|
### Goal resume fails with permanently ambiguous generation fingerprint
|
|
|
|
- Symptom:
|
|
Pause succeeds and the banner shows Resume, but clicking Resume fails. Logs
|
|
show `persist goal provenance: provider goal generation fingerprint is
|
|
permanently ambiguous`. Provider status may already be `active` while the
|
|
Host operation stays failed and no continuation Turn starts.
|
|
- Quick checks:
|
|
Correlate the pause and resume durable operation IDs with
|
|
`workspace_agent_goal_generation_fences` for the same fingerprint. Confirm the
|
|
set/adoption that created the Goal already owns that fingerprint. Inspect
|
|
Codex `thread/goal/set` pause/resume responses for a returned Goal payload.
|
|
- Root cause:
|
|
Pause and resume are status-only controls that reuse the set-time provider
|
|
generation. Re-binding that fingerprint to the pause/resume operation id
|
|
collides with the set binding and marks the fingerprint permanently
|
|
ambiguous, failing closed before the local status mirror and continuation
|
|
nudge run.
|
|
- Fix:
|
|
Keep generation binding on durable set/adoption only. For pause/resume, mirror
|
|
the provider or local Goal status without calling `bindGoalGeneration`.
|
|
- Validation:
|
|
With a durable provenance sink, set → pause → resume with distinct operation
|
|
ids and the same generation fingerprint. Resume must succeed, status must be
|
|
`active`, and the fingerprint must remain owned by the set operation.
|
|
- References:
|
|
[codex_appserver_goal.go](../../../packages/agent/daemon/runtime/codex_appserver_goal.go)
|
|
[codex_appserver_goal_provenance.go](../../../packages/agent/daemon/runtime/codex_appserver_goal_provenance.go)
|
|
[codex_appserver_adapter_goal_recovery_test.go](../../../packages/agent/daemon/runtime/codex_appserver_adapter_goal_recovery_test.go)
|
|
|
|
### Goal disappears after pause or resume
|
|
|
|
- Symptom:
|
|
Pause or resume succeeds, but the Goal banner disappears while the durable
|
|
Goal still exists.
|
|
- Quick checks:
|
|
Compare the Goal Control response's top-level `goal` with `state.desired`,
|
|
`state.observed`, and `state.tombstoned`. If `observed` is empty but
|
|
`desired` remains populated and not tombstoned, the provider supplied no
|
|
current observation; it did not clear the Goal.
|
|
- Root cause:
|
|
The response projected the provider's per-action observation as the visible
|
|
Goal. Some providers can apply pause or resume while returning no Goal
|
|
observation, so the required nullable response serialized that absence as an
|
|
explicit clear.
|
|
- Fix:
|
|
Host returns the durable desired projection as `GoalControlResult.Goal` and
|
|
keeps provider output in `GoalState.Observed`. The daemon only maps that
|
|
Host-owned result. Empty observation may produce `diverged` state, but only a
|
|
durable tombstone produces `goal: null`.
|
|
- Validation:
|
|
Set a Goal, make the provider return no Goal for pause and resume, and verify
|
|
both responses retain the objective with the expected paused/active status,
|
|
report divergence, and carry no pending operation.
|
|
- References:
|
|
[goal_control.go](../../../packages/agent/host/goal_control.go)
|
|
[goal_scenarios.go](../../../packages/agent/host/conformance/goal_scenarios.go)
|
|
[daemon_agent_sessions_goal.go](../../../services/tuttid/api/daemon_agent_sessions_goal.go)
|
|
|
|
### Revoked shared Goal starts again after handoff or desktop restart
|
|
|
|
- Symptom:
|
|
After a shared binding is revoked, the old Goal starts or resumes provider
|
|
work, may replace the Owner's newer Goal, or an unrelated active Turn is
|
|
canceled.
|
|
- Quick checks:
|
|
Resolve the original Goal operation by its stable `clientSubmitId`. Inspect
|
|
`workspace_agent_goal_generation_fences` for the exact operation ID, revision,
|
|
and repair epoch. `pending` or `processing` means Host still owns delivery;
|
|
absence means the revocation was never durably accepted. For any canceled
|
|
Turn, compare all three immutable `source_goal_*` fields with the fence.
|
|
- Root cause:
|
|
Revocation was treated as a one-shot notification, or code guessed that the
|
|
session's current active Turn belonged to the revoked binding. A failed
|
|
notification was lost on restart, and recovery could replay the old prepared
|
|
Goal before runtime learned the revocation.
|
|
- Fix:
|
|
Persist an exact Goal-generation fence before provider delivery. Process
|
|
fences before Goal operations during startup and each worker tick. Restore
|
|
all durable fences when a provider session resumes, and retain the exact set
|
|
in the runtime Controller across idle connection release so a replacement
|
|
adapter is fenced before its first user operation. Never resume an offline
|
|
provider solely for background fence or cancel delivery. Prepare the local
|
|
clear with the target revision as a compare-and-swap guard, and call
|
|
`CancelTurn` only for a live canonical Turn whose operation ID, revision,
|
|
and repair epoch all match. Keep the fence pending after cancellation
|
|
acceptance until the Turn is canonically terminal.
|
|
- Validation:
|
|
Cover failed delivery followed by restart, a pending target operation and
|
|
fence recovered together, a newer Owner Goal, repeated runtime ensure, and
|
|
matching versus unrelated active Turns. Simulate connection replacement and
|
|
prove fence reinstallation happens before provider dispatch. Simulate a
|
|
disconnect immediately before cancel and prove background recovery makes
|
|
zero provider Resume calls. Provider tests must prove that the exact
|
|
Codex/Claude generation never becomes canonical while later generations
|
|
remain admissible.
|
|
- References:
|
|
[Agent Host contracts](../../../packages/agent/host/README.md)
|
|
[Agent Goal Control Design](../../specs/2026-07-15-agent-goal-control-design.md)
|
|
[goal_generation_fence.go](../../../packages/agent/host/goal_generation_fence.go)
|
|
|
|
### Initial Goal prompt disappears when the Agent starts responding
|
|
|
|
- Symptom:
|
|
A new conversation opened with `/goal <objective>` first shows the submitted
|
|
command, then removes it as soon as the durable session or the first provider
|
|
response arrives. The response and processing state remain visible.
|
|
- Quick checks:
|
|
Compare the optimistic activation message with the durable
|
|
`session_audit`. Verify both carry the same `clientSubmitId` and inspect the
|
|
transcript projection after the overlay removes the optimistic twin. If the
|
|
durable Goal audit is filtered from all presentation output, the replacement
|
|
leaves no visible row.
|
|
- Root cause:
|
|
The optimistic activation was projected as an ordinary user message while
|
|
the canonical Goal control was correctly persisted as a session-level audit.
|
|
Overlay reconciliation removed the optimistic message by `clientSubmitId`,
|
|
then the message projector discarded the durable audit, so two individually
|
|
correct policies composed into a disappearing row.
|
|
- Fix:
|
|
Mark the optimistic activation as Goal control when
|
|
`initialGoalControl` is present. Project both optimistic and durable forms as
|
|
a dedicated `goal-control` row outside the Turn model. Derive the row identity
|
|
from the audit payload's client-submit message identity before falling back
|
|
to the durable operation ID, so replacement preserves the renderer key.
|
|
- Validation:
|
|
Project the optimistic-only state and the durable-after-overlay state. Both
|
|
must contain one `goal-control` row with the same ID and zero Turns. Also
|
|
verify the dedicated row renders, the first real provider Turn remains the
|
|
sole Turn, and reload produces the same control row.
|
|
- References:
|
|
[workspaceAgentMessageOverlay.ts](../../../packages/agent/gui/shared/workspaceAgentMessageOverlay.ts)
|
|
[workspaceAgentMessageProjection.ts](../../../packages/agent/gui/shared/agentConversation/projection/workspaceAgentMessageProjection.ts)
|
|
[workspaceAgentTimelineCanonical.ts](../../../packages/agent/gui/shared/workspaceAgentTimelineCanonical.ts)
|
|
|
|
### Goal clear duplicates the live Agent work section
|
|
|
|
- Symptom:
|
|
While a Goal Turn is still streaming, clicking clear inserts `/goal clear`
|
|
between two identical Agent work sections. Both sections continue updating
|
|
together even though the daemon executed only one Turn.
|
|
- Quick checks:
|
|
Inspect canonical Turns and message ownership. If Agent messages on both
|
|
sides of the turnless Goal audit carry the same Turn ID, compare transcript
|
|
group keys before inspecting provider retries or duplicate persistence.
|
|
- Root cause:
|
|
Chronological insertion produced `Turn T -> goal-control -> Turn T`. A
|
|
grouping pass treated the turnless row as a hard boundary and created two
|
|
groups with key `T`; the work-section map retained the later model for that
|
|
shared key, so both Turn positions rendered the same rows.
|
|
- Fix:
|
|
Keep a turnless session row inside the current presentation group only when
|
|
the next lifecycle-owned row proves that the surrounding Turn ID is
|
|
unchanged. Preserve the session row's null Turn ID; presentation continuity
|
|
must not manufacture lifecycle ownership.
|
|
- Validation:
|
|
Cover `T -> goal-control -> T` and assert one Turn group, one processed-time
|
|
section, one Goal control row, and one copy of each Agent row. Also cover
|
|
`T1 -> goal-control -> T2` and verify the Goal control remains an independent
|
|
orphan between different Turns.
|
|
- References:
|
|
[agentTranscriptModel.ts](../../../packages/agent/gui/shared/agentConversation/components/agentTranscriptModel.ts)
|
|
[AgentTranscriptView.tsx](../../../packages/agent/gui/shared/agentConversation/components/AgentTranscriptView.tsx)
|
|
|
|
### Recording fails when a tool message contains its runtime CWD
|
|
|
|
- Symptom:
|
|
Recording finalization fails with `expected_state_export_failed` and reports
|
|
an absolute path at `$.agent.sessions[*].messages[*].payload.input.cwd`.
|
|
The provider tape may already be complete.
|
|
- Quick checks:
|
|
Inspect the failed Recording row and the matching canonical tool message.
|
|
Distinguish the normalized tool envelope's direct `input.cwd` from a
|
|
tool-owned nested argument.
|
|
- Root cause:
|
|
The provider tape tokenizes the Session CWD, but the semantic-state adapter
|
|
previously copied the canonical tool message unchanged. The direct
|
|
`input.cwd` is provider runtime context, not part of Tutti's portable
|
|
semantic state.
|
|
- Fix:
|
|
Project the Agent graph before semantic-state validation. Remove only the
|
|
runtime `input.cwd` fields from normalized `tool_call` messages and
|
|
Interaction envelopes, including the normalized
|
|
`toolCall.input.cwd`. Convert an absolute executable prefix in normalized
|
|
command/title display fields to its basename. Do not branch on tool name,
|
|
recursively remove tool-owned arguments, mutate canonical messages, or
|
|
weaken the absolute-path validator.
|
|
- Validation:
|
|
Capture a tool message and matching Interaction with absolute normalized
|
|
runtime CWDs and executable prefixes, plus a nested relative
|
|
`arguments.cwd`. Require capture and validation to succeed, retain the
|
|
tool-owned nested argument, and prove the source Agent graph remains
|
|
unchanged.
|
|
- References:
|
|
[state.go](../../../services/tuttid/biz/agentsessionreplay/state.go)
|
|
[agent_session_replay_state.go](../../../services/tuttid/data/workspace/agent_session_replay_state.go)
|
|
|
|
### Cassette replay loses the provider session before the second stimulus
|
|
|
|
- Symptom:
|
|
Replay reaches checkpoint 1, then a later `session.send` fails with
|
|
`provider session was never established`. The replay error also reports that
|
|
an outbound write arrived while a recorded stdout chunk was next.
|
|
- Quick checks:
|
|
Inspect `provider/frames.jsonl` around the reported chunk. If a provider
|
|
notification immediately follows a successful request response and precedes
|
|
the next outbound request, compare the reader and writer goroutine timing.
|
|
- Root cause:
|
|
The process cassette encoded the provider byte-stream order, but replay
|
|
treated that order as a required Go goroutine schedule. A writer could run
|
|
before the reader consumed an already-recorded notification, so normal
|
|
concurrency was rejected as a transport mismatch and provider startup was
|
|
torn down.
|
|
- Fix:
|
|
When the next recorded chunk is inbound, block the replay write until the
|
|
receive path consumes it. Continue strict payload validation when the next
|
|
recorded outbound chunk becomes current.
|
|
- Validation:
|
|
Start a replay stream with `outbound -> stdout -> outbound`. Launch the
|
|
second send before receiving stdout; it must wait, then succeed after the
|
|
receive. Keep mismatch, pause, fast-forward, close, and full runtime tests
|
|
passing.
|
|
- References:
|
|
[process_transport_replay.go](../../../packages/agent/daemon/runtime/process_transport_replay.go)
|
|
[process_transport_cassette_test.go](../../../packages/agent/daemon/runtime/process_transport_cassette_test.go)
|
|
|
|
### Continue-session replay sends initialize before the recorded turn
|
|
|
|
- Symptom:
|
|
A restored replay Session reaches prompt dispatch, then fails with
|
|
`process cassette outbound mismatch`: the expected request is `turn/start`
|
|
or `session/prompt`, while the actual request is `initialize`. A second form
|
|
has the same expected and actual `turn/start`, but the actual request is
|
|
missing a provider-derived field such as Codex `collaborationMode`.
|
|
- Quick checks:
|
|
Inspect `provider/manifest.json` and the first outbound frame for the failed
|
|
connection. If recording started on a Session that was already live, its
|
|
`captureOrigin` must be `attached-live-connection`.
|
|
- Root cause:
|
|
Continue-session recording attached to an initialized provider connection,
|
|
so its tape correctly began at the next business request. Replay started a
|
|
new virtual connection but treated every tape as `process-start`, causing the
|
|
adapter to repeat initialization and provider-session resume traffic that
|
|
was outside the captured boundary. If initialization is already skipped but
|
|
a provider-derived field is missing, the semantic `initial-state.json`
|
|
dropped the protocol checkpoint while projecting the canonical historical
|
|
Session graph.
|
|
- Fix:
|
|
Persist the connection capture origin in provider tape schema v3. For an
|
|
attached live connection, restore the provider adapter's protocol checkpoint
|
|
through the historical Session's narrow `providerResumeCheckpoint` and skip
|
|
only the initialization and provider-session bootstrap. Do not export the
|
|
full private runtime context and do not infer missing state from the first
|
|
tape frame. Keep strict matching on the first captured business request.
|
|
- Validation:
|
|
Record a Codex and Cursor Turn after arming capture on an existing live
|
|
connection. Recreate each adapter over replay transport, resume the
|
|
historical Session, execute the same Turn, and require complete tape
|
|
consumption. The regression must cross semantic state JSON capture and Host
|
|
restore; a test that copies an in-memory runtime context directly into the
|
|
replay Session does not cover the product boundary. Also keep new-session
|
|
cold bootstrap tests passing.
|
|
- References:
|
|
[process_cassette.go](../../../packages/agent/daemon/runtime/process_cassette.go)
|
|
[codex_appserver_session.go](../../../packages/agent/daemon/runtime/codex_appserver_session.go)
|
|
[standard_acp_session.go](../../../packages/agent/daemon/runtime/standard_acp_session.go)
|
|
|
|
### Final ACP Markdown loses spacing between blocks
|
|
|
|
- Symptom:
|
|
A completed ACP answer reaches the transcript, but headings, lists, or fenced
|
|
code blocks run together even though the provider's `session/prompt` result
|
|
contains the expected newlines.
|
|
- Root cause:
|
|
The final-result extractor trimmed each nested content block before joining
|
|
them. Whitespace that separated adjacent Markdown blocks was therefore
|
|
removed before the turn normalizer and GUI received the answer.
|
|
- Fix:
|
|
Use trimming only to decide whether a content block is empty. Preserve the
|
|
original non-empty block text while recursively extracting and joining the
|
|
final assistant content.
|
|
- Validation:
|
|
Exercise a complete standard-ACP turn whose final result contains a heading,
|
|
list, and fenced code block. Require the projected assistant snapshot to keep
|
|
all internal Markdown newlines.
|
|
- References:
|
|
[acp_update_events.go](../../../packages/agent/daemon/runtime/acp_update_events.go)
|
|
[standard_acp_turn_test.go](../../../packages/agent/daemon/runtime/standard_acp_turn_test.go)
|
|
|
|
### Cassette replay reports a false final Session state mismatch
|
|
|
|
- Symptom:
|
|
Every recorded stimulus succeeds, but final replay validation reports an
|
|
exact path such as `$.agent.sessions[0].messages[0].id` or a Tutti Mode
|
|
snapshot `turnId`.
|
|
- Quick checks:
|
|
Inspect `expected-state.json` and capture the actual typed semantic state.
|
|
Confirm that Turns and Messages were captured in canonical sequence rather
|
|
than UUID order, and that every reference points to the corresponding
|
|
structural object.
|
|
- Root cause:
|
|
Replay legitimately regenerates Turn, assistant Message, interaction, and
|
|
product-object IDs. Comparing those raw values, or ordering related arrays by
|
|
random UUID, mistakes alpha-equivalent semantic graphs for drift. Message
|
|
payload `seq` is also runtime timing metadata rather than a semantic oracle.
|
|
- Fix:
|
|
Keep original IDs in `initial-state.json` so Host can restore resumable
|
|
history. For final verification, alpha-normalize structural IDs and all their
|
|
references after ordering entities by canonical sequence; omit runtime-only
|
|
payload sequence values. Continue to compare semantic content exactly.
|
|
- Validation:
|
|
Cover regenerated Turn and Message IDs, changed payload sequence values, and
|
|
a real semantic content mismatch. Run both create-session and continue-session
|
|
current-build record/replay loops.
|
|
- References:
|
|
[run-agent-session-replay.mjs](../../../tools/scripts/run-agent-session-replay.mjs)
|
|
[run-agent-session-replay.test.mjs](../../../tools/scripts/run-agent-session-replay.test.mjs)
|
|
|
|
### Replay aborts with `Promise was collected (-32000)`
|
|
|
|
- Symptom:
|
|
A renderer intent has already started, but the temporary CDP runner aborts
|
|
while awaiting its result. Provider replay then reports a partially consumed
|
|
connection during shutdown.
|
|
- Root cause:
|
|
Chromium may collect the remote Promise backing `Runtime.evaluate` while the
|
|
renderer operation is still running. Retrying the renderer command directly
|
|
can duplicate a non-idempotent intent.
|
|
- Fix:
|
|
Register the invocation in the renderer by method and Activity Event ID
|
|
before dispatch. On this CDP error, evaluate the same lookup again and await
|
|
the stored result instead of invoking the command twice.
|
|
- Validation:
|
|
Make the first CDP evaluation fail with `Promise was collected`; the second
|
|
evaluation must reuse the identical invocation expression and complete the
|
|
event exactly once.
|
|
- References:
|
|
[run-agent-session-replay.mjs](../../../tools/scripts/run-agent-session-replay.mjs)
|
|
[run-agent-session-replay.test.mjs](../../../tools/scripts/run-agent-session-replay.test.mjs)
|
|
|
|
### Cassette replay times out before a recorded queued Turn
|
|
|
|
- Symptom:
|
|
Replay stops on `timed out waiting for renderer effect queue/sendPrompt`,
|
|
leaves later activity events untouched, and consumes only the provider frames
|
|
before a long-running Turn settles. The cassette itself contains the missing
|
|
intent, effect, and final Turn.
|
|
- Quick checks:
|
|
Compare adjacent `activity-events.jsonl` timestamps. If the effect was
|
|
recorded more than the renderer effect timeout after its intent, check
|
|
whether replay dispatched both events immediately instead of waiting for
|
|
their recorded interval.
|
|
- Root cause:
|
|
Provider frames followed the daemon playback clock, but the runner iterated
|
|
activity events as fast as JavaScript completed them. Effect verification
|
|
therefore began while the recorded Turn was still running and expired before
|
|
the effect could exist.
|
|
- Fix:
|
|
Advance activity events by relative `occurredAtUnixMs` using the daemon's
|
|
playback state. Pause freezes the activity clock, speed scales it, and
|
|
checkpoint fast-forward skips its remaining wait.
|
|
- Validation:
|
|
Cover normal recorded delay, processing time between events, pause/resume,
|
|
selected speed, and fast-forward. Then replay the failing cassette and require
|
|
the final checkpoint, all provider frames, and every expected Turn to pass.
|
|
- References:
|
|
[run-agent-session-replay.mjs](../../../tools/scripts/run-agent-session-replay.mjs)
|
|
[run-agent-session-replay.test.mjs](../../../tools/scripts/run-agent-session-replay.test.mjs)
|
|
|
|
### Edited prompt remains after edit-and-retry
|
|
|
|
- Symptom:
|
|
The replacement answer appears, but the original user prompt remains,
|
|
reappears after reconnect, or disappears only after switching Sessions.
|
|
- Quick checks:
|
|
Compare the daemon Session-detail Turn projection and effective message read
|
|
with the renderer snapshot. If SQLite marks the old Turn `retracted`, verify
|
|
Session detail uses `ListEffectiveSessionTurns`, not the complete audit
|
|
reader. Then inspect the SessionEngine required/applied history revisions and
|
|
any terminal optimistic message-delta row from the retracted Turn.
|
|
- Root cause:
|
|
Incremental reconciliation can add or update rows but cannot delete a cached
|
|
Turn missed during disconnect. A second variant replaces canonical messages
|
|
while leaving a confirmed optimistic projection alive; a third lets a
|
|
realtime event race a full-history read.
|
|
- Fix:
|
|
Serialize same-Session reads, require a full authoritative replacement for a
|
|
changed edit-retry revision, page backward from a newest-version anchor, then
|
|
drain its live tail and apply one composite Session/Turn/Message snapshot.
|
|
Source the detail projection from effective Turns. Reconcile pending intents,
|
|
attention, and terminal optimistic message-delta rows from that same Turn set
|
|
and stable `clientSubmitId` set; preserve unresolved turnless controls. Retry
|
|
transient projection failures while connected and recheck cached Sessions on
|
|
reconnect.
|
|
- Validation:
|
|
Cover missed events plus reconnect, stale unknown messages, transient
|
|
failures, revision changes during paging, optimistic initial prompts, and a
|
|
terminal delta for a retracted Turn. The final detail and transcript must
|
|
contain only effective Turns and messages while preserving attachments,
|
|
non-text input, turnless controls, and real filesystem effects.
|
|
- References:
|
|
[workspaceAgentActivityReconcileBridge.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/workspaceAgentActivityReconcileBridge.ts)
|
|
[sessionReconcileExecutor.ts](../../../packages/agent/activity-core/src/sessionReconcileExecutor.ts)
|
|
[pendingIntents.authoritativeHistory.ts](../../../packages/agent/activity-core/src/engine/pendingIntents.authoritativeHistory.ts)
|
|
|
|
### Goal-mode recording stalls waiting for a `/goal` palette option
|
|
|
|
- Symptom:
|
|
A Session-Replay recording scenario (or any UI driver) waits forever for
|
|
`agent-gui-composer-slash-command-goal` after typing `/goal` into the
|
|
composer, while typing `/` shows only skill options.
|
|
- Quick checks:
|
|
Inspect `goalDraftObjectiveFromPrompt` in `composerDraftUtils.ts` and the
|
|
`isGoalModeActive` guard in `useComposerPaletteCatalog.ts`. Programmatic
|
|
probes that overwrite the draft via `execCommand('insertText')` can leave the
|
|
React palette draft stuck on a previous `/goal` prefix, which hides all
|
|
command entries and makes the palette look broken for unrelated queries.
|
|
- Root cause:
|
|
`/goal` is a prefix syntax, not a palette command. The moment the draft
|
|
matches `^\s*\/goal(\s+…)?$`, `isGoalModeActive` becomes true, the slash
|
|
query is nulled, and the command section of the palette closes. A palette
|
|
option for the full token can never appear.
|
|
- Fix:
|
|
Drive goal mode by typing the whole `/goal <objective>` draft, waiting for
|
|
the goal badge (`[data-agent-goal-badge="true"]`) plus an enabled send
|
|
button, then clicking send (`submitGoalPrompt` in the record-scenario CDP
|
|
helpers). Never wait for a `/goal` palette option.
|
|
- Validation:
|
|
`pnpm e2e:agent-gui -- --record … --scenario l01` proceeds past goal
|
|
activation (goal turn starts) instead of stalling on the palette wait.
|
|
- References:
|
|
`tutti-agent-session-replay-cases/scenario-runtime/cdp-helpers.mjs`
|
|
[composerDraftUtils.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/composer/composerDraftUtils.ts)
|
|
[useComposerPaletteCatalog.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/composer/useComposerPaletteCatalog.ts)
|
|
|
|
### Replay transport mismatch on `attachmentId`
|
|
|
|
- Symptom:
|
|
Replay fails with 409 `agent_session_replay_transport_mismatch` at a state
|
|
path like `$.agent.sessions[i].messages[j].payload.content[k].attachmentId`.
|
|
- Root cause:
|
|
Attachment identifiers are runtime-generated. Replay re-uploads prompt
|
|
attachments and receives fresh IDs, so byte-equality against the recorded
|
|
state can never hold.
|
|
- Fix:
|
|
Register message-content `attachmentId` values as alpha-equivalent
|
|
identities in `registerReplayIDs` (`services/tuttid/biz/agentsessionreplay/state.go`),
|
|
like session/turn/message IDs.
|
|
- Validation:
|
|
`TestCompareTuttiReplayStateTreatsAttachmentIDsAsAlphaEquivalent` in
|
|
`state_test.go`; a full record→replay of an image-input cassette passes.
|
|
- References:
|
|
[state.go](../../../services/tuttid/biz/agentsessionreplay/state.go)
|
|
|
|
### Replay transport mismatch on Goal Control `operationId`
|
|
|
|
- Symptom:
|
|
Goal cassette record and UI/provider playback succeed, but final transport
|
|
verification fails with 409 at
|
|
`$.agent.sessions[i].messages[j].payload.operationId` on the
|
|
`goal-control:*` session_audit row.
|
|
- Root cause:
|
|
Goal Control operation identities are minted per run. Replay creates a new
|
|
durable operation while replaying the same `/goal` activation, so the audit
|
|
payload's `operationId` cannot match byte-for-byte.
|
|
- Fix:
|
|
Register message-payload `operationId` values as alpha-equivalent identities
|
|
in `registerReplayIDs`, alongside session/turn/message/attachment IDs.
|
|
- Validation:
|
|
`TestCompareTuttiReplayStateTreatsGoalControlOperationIDsAsAlphaEquivalent`;
|
|
record→audit→replay of `l01_codex` (`l01`) passes.
|
|
- References:
|
|
[state.go](../../../services/tuttid/biz/agentsessionreplay/state.go)
|
|
`tutti-agent-session-replay-cases/cases/l01/scenario.mjs`
|
|
|
|
### Project-session replay never shows the restored session in the rail
|
|
|
|
- Symptom:
|
|
Replaying a project-session cassette stalls waiting for
|
|
`agent-gui-conversation-item-<sessionId>`; the conversation rail is empty.
|
|
- Root cause:
|
|
Recording seeds the project into `user_projects` before driving the UI, but
|
|
replay bootstrap did not. Sessions whose `railPlacement.kind` is `project`
|
|
render only under a project rail section, and that section only exists when
|
|
the project row is present.
|
|
- Fix:
|
|
During replay preparation, detect a `railPlacement.kind === "project"`
|
|
activity payload and seed the same project (`seedRecordingUserProject`)
|
|
before launching the desktop; enter the project surface before replaying the
|
|
activation intent.
|
|
- References:
|
|
[run-agent-session-replay.mjs](../../../tools/scripts/run-agent-session-replay.mjs)
|
|
[recording.mjs](../../../tools/scripts/agent-session-replay-runner/recording.mjs)
|
|
|
|
### Interaction replay times out at an `interaction.pending` checkpoint
|
|
|
|
- Symptom:
|
|
Replaying a cassette with an approval/question interaction fails with
|
|
`checkpoint_readiness_timeout` right before the
|
|
`interaction/responseRequested` intent; provider frames stop at the chunk
|
|
preceding the recorded decision write-back. A concurrent
|
|
`agent_session.process_start` log with empty `room_id`/`agent_session_id`
|
|
is the periodic provider availability probe, not a session relaunch — do
|
|
not chase it.
|
|
- Root cause:
|
|
Checkpoint readiness predicates compared recorded activity-layer vocabulary
|
|
(`turn.phase == "waiting_approval"`, `call.status == "running"`) against
|
|
the canonical store vocabulary (`waiting`, `waiting_approval`) by string
|
|
equality, so interaction-wait checkpoints could never become ready.
|
|
- Fix:
|
|
Fold both sides to canonical vocabulary before comparing
|
|
(`semantic_readiness.go`), and record canonical values into new checkpoint
|
|
plans (`checkpoint_provider_candidates.go`). Old cassettes stay compatible
|
|
through the replay-side fold.
|
|
- Validation:
|
|
`TestCanonicalTurnPhaseFoldsActivityVocabulary` and
|
|
`TestDescribeProviderEventFoldsTurnPhaseToCanonicalVocabulary`; replaying
|
|
approval, question, and child-approval cassettes passes.
|
|
- References:
|
|
[semantic_readiness.go](../../../services/tuttid/service/agentsessionreplay/semantic_readiness.go)
|
|
[checkpoint_provider_candidates.go](../../../services/tuttid/service/agentsessionreplay/checkpoint_provider_candidates.go)
|
|
|
|
### Replay times out waiting for idle before a busy-queue `submit/requested`
|
|
|
|
- Symptom:
|
|
Replaying a cassette that enqueues prompts while a turn is still running
|
|
(queue edit / remove / automatic drain) fails with
|
|
`timed out waiting for replay Session … to become idle` on a
|
|
`submit/requested` activity event. The session's `activeTurn.phase` stays
|
|
`running` because the filler turn only settles after later tape actions.
|
|
- Quick checks:
|
|
Inspect `activity-events.jsonl` around the failing sequence. Busy-queue
|
|
submits have no `queue/sendPrompt` / `session/activate` effect with
|
|
`causedByEventId` equal to that submit's `eventId` (they are later canceled
|
|
or drained after the active turn settles). Older cassettes may still show
|
|
`submitDiagnostics.queued: false` on those intents.
|
|
- Root cause:
|
|
The temporary runner treated every `submit/requested` with
|
|
`submitDiagnostics.queued === false` as an immediate send and waited for
|
|
session idle first. The GUI trace starts with `queued: false`, and until the
|
|
engine stamped real queue admission onto the intent, busy-queue submits were
|
|
recorded with that stale false. Waiting for idle deadlocks behind the turn
|
|
that caused the queue in the first place.
|
|
- Fix:
|
|
Stamp `submitDiagnostics.queued` from engine queue admission before the
|
|
intent is observed (`selectEngineSubmitWouldBeVisibleInQueue`). On replay,
|
|
wait for idle only when the submit caused a send/activate effect (and skip
|
|
for `queued: true`, `send_now`, and `immediate`). Re-record queue scenarios
|
|
so new tapes carry the correct diagnostic; older tapes replay via effect
|
|
causation.
|
|
- Validation:
|
|
`submit idle wait skips busy-queue submits and honors send causation`;
|
|
`semantic prompt submission reports visible queue admission for a busy Session`;
|
|
record and replay queue-only `c04_codex`; record and replay native-guidance
|
|
`c06_codex` separately.
|
|
- References:
|
|
[run-agent-session-replay.mjs](../../../tools/scripts/run-agent-session-replay.mjs)
|
|
[createAgentSessionEngine.ts](../../../packages/agent/activity-core/src/engine/createAgentSessionEngine.ts)
|
|
[promptQueue.admission.ts](../../../packages/agent/activity-core/src/engine/promptQueue.admission.ts)
|
|
|
|
### Replay deadlocks at a `turn.canceled` checkpoint of a canceled compaction
|
|
|
|
- Symptom:
|
|
Replay fails with `checkpoint_readiness_timeout` at the checkpoint after a
|
|
canceled `/compact` turn, and the transport reports a deterministic
|
|
`connection ... consumed N of M chunks` where the next unconsumed chunk is
|
|
the inbound frame right before the recorded `turn/interrupt` outbound. The
|
|
canceled turn settles only after Host's cancel grace timeout (~2 minutes),
|
|
the live connection is torn down, and the retry turn fails on a provider
|
|
relaunch that has no recorded connection.
|
|
- Root cause:
|
|
Activity-boundary checkpoint cursors were taken from the observation lane
|
|
(last provider unit that carried checkpoint observation events). A
|
|
compaction turn's interrupted `turn/completed` settles canonical state
|
|
without emitting observations, so the recorded `turn.canceled` cursor
|
|
stopped before the interrupt round trip its own readiness
|
|
(`turn.status == "canceled"`) requires. At replay the input barrier parks
|
|
the reader at that cursor, the daemon's `turn/interrupt` send waits behind
|
|
the unreleased inbound chunk, and readiness can never hold — an
|
|
unsatisfiable plan, not a replay-transport bug.
|
|
- Fix:
|
|
The recording transport already reports every completed provider input
|
|
unit; fold that handled lane into the checkpoint recorder
|
|
(`ObserveProviderInputUnit`) and record activity-boundary cursors as the
|
|
per-connection max of the observation lane and the handled lane
|
|
(`activityBoundaryCursor`). Provider-observation checkpoint cursors keep
|
|
using the observation lane only. Cassettes recorded with the stale cursor
|
|
must be re-recorded.
|
|
- Validation:
|
|
`TestActivityBoundaryCursorCoversHandledUnitsBeyondObservationLane`;
|
|
re-recording and replaying the compaction-cancel-retry cassette passes, and
|
|
the cancel-and-resend cassette replay stays green.
|
|
- References:
|
|
[checkpoint_provider_candidates.go](../../../services/tuttid/service/agentsessionreplay/checkpoint_provider_candidates.go)
|
|
[checkpoint_activity_boundaries.go](../../../services/tuttid/service/agentsessionreplay/checkpoint_activity_boundaries.go)
|
|
|
|
### New Agent conversation rejects a model that is no longer offered
|
|
|
|
- Symptom:
|
|
A newly submitted conversation fails with `model value is not supported by
|
|
agent target`, although the current model picker does not offer that model.
|
|
Alternatively, the selected model appears to start but the provider actually
|
|
continues on its own default.
|
|
- Root cause:
|
|
A target-scoped remembered composer default outlived a changed model catalog,
|
|
or a dependent reasoning default remained bound to the retired model after
|
|
model fallback,
|
|
or the ACP runtime treated a rejected startup model selection as a
|
|
best-effort setting and silently retained the provider default. Reapplying a
|
|
model that `session/new` already selected can also trigger an unnecessary
|
|
provider-side reconfiguration failure.
|
|
- Fix:
|
|
At Create, distinguish a target-scoped persisted default from a model
|
|
explicitly supplied by the caller. For a provider or Agent Extension,
|
|
resolve an obsolete persisted default to the current catalog default
|
|
reported by that same target; never use a different provider. Resolve
|
|
non-explicit per-model reasoning against that effective model while keeping
|
|
explicit caller values strict. A strict per-model reasoning catalog must
|
|
omit an inherited value when it cannot prove that the target model supports
|
|
it. Treat an explicit ACP model selection as identity-bearing: leave an
|
|
already-selected model unchanged, and if a real model change is rejected,
|
|
abort startup rather than falling back.
|
|
- Validation:
|
|
Cover an obsolete persisted default with a multi-model catalog whose reported
|
|
current model is not first, a stale dependent reasoning default, and an
|
|
unsupported explicit selection separately with generic extension fixtures.
|
|
Inject a `session/set_model` rejection into the standard ACP transport test.
|
|
|
|
### Agent send is stopped while an earlier provider process is still exiting
|
|
|
|
- Symptom:
|
|
After an idle Standard ACP, Codex, Tutti Agent, or Claude session is released,
|
|
the next message may reconnect once, but a later Start/Resume returns
|
|
`workspace_operation_failed` with reason
|
|
`agent.process_cleanup_pending`. The message does not reach the provider; in
|
|
AgentGUI the submitted draft is restored and a localized retry message is
|
|
shown.
|
|
- Quick checks:
|
|
Correlate `agent_session.acp.close` stages with
|
|
`agent_session.live_resource_cleanup.failed`. A close failure followed by one
|
|
replacement-process start, then no additional start for the rejected retry,
|
|
confirms cleanup backpressure rather than provider prompt rejection.
|
|
- Root cause:
|
|
A provider process can ignore graceful termination and fail the bounded
|
|
transport Close. Dropping that handle allows unbounded orphan processes;
|
|
allowing its late inbound handler to remain active can also attribute old
|
|
output or approval requests to the replacement Turn.
|
|
- Fix:
|
|
Keep failed and replaced clients under their adapter's ownership, quarantine
|
|
their message handlers, and retry at most one failed Close budget per adapter
|
|
sweep. Codex and Tutti Agent mark a close-failed current client unusable;
|
|
Claude also rejects every late event whose physical connection is no longer
|
|
the current Session generation.
|
|
Once a failed handle is retired, Start/Resume performs one bounded cleanup
|
|
attempt and refuses to spawn another process while cleanup remains pending.
|
|
Preserve the stable reason through the Host/API boundary so AgentGUI can
|
|
restore the draft and present i18n copy.
|
|
- Validation:
|
|
Cover release failure followed by one successful replacement, a blocked next
|
|
Resume with unchanged spawn/prompt counts, startup/load failure retention,
|
|
stale retired-client output, Plan-mode persistence without an eager spawn,
|
|
per-adapter sweep budgets, API classification, localized presentation, and
|
|
failed-submit draft restoration.
|
|
- References:
|
|
[standard_acp_session.go](../../../packages/agent/daemon/runtime/standard_acp_session.go)
|
|
[standard_acp_resource_ownership.go](../../../packages/agent/daemon/runtime/standard_acp_resource_ownership.go)
|
|
[codex_appserver_resource_ownership.go](../../../packages/agent/daemon/runtime/codex_appserver_resource_ownership.go)
|
|
[claude_sdk_resource_ownership.go](../../../packages/agent/daemon/runtime/claude_sdk_resource_ownership.go)
|
|
[AgentGUIEngineSettlementController.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/AgentGUIEngineSettlementController.ts)
|
|
|
|
### Codex rejects `turn/start` with `AbsolutePathBuf deserialized without a base path`
|
|
|
|
- Symptom:
|
|
Codex initializes and `thread/start` succeeds, but the first `turn/start`
|
|
fails with JSON-RPC `-32600` and `AbsolutePathBuf deserialized without a base
|
|
path`. On a managed POSIX runtime, another form accepts the Turn but every
|
|
tool command, including `pwd`, fails with
|
|
`Failed to create unified exec process: No such file or directory (os error 2)`
|
|
even though provider-process CWD preflight succeeded.
|
|
- Root cause:
|
|
Tutti sent the POSIX-only `/sandbox-tmp` writable root as though it were a
|
|
portable absolute host path. Codex's Windows `AbsolutePathBuf` parser rejects
|
|
it even when the request also carries `cwd`; the per-turn working-directory
|
|
override does not make a POSIX-rooted string into a Windows absolute path.
|
|
Tutti also once omitted the Session `cwd` from the Turn override. Supplying
|
|
that field from the raw persisted Session value introduced the inverse POSIX
|
|
failure: a stored `/workspace/<room-id>` mount path escaped the managed
|
|
Agent's logical `/workspace` view even though `thread/start` and the provider
|
|
process had already projected it. Request-shape mocks accepted these invalid
|
|
combinations without exercising the real process or Rust parser boundary.
|
|
- Fix:
|
|
Send the non-empty provider-visible Session `cwd` on every Codex
|
|
`turn/start`. Apply the same room-mount-to-logical-workspace projection used
|
|
by `thread/start` and provider launch while preserving native Windows paths.
|
|
Omit the POSIX `/sandbox-tmp` projection on Windows, and keep it on POSIX
|
|
hosts where it represents the logical `/tmp` write target.
|
|
- Validation:
|
|
Cover a stored room mount root and child path, an already-logical workspace
|
|
path, and a native Windows path in the emitted `turn/start.cwd`. Cover Windows
|
|
and POSIX sandbox policy construction, and run the Windows contract test
|
|
against a real pinned `codex.exe app-server`. The contract test submits the
|
|
historical payload both without and with `cwd` as negative controls, then
|
|
verifies that the production payload crosses the same parser without an
|
|
`AbsolutePathBuf` error.
|
|
|
|
### A Windows Codex session disappears while auth projection is starting
|
|
|
|
- Symptom:
|
|
session creation stalls until its request is canceled, and no runnable
|
|
session remains visible. The log ends in Mutagen `context canceled` during
|
|
auth projection.
|
|
- Root cause:
|
|
Mutagen setup inherited the transport request context. Closing or timing out
|
|
that request killed a partially-created external sync session before runtime
|
|
publication.
|
|
- Fix:
|
|
treat auth projection as Host-owned startup work: detach it from request
|
|
cancellation, bound each Mutagen command to 20 seconds, and serialize setup
|
|
for the same stable auth source. A guarded copy fallback is allowed only when
|
|
Mutagen is unavailable or a failed session was successfully terminated; if
|
|
cleanup cannot be confirmed, fail without starting a second projection.
|
|
- Validation:
|
|
cancel the caller context before projection and verify that the injected
|
|
resolver and runner receive a live bounded context. Cover create and flush
|
|
timeout, cleanup failure, copy reconciliation, and concurrent projections
|
|
sharing one auth source.
|