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
1790 lines
107 KiB
Markdown
1790 lines
107 KiB
Markdown
# Agent Activity Packages
|
|
|
|
Status: current implemented architecture
|
|
|
|
This document records the package split for reusable Agent Activity and Agent
|
|
GUI surfaces. The goal is to make the agent session data flow reusable by other
|
|
repositories while keeping host-specific transport and desktop integration out
|
|
of the shared packages.
|
|
|
|
System-wide ownership and flow rules live in
|
|
[Agent GUI Node](./agent-gui-node.md). This document is the detailed package
|
|
contract for activity-core/runtime/adapter changes, not required reading for a
|
|
presentation-only edit.
|
|
|
|
## Design Goals
|
|
|
|
- Put reusable agent session state, event merging, and attention selectors
|
|
behind a host-agnostic core package.
|
|
- Keep `apps/desktop` responsible for `tuttid`, preload, Electron, local file,
|
|
and runtime integration.
|
|
- Let Agent GUI and Message Center consume one shared Agent Activity snapshot
|
|
instead of building separate session caches.
|
|
- Prepare for external repository adoption through a narrow adapter interface.
|
|
|
|
## Package Map
|
|
|
|
The current package family is:
|
|
|
|
```text
|
|
packages/agent/activity-core
|
|
@tutti-os/agent-activity-core
|
|
|
|
packages/agent/activity-tuttid-adapter
|
|
@tutti-os/agent-activity-tuttid-adapter
|
|
|
|
packages/agent/gui
|
|
@tutti-os/agent-gui
|
|
|
|
packages/agent/activity-replication
|
|
github.com/tutti-os/tutti/packages/agent/activity-replication
|
|
|
|
packages/agent/host
|
|
github.com/tutti-os/tutti/packages/agent/host
|
|
|
|
packages/agent/store-sqlite
|
|
github.com/tutti-os/tutti/packages/agent/store-sqlite
|
|
|
|
packages/agent/store-sqlite/canonical
|
|
github.com/tutti-os/tutti/packages/agent/store-sqlite/canonical
|
|
```
|
|
|
|
`packages/agent/store-sqlite/canonical` is the single authority for canonical
|
|
activity contracts. It owns phase, outcome, origin, interaction-kind and
|
|
interaction-status vocabulary; pure activity projection snapshots and merge
|
|
functions; commit-observer report types; and the provider identity,
|
|
capability, and plan-decision vocabulary needed by canonical persistence.
|
|
Daemon packages retain compatibility aliases, while runtime mechanics remain
|
|
daemon-owned.
|
|
|
|
New canonical messages always carry an explicit
|
|
`userVisibleAssistantResponse` classification. Runtime message producers
|
|
classify the message before reporter and local-store projections write it:
|
|
user-facing assistant text, plans, and visible failures are `true`; user input,
|
|
reasoning, tool calls, and system notices are `false`. Every projection preserves
|
|
that classification. Replication and read contracts continue to accept a
|
|
missing value from older stored messages, so compatibility stays at the read
|
|
boundary instead of being inferred by presentation code.
|
|
|
|
Canonical tool-call persistence also owns the replication-safe payload budget.
|
|
Provider `content` is projected into the formal output fields before MCP
|
|
`structuredContent` is normalized: exact string leaves that duplicate
|
|
`output.text` keep their map key or array position with an explicit duplicate
|
|
marker, while other structured string leaves are deterministically and
|
|
UTF-8-safely shortened with the standard truncation marker. Inputs and
|
|
non-string structured values are never shortened. The final encoded payload
|
|
must fit the 768 KiB canonical budget; if required data alone cannot fit, the
|
|
write returns an error and the transaction does not persist an unsynchronizable
|
|
message. A versioned store migration applies the same projection to retained
|
|
oversized tool calls, including MCP messages, and updates a row only after its
|
|
encoded replacement satisfies the budget. Replication consumers derive a new
|
|
fingerprint and mutation identity from that physical repair, which clears any
|
|
retry schedule tied to the obsolete oversized representation.
|
|
|
|
## Responsibilities
|
|
|
|
### `packages/agent/host`
|
|
|
|
`host` is the provider-neutral Go application boundary for canonical agent
|
|
session and turn lifecycle work. It owns lifecycle input/result contracts,
|
|
narrow canonical-store and runtime ports, runtime preparation and attachment
|
|
materialization ports, clock and scheduler ports, and post-commit observer
|
|
hooks. The `conformance` subpackage owns reusable typed lifecycle scenarios so
|
|
the legacy `tuttid` service, the extracted Host, and downstream adapters can be
|
|
checked against the same behavior baseline.
|
|
|
|
Session read and metadata commands follow the same boundary. `GetSession`
|
|
returns canonical truth with an optional live observation. Settings updates
|
|
are serialized with resume and split between historical persistence and live
|
|
runtime mutation; provider normalization stays in an adapter policy. Pin and
|
|
canonical delete are Host commands, while authorization, transport DTOs,
|
|
shared bindings, and local view cleanup remain adapter-owned.
|
|
|
|
Single and batch canonical delete are the same Host command shape. The store
|
|
plans the complete descendant closure, Host quiesces that exact closure under
|
|
the shared session-mutation lane, and the write transaction rejects a stale
|
|
plan. Renderer-side request loops are not an atomic batch-delete
|
|
implementation. `daemon/hostadapter` is the official daemon-runtime-to-Host
|
|
adapter, `host.SQLiteWorkspaceStore` is the workspace-routed canonical store,
|
|
and `daemon/modelcatalog` owns provider model/reasoning/speed normalization;
|
|
product daemons compose these modules instead of copying their mappings.
|
|
Tool adapters preserve provider-owned terminal state together with raw output
|
|
and exit code. Generic ACP normalization prefers an explicit provider status;
|
|
only when it is absent does it apply the process convention of zero success and
|
|
nonzero failure. The generic layer and GUI never reinterpret status by parsing
|
|
specific command strings.
|
|
|
|
Canonical delete is a lossless tombstone transition. The transaction preserves
|
|
each selected root/child Session component, its Turns, Messages, Interactions,
|
|
effective history, turn submissions, provider resume identity, and attachment
|
|
references.
|
|
It terminalizes only work that cannot safely become live again: active Turns
|
|
are interrupted, pending Interactions are superseded, and runtime/Goal
|
|
operations are settled. Every member of one component receives the same
|
|
deletion timestamp, recoverable-deletion generation, and component size without
|
|
rewriting the Session's original `updated_at`, rail placement, project path, or
|
|
other metadata. Stable Goal and effective-history rows remain unchanged;
|
|
pending Goal work is terminalized without rewriting desired/observed Goal
|
|
content, revision, or tombstone meaning. Components are computed from
|
|
connectivity inside the exact delete set, rather than canonical root identity,
|
|
so sibling child subtrees in a
|
|
batch remain independently restorable. Repeating the same delete is a no-op and
|
|
does not extend retention. Deleting a new live ancestor absorbs any complete,
|
|
current recoverable descendant components into one fresh generation; those
|
|
already-deleted members are not counted or settled again. A legacy or
|
|
incomplete descendant is not upgraded, so the resulting topmost component
|
|
fails restore validation while remaining eligible for explicit permanent
|
|
deletion.
|
|
|
|
`Host.ListDeletedSessions` exposes topmost deleted Sessions in one Workspace—a
|
|
root or child tombstone whose parent is not tombstoned—with title/project
|
|
filtering and stable `updated_at DESC, session id ASC` cursor paging.
|
|
Deleted-session classification and filtering use the immutable persisted
|
|
`railSectionKey` exclusively. The read contract carries that key on every
|
|
Session and project option; `rail_project_path` remains optional presentation
|
|
metadata for labels and restore context and must not become a join or filter
|
|
identity. Transport adapters may resolve an explicit legacy project-path
|
|
filter to its canonical section key before calling Host, but must never infer a
|
|
deleted Session's section from that path or from `cwd`.
|
|
`Host.RestoreDeletedSession` clears the tombstone for the exact complete
|
|
component in one transaction and deliberately does not start or resume a
|
|
provider runtime. Tombstones created by the older lossy implementation remain
|
|
visible for permanent deletion but are explicitly not restorable. A restored
|
|
Session follows the ordinary resume policy only after a later user action. The
|
|
post-commit projection publishes one explicit `session_restored` wake hint per
|
|
restored member. A frontend engine may clear its late-event tombstone only for
|
|
that explicit event, then must hydrate authoritative Session detail before the
|
|
Session becomes visible again; an ordinary reconcile event cannot resurrect a
|
|
deleted Session.
|
|
|
|
Permanent deletion is intentionally distinct from the normal canonical delete
|
|
command. `Host.PurgeDeletedSessions` accepts a cutoff and bounded batch limits,
|
|
while `store-sqlite` selects each topmost tombstone with its complete descendant
|
|
tree, fences every member with its exact `deleted_at` value, and removes an
|
|
eligible tree in one transaction. A tree with any live or too-new member is
|
|
retained as a unit. The first eligible tree may exceed the normal row or payload
|
|
bound so that a large tree cannot permanently starve, while blocked trees do not
|
|
prevent unrelated eligible components from being cleaned. The Store transaction
|
|
seams let the Workspace wrapper remove canonical rows, Tutti Mode state, and a
|
|
durable resource-cleanup outbox item in the same commit. Host does not choose
|
|
retention periods or filesystem paths.
|
|
Explicit workspace-scoped purge validates the physical boundary instead of
|
|
recoverability metadata: every reachable descendant must already be
|
|
tombstoned, after which legacy or incomplete components can be removed
|
|
leaf-first in one transaction.
|
|
The `tuttid` maintenance adapter owns the device-global 15/30-day preference,
|
|
idle-aware scheduling, once-per-day durable eligibility marker, manual purge
|
|
route, and optional database compaction. Workspace-scoped manual purge uses the
|
|
same idle gate and deletes one complete topmost component or every topmost
|
|
tombstone in that Workspace, independent of renderer filters.
|
|
|
|
After that commit, the maintenance adapter first attempts cleanup immediately
|
|
and retries failed entries during later idle maintenance windows; cleanup
|
|
failure never resurrects the deleted row or changes a successful HTTP result.
|
|
The outbox key remains Workspace/Session for per-purge durability, while the
|
|
current runtime sidecar and copied-attachment paths are keyed only by Session
|
|
ID. A pending outbox row therefore fences insertion of that ID in every
|
|
Workspace, and the cleanup worker queries the full canonical database and skips
|
|
deletion if any live or tombstoned row still owns it. Multiple Workspace queue
|
|
rows for one ID are handled idempotently. Recoverable tombstones keep both
|
|
resources; Workspace-keyed runtime and Model Gateway cleanup still runs, while
|
|
the global Agent/browser releaser is skipped if another Workspace has a live
|
|
owner with that ID. Managed worktrees are independent Workspace resources:
|
|
soft deletion, hard deletion, restore, and purge do not retain or remove them.
|
|
They are removed only through the explicit managed-worktree operation.
|
|
|
|
Automatic maintenance starts ten minutes after daemon readiness, checks at
|
|
30-minute intervals, and records completion at most once per 24 hours. It runs
|
|
only while no Agent turn is active and stops between short batches when Agent
|
|
work begins. Manual cleanup uses the same serialized maintenance service but
|
|
targets every current tombstone.
|
|
Only an explicit manual sweep may request database compaction. The daemon
|
|
attempts it after the final idle batch only when the database is small and at
|
|
least one quarter of its pages are reclaimable; automatic maintenance never
|
|
runs a full-database compaction.
|
|
|
|
`store-sqlite` owns the transaction implementation. Its caller-owned
|
|
`TransactionParticipant` seam lets an adapter append a durable outbox marker
|
|
to the same transaction as runtime/goal operation intent, canonical facts, and
|
|
non-re-derivable deletion tombstones without exposing `*sql.Tx` to Host domain
|
|
code. The seam is reserved for facts that must commit or roll back together;
|
|
re-derivable projection gates are repaired by consumers instead.
|
|
|
|
For durable workspace activity reads, `store-sqlite.AgentStateReader` returns
|
|
the canonical root Sessions composed with each Session's latest Turn. It does
|
|
not copy entity fields into a presentation model and does not include online
|
|
Presence or host-owned execution attribution. Hosts that own those independent
|
|
authorities compose them only at their API or product-view boundary.
|
|
|
|
After commit, Host emits typed `CommittedDelta` values through one
|
|
`CommitObserver`. Activity state, messages, root settlement, runtime and goal
|
|
operation milestones, projection-dirty identities, and canonical view
|
|
invalidations all use that path. Observer failure cannot roll back an already
|
|
committed command. Reliable delivery therefore depends on the durable marker,
|
|
while event-stream publication and cache invalidation remain post-commit wake
|
|
hints.
|
|
|
|
The Host release module depends on `store-sqlite` and its `canonical` module,
|
|
not on daemon, sidecar, or `tuttid` packages. It exposes `Run` to supervise the
|
|
runtime-operation, goal-operation, and reconcile-inbox workers as one
|
|
lifecycle, while retaining the individual worker entrypoints. The module does
|
|
not own transport, authorization, room or device identity,
|
|
process or VM implementations, HTTP/OpenAPI shapes, Electron integration, or
|
|
control-plane DTOs. `tuttid` production wiring delegates lifecycle decisions
|
|
to Host and keeps only its adapter responsibilities.
|
|
|
|
### `packages/agent/activity-replication`
|
|
|
|
`activity-replication` is the versioned Go wire contract for projecting an
|
|
owner-local canonical activity store into a cloud read model. It owns batch,
|
|
mutation, entity-key, scope, and snapshot JSON shapes; structural validation;
|
|
duplicate and stale acknowledgement semantics; and backend-neutral
|
|
conformance fixtures. Source/projection and sink/acknowledgement harnesses are
|
|
separate: the SQLite canonical store runs the source fixtures in this
|
|
repository, while external builders and MySQL sinks consume the matching side
|
|
of the same wire cases.
|
|
|
|
It imports the canonical activity vocabulary from
|
|
`packages/agent/store-sqlite/canonical`; it must not redefine turn phase,
|
|
outcome, origin, interaction kind, or interaction status values. It contains
|
|
no SQL, transport, authorization, WebSocket, GUI state, or local command-state
|
|
upserts. Legacy command-state entity names remain decodable only for projection
|
|
tombstone deletes.
|
|
|
|
### `@tutti-os/agent-activity-core`
|
|
|
|
`agent-activity-core` is host-agnostic and must not import React, Electron,
|
|
desktop preload APIs, or the generated `tuttid` client.
|
|
|
|
The published Tutti Mode activation contract retains
|
|
`orchestrationIntensity` as a deprecated effect alias during the
|
|
effect/speed migration. New fields take precedence when both forms are
|
|
present; presentations and daemon responses continue to emit the alias with
|
|
the normalized effect value. The tuttid adapter also accepts an older response
|
|
that contains only the alias and assigns balanced speed (`50`). Removing the
|
|
alias or its selector requires an explicit breaking package/API release.
|
|
|
|
It owns:
|
|
|
|
- agent activity contracts used by UI packages and host adapters
|
|
- the host adapter interface
|
|
- canonical session, turn, interaction, message, composer-option, prompt-queue,
|
|
attention, and edit-retry command state inside one workspace engine
|
|
- memoized projection from engine state to the `AgentActivitySnapshot` runtime
|
|
contract
|
|
- message merge, immutable presentation-sequence ordering, mutable version
|
|
cursor handling, and duplicate handling
|
|
- authoritative Session reconcile execution: scope selection, mapped detail
|
|
aggregation, message cursor/window policy, bounded page draining, the
|
|
discovery/detail race fence, and atomic Engine application
|
|
- a 50ms streaming message reconcile window, aligned with the daemon streaming
|
|
report coalescer, for cached `message_update` observations whose inline
|
|
message versions have a gap; explicit reads, lifecycle transitions, and state
|
|
repair remain immediate, and the deferred read still uses the authoritative
|
|
message cursor
|
|
- selectors for reusable derived state
|
|
- `selectNeedsAttentionCount`
|
|
- `selectNeedsAttentionItems`
|
|
- the workspace session engine (`createAgentSessionEngine` under
|
|
`src/engine/`): intent dispatch loop, domain-composed pure reducers,
|
|
command-description effect executor, expiry-intent clock, and intent frame
|
|
batching, with scheduler/clock/command ports injected by the host
|
|
- the typed frontend effect seam for activation, prompt send, Goal Control,
|
|
settings update, turn cancellation, Interaction response, rename, pin, and
|
|
batch delete,
|
|
including lossless command projection, authoritative Session result
|
|
validation for rename and pin, validated delete-result tombstone projection,
|
|
shared mutation settlement, and a serialized settings-precondition state
|
|
machine
|
|
- semantic `AgentSessionEngine` methods for composer-option loading, Session
|
|
activation, existing-Session Prompt submission, Goal Control, Session stop,
|
|
Interaction response submission, existing-Session settings updates, rename,
|
|
pin, and batch delete. Activation, Prompt, Goal Control, stop, Interaction,
|
|
and settings updates are intent admission: the Engine owns workspace and
|
|
protocol construction.
|
|
Activation owns requested/expiry timestamps, the 120-second confirmation
|
|
window, and the accepted result. This window contains the 90-second
|
|
new-Session activation command instead of allowing presentation expiry to
|
|
race a valid slow startup. The caller retains the stable activation request
|
|
and client submit identities used by optimistic state, dismissal, draft
|
|
recovery, and idempotent retry. While an activation record exists, its
|
|
request identity is single-use: the semantic method reports acceptance only
|
|
when the current dispatch creates a new record, not when a rejected duplicate
|
|
finds an older record for the same Session and mode. Prompt submission owns
|
|
routing, the same confirmation window, and the accepted/queued result while
|
|
its caller retains the stable client submit identity. The queue send command
|
|
uses a 90-second delivery timeout (aligned with new-Session activation) so
|
|
inactive-session Resume/Start can finish inside the 120-second confirmation
|
|
window. Stop, Interaction, and
|
|
settings additionally own command identity plus the 30-second delivery
|
|
timeout.
|
|
The admitted request identity is also projected as the required
|
|
`activationId` on the typed host effect. Pending activation state records the
|
|
command settlement outcome and timestamp separately from the first canonical
|
|
Session observation. Snapshot observations distinguish missing Session,
|
|
workspace mismatch, stale new-Session evidence, and a matching Session.
|
|
Consumers can therefore measure command and snapshot latency independently;
|
|
repeated identical observations do not churn state, and a late command result
|
|
cannot regress an already confirmed activation.
|
|
Session stop owns the 30-second first-Turn waiting window and duplicate
|
|
admission across Desktop and Mobile. Interaction submission owns canonical
|
|
pending-target admission,
|
|
in-flight deduplication, and exact failed-response retry; it never recovers
|
|
omitted answer fields from a prior attempt. Settings updates own serialized
|
|
patch merging and recognition of a fresh user update as retry after unknown
|
|
delivery. Consumers observe the existing operation projections. The other
|
|
methods additionally hide cache or mutation coordination, settlement
|
|
waiting, and canonical result projection from product hosts. Mutation
|
|
methods own timeout and cancellation policy; hosts retain transport, DTO
|
|
mapping, AbortSignal propagation, and product-specific command extensions
|
|
(see
|
|
[Agent GUI Node](./agent-gui-node.md#4-workspace-frontend-engine))
|
|
|
|
The public host-effect seam is `AgentSessionEffectPort`; the public
|
|
application-write seam is the semantic `AgentSessionEngine` methods.
|
|
Product hosts call `updateSessionSettings` for an existing Session instead of
|
|
constructing `session/settingsUpdateRequested` protocol fields. Settings held
|
|
before Session activation remain part of the activation flow and do not pass
|
|
through this existing-Session method.
|
|
Desktop AgentGUI and Mobile call `activateSession` instead of constructing
|
|
`activation/requested` protocol fields. Product surfaces still choose target,
|
|
placement, initial settings/content, and durable request identities; the Engine
|
|
admits that domain request under one workspace scope and confirmation policy.
|
|
Only an admitted activation may clear the new-Session draft. Actual Session
|
|
creation, resume, and initial Turn lifecycle remain Agent Host semantics behind
|
|
the host effect port. The typed effect returns an authoritative Session for a
|
|
new activation and the complete authoritative detail aggregate for an existing
|
|
activation. The Engine validates activation mode plus workspace/Session
|
|
identity and nested Session/Turn/Interaction entities before applying that
|
|
result through its canonical reducers. Desktop and Mobile effects may retain
|
|
product-local integration and observability, but must not dispatch Session
|
|
state before returning. The effect executor marks activation results with the
|
|
versioned `activation-v1` result contract; commands whose result shape is not
|
|
authoritative remain opaque.
|
|
An admitted new-Session activation may additionally carry the provider-neutral
|
|
`isolation: "worktree"` launch request. Activity Core preserves that field
|
|
through pending intent and typed effect projection; the tuttid adapter maps it
|
|
through the generated Create Session contract. It does not allocate a worktree
|
|
or choose a repository. Before allocation, the tuttid product adapter resolves
|
|
the exact Agent Target and rejects shared/remote target identities; the same
|
|
target-aware rule protects its worktree-support probe, so a caller cannot
|
|
bypass the AgentGUI visibility gate through the public daemon API. Agent Host
|
|
resolves the selected `cwd`, creates the
|
|
worktree from the current `HEAD` even when the checkout is dirty, and returns
|
|
the authoritative Session with durable isolation metadata. The canonical
|
|
Session projection carries `mode`, `worktreePath`, `branch`, and `baseCommit`
|
|
through list/detail/event flows so renderers do not infer isolation from `cwd`.
|
|
The transport and published core field remain optional for compatibility;
|
|
canonical normalizers map omission to `null`. Omitting the create field retains
|
|
the existing local-checkout behavior, preserving older hosts and external
|
|
AgentGUI consumers.
|
|
New-Session Goal Control uses the same activation path. The Engine carries a
|
|
typed `initialGoalControl`; Desktop and Mobile send it through the typed Create
|
|
contract with empty initial content. Agent Host creates the Session and durable
|
|
Goal operation without creating a Turn. It establishes the canonical Session
|
|
title from the display prompt, or from a synthesized `/goal` command when the
|
|
display prompt is absent. Non-empty initial content and typed initial Goal are
|
|
mutually exclusive, and product adapters must not reconstruct the command from
|
|
display text.
|
|
The public Session contract optionally carries `goalSyncState` as a narrow
|
|
Host-owned observation: revision, sync status, pending operation identity, and
|
|
optional `executionPending` proof. It does not move Goal saga ownership into
|
|
Activity Core. Missing state means the host cannot prove progress; explicit
|
|
`null` clears prior evidence, and Session merge preserves prior evidence when a
|
|
compatible partial projection merely omits the field. AgentGUI may use an
|
|
identified pending/applying/unknown operation for initial-Goal loading
|
|
continuity without inventing a Turn or a UI timeout. `synced` alone is terminal
|
|
mutation evidence, not proof that a future Turn will exist; only explicit
|
|
Host-owned `executionPending=true` bridges convergence to the first exact Goal
|
|
Turn. Terminal/non-active Goal evidence and that Turn clear the proof.
|
|
Desktop AgentGUI and Mobile call `stopSession` instead of constructing
|
|
`session/stopRequested` protocol fields. The same method stops an active Turn
|
|
or records a bounded request that cancels the first Turn produced by an
|
|
in-flight activation. When an implicit stop observes a submit admission without
|
|
an active Turn, it retains only a submit that may still produce an unsettled
|
|
Turn: definitive admission failure and a known settled canonical Turn are not
|
|
stop targets. A visible queued prompt that has not entered delivery is backlog,
|
|
not a stop target; a queued prompt remains eligible only while its exact queue
|
|
record is in flight or has uncertain delivery. Missing or out-of-order
|
|
canonical evidence for an immediate admission remains eligible, and late
|
|
activity messages correlate the exact submit identity before issuing the Turn
|
|
cancel.
|
|
Desktop AgentGUI and Mobile call `submitPrompt` for an existing Session instead
|
|
of constructing `submit/requested` protocol fields or reading multiple Engine
|
|
selectors to infer whether the submission was admitted. The Engine fixes the
|
|
same confirmation window and routing semantics for both surfaces; each surface
|
|
uses the returned accepted/queued result to decide whether its submitted draft
|
|
may be cleared. New-Session initial content remains part of activation.
|
|
AgentGUI, Message Center, Desktop notifications, and Mobile call
|
|
`submitInteractionResponse` for a canonical pending Interaction instead of
|
|
constructing `interaction/responseRequested` protocol fields. A failed
|
|
submission becomes retryable only when the surface explicitly submits the same
|
|
answer again; a missing or changed answer fails closed.
|
|
AgentGUI calls `controlGoal` for an existing Session instead of calling the
|
|
runtime adapter or constructing `goal/controlRequested`. The caller proposes
|
|
a stable client-submit identity and admission returns the effective identity
|
|
used by the Engine; the Engine owns command identity, 30-second
|
|
timeout, one in-flight admission per Session, optimistic Goal projection,
|
|
typed result validation, and exact-identity retry after unknown delivery.
|
|
Desktop and Mobile `controlGoal` effects only forward transport and map the
|
|
authoritative Session, Goal, durable operation identity, and Goal state. Goal
|
|
Control responses carry `goal` as a required nullable field so a successful
|
|
clear is an explicit `null`, never an omitted value that can fall back to a
|
|
stale runtime Session projection. That field is Host's durable desired
|
|
projection, while provider output remains separately observable through Goal
|
|
state. An empty pause/resume observation therefore records divergence without
|
|
erasing the visible Goal; only a durable tombstone returns `null`. The daemon
|
|
overlays that Host-owned result onto `session.goal`, and the Engine applies the
|
|
same invariant when a synced typed result reaches its canonical Session state.
|
|
Daemon Session reads apply the same authority rule: unresolved durable state
|
|
projects `desired`; synchronized state projects `observed` progress; a durable
|
|
tombstone clears the Goal. The Goal-state timestamp advances the Session
|
|
envelope. Single-Session reads, batch lists, refreshes, and realtime-driven
|
|
reloads therefore cannot overwrite a newer Goal with an older provider snapshot
|
|
or resurrect a completed Goal as active.
|
|
After Session creation, any later Goal Control operation retires the initial
|
|
activation Goal as a presentation source. The operation result and canonical
|
|
Session projection then own the banner, so the creation-time objective cannot
|
|
shadow a replacement Goal.
|
|
Engine-originated requests always send their caller-stable identity through to
|
|
the existing Agent Host Goal saga.
|
|
Every admitted mutation reaches Host; frontend Goal equality is not a no-op
|
|
rule because Host owns durable revision and audit creation. Pending/applying
|
|
Host state settles the frontend request as accepted, while transport loss,
|
|
timeout, opaque/malformed success, or Host unknown/diverged state stays
|
|
unknown. A retry of the same unknown mutation reuses the original
|
|
`clientSubmitId`, while a definitive failure releases that identity so the
|
|
next explicit attempt is a new operation. Generic Session reconciliation and
|
|
Goal value equality cannot settle that operation because neither proves its
|
|
durable identity.
|
|
The public Goal presentation and settlement maps are sparse. The Engine
|
|
updates them only for Session identities whose canonical Goal, Goal operation,
|
|
or Goal-bearing activation changed; it indexes pending Goal activations once
|
|
per relevant drain. Turn streaming and unrelated Session metadata preserve
|
|
the Goal branch and unrelated per-Session presentation references.
|
|
Desktop and Mobile keep a submitted Goal draft until that settlement reaches
|
|
`accepted` or `succeeded`, and clear it only if the user has not edited the
|
|
draft in the meantime. A definitive failure retains the text but releases the
|
|
pending identity; an unknown result retains both the text and identity for
|
|
explicit retry.
|
|
Rename and pin effects return an authoritative Session envelope, and batch
|
|
delete returns the complete typed deletion result. Reducers still validate
|
|
those results before applying canonical state, while the public port prevents
|
|
hosts from inventing a different result shape.
|
|
The reducer mutation protocol remains an Engine implementation detail; product
|
|
hosts use the semantic mutation methods. Prompt precondition ordering and its
|
|
helper port are also not exported from the package root. Reducer-only prompt continuation intents are
|
|
absent from public `EngineIntent`; their bookkeeping is also absent from
|
|
`AgentSessionEngineState`, `getSnapshot()`, and subscription callbacks.
|
|
|
|
Edit retry follows the same frontend command rule as pin, delete, cancel, and
|
|
reconcile: the engine owns the stable client operation id, pending/failure
|
|
record, typed recovery intent, and authoritative state-and-message follow-up.
|
|
React may retain an unsent editor draft, but it must not subscribe to raw
|
|
transport events or sequence edit-retry HTTP calls itself. The Desktop command
|
|
port translates `turn/editRetry` and `turn/recoverEditRetry` to the injected
|
|
`TuttidClient`; the result re-enters the reducer as `reconciling`, while only
|
|
the authoritative session-detail projection may replace edit-retry
|
|
availability and release that fence.
|
|
|
|
Edit retry also requires deletion-capable history convergence. The engine keeps
|
|
the required history revision separate from the applied revision. An empty
|
|
cache may establish its initial revision through the ordinary detail-plus-page
|
|
read; once messages are cached, a changed or explicitly required revision must
|
|
use one composite authoritative snapshot that replaces Session, Turn, and
|
|
Message projections instead of incrementally merging them. Desktop serializes
|
|
same-Session events and reads, loads the complete descending history at a fixed
|
|
newest-page anchor, drains the ascending tail, fences the read with session
|
|
detail before and after, and retries transient projection failures while the
|
|
event stream is connected. Reconnect rechecks every cached Session.
|
|
|
|
That composite snapshot also reconciles settled optimistic submissions and
|
|
completion attention. It removes an optimistic row only when its stable Turn
|
|
and client-submit identity are both absent from effective history; unresolved
|
|
requests and turnless controls remain. Historical Turns are projection truth,
|
|
not live attention signals. Files changed by a retracted Turn remain real
|
|
filesystem effects and are not compensated by this renderer replacement.
|
|
The daemon Session-detail projection must therefore read effective Turns;
|
|
complete `ListSessionTurns` results remain available only to audit-oriented
|
|
queries. Terminal message-delta overlays use the same effective Turn and
|
|
client-submit identities when authoritative history makes omission meaningful.
|
|
|
|
It does not own:
|
|
|
|
- HTTP path construction
|
|
- authentication
|
|
- `EventSource` or fetch implementation details
|
|
- `tuttid` generated client usage
|
|
- workspace file access
|
|
- Electron IPC or preload APIs
|
|
- React hooks or UI components
|
|
|
|
### `@tutti-os/agent-activity-tuttid-adapter`
|
|
|
|
`agent-activity-tuttid-adapter` is the monorepo-private, narrow
|
|
platform-neutral mapping boundary between generated
|
|
`@tutti-os/client-tuttid-ts` workspace-agent DTOs and `agent-activity-core`
|
|
entities. Desktop and Mobile consume the same protocol-v2 contract assertions
|
|
and Session, Turn, Message, and Tutti-mode projections. The package also owns
|
|
the pure outbound create-Session and send-input projections into generated
|
|
tuttid request DTOs. Those projections use explicit allowlists: activity-only
|
|
prompt fields never cross the HTTP boundary, and Turn `capabilityRefs` survive
|
|
both immediate send results and later detail reconciliation.
|
|
Current-user identity is mandatory mapper input from the application host:
|
|
Desktop supplies its local AgentGUI identity and Mobile supplies the immutable
|
|
authenticated account user id.
|
|
|
|
It may depend on the generated client and `agent-activity-core`, but must not
|
|
own HTTP execution, authentication, retries, event subscriptions, logging,
|
|
i18n, Electron/React Native APIs, DI scopes, or command orchestration. Those
|
|
remain application-host responsibilities. Application hosts pass canonical
|
|
activity inputs to this package and execute the returned generated request;
|
|
for shared create/send/Turn contracts they do not maintain host-local request
|
|
mirrors, object-spread casts, or response-field allowlists. If a proposed
|
|
extraction requires a large callback surface for host concerns, keep it in the
|
|
application adapter instead.
|
|
|
|
### `@tutti-os/agent-gui`
|
|
|
|
`agent-gui` is the renamed successor of
|
|
`@tutti-os/agentactivity-renderer`.
|
|
|
|
It owns:
|
|
|
|
- `AgentGUI`
|
|
- the `AgentGUIRuntime` provider and hooks used by AgentGUI
|
|
- Agent GUI workbench node UI
|
|
- session list and detail rendering
|
|
- timeline, tool call, approval, and interactive prompt presentation
|
|
- package-owned stylesheet entrypoint
|
|
- React-facing hooks or providers that are specific to Agent GUI
|
|
- Message Center snapshot model and UI while it shares AgentGUI activity and
|
|
interaction ownership
|
|
|
|
The published `@tutti-os/agent-gui/activity-list-projection` entrypoint is the
|
|
single high-level projection for compact Agent Activity cards. Hosts first map
|
|
transport DTOs to canonical Sessions, Presences, Messages, and Turn
|
|
file-change snapshots, then call this projection. Product hosts may enrich the
|
|
result with viewer-relative identity, avatars, board lanes, and navigation
|
|
actions; they must not rebuild status, title, summary, ordering, provider, or
|
|
artifact semantics.
|
|
|
|
Room-scoped attention consumers that need every actionable target use the
|
|
Agent GUI package's `selectWorkspaceAgentAttentionItems` projection. Unlike the
|
|
conversation-shaped Message Center model, this projection returns one entry per
|
|
canonical `(workspaceId, agentSessionId, turnId, requestId)` target, including
|
|
multiple pending interactions in one root conversation. The display item keeps
|
|
the root conversation identity for navigation, while the target keeps the exact
|
|
child interaction identity for commands. Hosts may enrich or filter these
|
|
entries for local owner/device authority, but must not rebuild prompts from
|
|
transcript rows or collapse them by root session id.
|
|
|
|
`WorkspaceAgentMessageCenterCard` keeps compact prompts and digest summaries by
|
|
default. Dense room boards may opt into its full prompt variant and suppress the
|
|
redundant digest only while a canonical prompt is present; both variants submit
|
|
through the same exact target and shared prompt surface.
|
|
|
|
It may depend on `@tutti-os/agent-activity-core`.
|
|
|
|
Agent GUI reads activity data and resolves the workspace Engine through
|
|
`AgentGUIRuntime`; lifecycle writes use semantic Engine methods or typed
|
|
intents. The runtime has no duplicate activation, submit, Goal, Interaction,
|
|
settings, Tutti Mode, or unactivation callbacks. The effective `AgentHostApi`
|
|
is limited to host
|
|
capabilities such as files, clipboard, runtime metadata, account/project
|
|
lookup, diagnostics, setup, and OS/Workbench helpers. The public
|
|
`AgentHostInputApi` no longer exposes the legacy `agentSessions` activity
|
|
lifecycle shape; production AgentGUI must not use the host capability contract
|
|
as an activity source.
|
|
Large pasted text is a separate runtime capability rather than an inference
|
|
from generic file upload. `stagePastedText` returns one provider-readable
|
|
locator: a local archive `path`, or an ordinary prepared remote-file `url`
|
|
with optional object-store identity metadata. AgentGUI keeps the local path
|
|
behavior as a read-file instruction, while a remote locator is emitted as an
|
|
ordinary prepared file so shared hosts use their existing attachment transport;
|
|
it must never be disguised as a local path. The conversation `displayPrompt`
|
|
for a remote pasted-text asset carries only its safe preview mention; short-
|
|
lived URLs and object-store locators remain in structured prompt content and
|
|
must not enter caller-visible transcript projections.
|
|
Device-global quick prompts are also an optional host capability rather than
|
|
activity data. The desktop adapter combines the generated `tuttid` client and
|
|
global invalidation events behind
|
|
`AgentHostApi.quickPrompts`. AgentGUI may subscribe to that capability to render
|
|
the composer picker and management dialogs, but quick-prompt entities must not
|
|
be copied into `AgentGUIRuntime`, a workspace engine, a Session, or a Turn.
|
|
The daemon remains authoritative for durable prompt records and optimistic
|
|
version conflicts; event payloads are content-free refresh hints.
|
|
Quick-prompt ordering follows the same boundary: `tuttid` stores a dense
|
|
integer `sort_order` and accepts one moved prompt plus a nullable
|
|
`beforePromptId` anchor and the moved prompt's expected version. Desktop may
|
|
project the resulting order optimistically, but it must replace that projection
|
|
with the daemon's authoritative list. AgentGUI only renders and requests moves;
|
|
hosts that omit the optional move capability keep the library read/write-only.
|
|
The v2 prompt-order schema is a forward-only daemon migration. After it is
|
|
applied, rollback means reverting the renderer surface; do not run an older
|
|
daemon writer against that database.
|
|
Older writers do not maintain `sort_order`, so binary daemon downgrade followed
|
|
by create/delete is not a supported recovery path.
|
|
Conversation rail sections are also an `AgentGUIRuntime` contract:
|
|
AgentGUI calls `listSessionSections` for the first page of every returned rail
|
|
section and `listSessionSectionPage` for Show more by `sectionKey` and cursor.
|
|
Hosts must pass those calls through to the daemon section endpoints so project
|
|
sections come from current user projects and session membership comes from
|
|
persisted `rail_section_key`, not frontend cwd grouping or project-root
|
|
filters.
|
|
Hosts whose first-page section endpoint accepts less than AgentGUI's default
|
|
refresh limit declare the positive backend maximum through
|
|
`conversationRailQueryLimits.sectionRefreshLimitMax`; AgentGUI clamps adaptive
|
|
same-scope and scope-switch refreshes before calling `listSessionSections`.
|
|
The published `@tutti-os/agent-gui/conversation-rail-runtime` entrypoint owns
|
|
the host-neutral Rail query/mutation cohort. Its stable host surface is the
|
|
typed `createAgentConversationRailRuntime` factory plus the runtime/source
|
|
types; method-name manifests and UI capability inspection are package-internal.
|
|
Downstream hosts such as tsh compose the typed factory and do not import those
|
|
test or presentation helpers. The sibling
|
|
`@tutti-os/agent-gui/conversation-rail-controller` entrypoint owns the
|
|
controller interface, workspace-Engine-scoped query caches, and
|
|
`createAgentGUIConversationRailQueryController` factory. Product hosts provide
|
|
the canonical Rail queries and construct the controller with their workspace
|
|
Engine through that factory; they must not instantiate the package-internal
|
|
implementation or copy query scope, first-page refresh, cursor pagination,
|
|
stale-request fencing, membership reconciliation, Engine ingestion, forwarding
|
|
methods, or cache lifetime into app or renderer composition code.
|
|
Transport adapters still own HTTP/IPC DTO mapping, authorization, and protocol
|
|
errors. In particular, `listSessionSectionDeletionCandidates` and
|
|
`deleteSessionsBatch` are one atomic batch-deletion capability. AgentGUI disables
|
|
the action and reports
|
|
`agent.gui.conversation_batch_delete.capability_incomplete` when a host exposes
|
|
only one half, instead of accepting a click that cannot complete.
|
|
Desktop and Native Mobile use the same headless Rail controller. Generated
|
|
Session DTOs from first-page and pagination responses are transient adapter
|
|
input and are immediately upserted into the workspace Engine; they are never
|
|
retained in a second host Rail entity store. Hosts retain only transport
|
|
mapping, runtime-availability policy, surface lifecycle, presentation, and
|
|
host-specific refresh cadence such as Mobile disconnected polling.
|
|
The pure Rail contracts are the canonical source for both the controller and
|
|
the `AgentGUIRuntime` Rail aliases. The public
|
|
controller snapshot exposes memberships, ordered ids, pagination, search, and
|
|
request state only; Desktop joins it with Engine state for localized
|
|
conversation summaries outside the headless entrypoint. Resolved cache entries
|
|
are shared by controllers created for the same workspace Engine; the factory,
|
|
not `AgentGUIRuntime` or a host adapter, owns that registry. The cache
|
|
implementation has no published AgentGUI subpath. In-flight first-page entity
|
|
payloads stay scoped to one attached controller generation so an obsolete mount
|
|
cannot ingest or cache them.
|
|
Every daemon `WorkspaceAgentSession` response carries the persisted membership
|
|
as required `railSectionKey`. The desktop adapter rejects a missing or blank
|
|
value as a protocol contract error; it must not manufacture `conversations` or
|
|
derive a project key from `cwd`.
|
|
Agent-generated-file search follows the same membership contract. The daemon
|
|
reads a bounded window of recently settled `Turn.fileChanges` snapshots,
|
|
joins them to non-deleted sessions by exact required `sectionKey`, and combines
|
|
file state in Go. The desktop provider passes the active conversation's
|
|
`railSectionKey`, the selected project's persisted `sectionKey`, or the fixed
|
|
`conversations` key. It fails closed when that identity is unavailable.
|
|
Neither the daemon nor the renderer maintains a generated-file projection or
|
|
scans activity messages as a fallback, and pre-contract history is not
|
|
backfilled from messages.
|
|
The session service synchronously persists and reads back the initial runtime
|
|
session before returning a successful Create response, so the response never
|
|
races the runtime's asynchronous activity reporter. The store assigns
|
|
`railSectionKey` on that first persistence and preserves it across later runtime
|
|
reports, `cwd` changes, and user-project list refreshes. Removing a project is
|
|
an authoritative daemon operation. It repeatedly sends live unpinned root
|
|
Sessions through Host batch deletion, then uses one compare-and-finalize SQLite
|
|
transaction to rehome retained pinned trees and recoverable tombstones to
|
|
`conversations` and remove the user-project row. A concurrent initial Session
|
|
write validates an explicit project placement against the project rows in its
|
|
own transaction, so a stale placement cannot recreate an orphan section after
|
|
removal.
|
|
Section and pinned-page results include required `totalCount` for the complete
|
|
target-filtered scope before cursor pagination. AgentGUI uses it to subtract a
|
|
transient active-row overlay from remaining unseen rows; hosts must preserve the
|
|
field end to end instead of recomputing it from the bounded `sessions` array.
|
|
The `listSessionSections` bootstrap also carries the first pinned session page,
|
|
and pinned Show more uses the dedicated pinned page endpoint/runtime method.
|
|
Pinned is not a section kind; it is a session/rail-record projection derived
|
|
from `pinnedAtUnixMs` so pinned conversations can render on first load even
|
|
when they are older than the first ordinary project or Chats page.
|
|
When AgentGUI's provider rail is narrowed to one target, the runtime request
|
|
must include `agentTargetId`; hosts and the daemon apply it before section
|
|
pagination so `hasMore` describes the target-filtered rail, not the unfiltered
|
|
workspace history.
|
|
Conversation search uses the optional `listSessionsPage` runtime query backed
|
|
by `GET /v1/workspaces/{workspaceID}/agent-sessions`. The daemon applies
|
|
`searchQuery` and `agentTargetId` to the complete visible workspace session set
|
|
before cursor pagination; this is not a filter over already-loaded section
|
|
pages. Search pages follow the same normalized ownership rule as section pages:
|
|
returned sessions are upserted into the workspace engine, while the search
|
|
query retains only ordered ids, cursor, and request state. The UI joins those
|
|
ids to canonical engine entities. Search rows are placed by exact equality
|
|
between the session's `railSectionKey` and a daemon-returned section key;
|
|
`pinnedAtUnixMs` remains the independent pinned projection. Missing keys are
|
|
invalid desktop protocol data, not a signal to infer membership from cwd or a
|
|
resolved project. Hosts without this optional query may keep a
|
|
loaded-row-only local title filter for previews, but desktop hosts must pass the
|
|
backend query and pagination fields through unchanged.
|
|
The canonical SQLite repository owns the shared root-session query semantics
|
|
for conversation search, target filtering, visibility, stable ordering, and
|
|
cursor pagination. A composing host may pass an explicit authorized Session-id
|
|
set into that repository query; the repository applies it before pagination,
|
|
section totals, and batch-deletion candidate selection. Service and host
|
|
adapters may hydrate or authorize the returned entities, but must not copy the
|
|
filter/sort/page algorithm into transport-specific code.
|
|
Activating a conversation must not by itself call `listSessionSections` again.
|
|
Likewise, active detail provider changes should not reload section first pages.
|
|
Page sessions must be upserted into the workspace engine, while the rail query
|
|
cache retains only ordered session ids, cursors, totals, and section metadata.
|
|
The shared conversation-Rail controller factory keeps resolved cache entries
|
|
per workspace `AgentSessionEngine`, rather than exposing cache ownership through
|
|
a host runtime or mounted controller. Fresh first pages are reused for 30
|
|
seconds across controller remounts and repeated target switches. In-flight
|
|
request coalescing remains controller-local; the factory shares only resolved
|
|
entries across controllers. The cache never owns session entities, titles,
|
|
lifecycle, or interaction state.
|
|
Rename, pin, delete, and through-Turn Fork are Engine mutations, not direct
|
|
runtime calls from AgentGUI. Rename, pin, and delete enter through semantic
|
|
Engine operations rather than reducer-protocol assembly in a product host.
|
|
The engine records the pending mutation, emits one semantic command, and feeds
|
|
the command result back through its reducer loop. Successful rename and pin
|
|
Session results plus validated delete tombstones enter canonical state as
|
|
follow-up intents in the same engine drain. The product activity facade awaits
|
|
the semantic rename, pin, and delete methods and never allocates mutation
|
|
identity, chooses timeout policy, or reads mutation records. The command port is
|
|
the only transport executor. Settled mutation records use a bounded window;
|
|
they are workflow evidence, not an unbounded history store.
|
|
Goal Control follows the same single-core rule without becoming a Session
|
|
metadata mutation: `goalControl.operationsBySessionId` retains only the latest
|
|
frontend operation for each Session. A validated typed response applies its
|
|
Session through the canonical lifecycle reducer and retains Goal operation
|
|
identity/state as settlement evidence. Canonical Session Goal equality is only
|
|
presentation data and never confirms a pending or unknown operation. Durable
|
|
Goal revision, provider application, recovery, audit, and idempotency remain
|
|
owned by `packages/agent/host`. The package root exposes a narrow settlement
|
|
projection rather than the internal operation ledger, and `getSnapshot()` does
|
|
not carry that ledger at runtime.
|
|
Fork is long-lived: an HTTP `202 accepted` keeps the mutation in flight until
|
|
the canonical target Session with matching durable lineage is upserted. The
|
|
Engine disables only another Fork for that exact source Turn. Source activity,
|
|
pending Interactions, and an observation ACK for an already committed child do
|
|
not become Fork availability gates.
|
|
The Desktop activity adapter reconciles an accepted operation through the
|
|
durable operation GET endpoint with capped backoff. It never redispatches the
|
|
provider mutation. A committed result enters the Engine as the canonical child;
|
|
failed or delivery-unknown results terminate the mutation so the action can
|
|
report failure and be retried with the correct identity.
|
|
When one of those canonical commits changes page membership, the rail query
|
|
controller reloads only the affected first pages. Its public snapshot contains
|
|
daemon membership and query publication state, not derived Engine
|
|
conversations. The controller keeps committed membership visible while draft
|
|
page requests are pending, then publishes the resolved membership once. The
|
|
Desktop adapter independently subscribes to the Engine and joins that canonical
|
|
state with the headless snapshot to derive localized conversations; it does not
|
|
own a second Session or Rail query cache. The controller does not inspect Engine
|
|
mutation records. A failed targeted read leaves committed membership visible
|
|
and locks membership-sensitive actions until an authoritative scoped refresh
|
|
succeeds.
|
|
Attach compares current canonical membership with the last observed membership
|
|
and invalidates interrupted draft work before bootstrap, so changes completed
|
|
while every panel is closed are revalidated without mutation-history coupling.
|
|
Section first-page reloads should be tied to workspace, rail filter, user
|
|
project, or session membership changes.
|
|
Historical rows already owned by loaded section pages can be absent from a
|
|
later bounded list response. Snapshot omission is not deletion: the engine
|
|
keeps those entities until an explicit removal event. Hydrating or updating one
|
|
is an entity-detail change, not a rail membership change; loaded pages and
|
|
cursors must remain intact. Do not use raw engine session order or count as the
|
|
section query invalidation key.
|
|
|
|
`AgentActivity*` types are the canonical frontend agent activity data model.
|
|
Agent GUI must import `AgentActivitySession`, `AgentActivitySnapshot`, and
|
|
`AgentActivityPresence` from `agent-activity-core`; it must not recreate those
|
|
entities in a handwritten aggregate. GUI-only projections stay in focused
|
|
timeline, synchronization, summary, and message-overlay modules. Working and
|
|
completion decisions derive from canonical `activeTurn` and `latestTurn`
|
|
state, not from legacy session-level lifecycle mirrors.
|
|
Canonical sessions also carry typed `settings`, `permissionConfig`,
|
|
`capabilities`, `usage`, `goal`, `imported`, and root/parent relationship fields
|
|
from the daemon. Desktop adapters preserve those fields and must not recreate
|
|
them from `runtimeContext`, `lastError`, or module-global per-session defaults.
|
|
Provider-native child work is represented only by child sessions and their
|
|
turns; there is no parallel session metadata summary for it.
|
|
Create/send intents carry the provider `planMode` setting independently from a
|
|
Tutti-owned activation intent. `/plan` controls the boolean provider setting;
|
|
`/tutti` creates or advances a durable `TuttiModeActivation`. Pending draft
|
|
intent, creation, revision-checked updates, daemon projection, and canonical
|
|
Turn snapshots preserve both values without treating them as a mutually
|
|
exclusive mode enum. Historical `capabilityRefs` remain audit data and are
|
|
trimmed and deduplicated by source plus capability, but they do not own the
|
|
activation. Every submit route, including immediate sends, active-turn
|
|
guidance, queued delivery, and provider Plan feedback, preserves that audit
|
|
metadata when Tutti is active.
|
|
|
|
Pending submit records preserve business provenance separately from their
|
|
opaque `clientSubmitId`. Provider Plan feedback uses the typed
|
|
`plan-feedback` source with its exact Turn and request identities, and
|
|
host-agnostic consumers locate it through the activity-core selector rather
|
|
than parsing an ID prefix. AgentGUI-generated Prompt identities are UUID v4 so
|
|
hosts may safely reuse them at a stricter shared-execution boundary; Activity
|
|
Core itself keeps accepting opaque caller-owned identities.
|
|
|
|
A create timeout does not negate the optimistic Tutti activation: the draft
|
|
and badge remain pending until a canonical active revision arrives or the
|
|
operation reaches a definitive failure/confirmation expiry. Revision-checked
|
|
updates own their follow-up reconcile command. Unrelated hydration cannot clear
|
|
an uncertain update unless a newer revision with the requested status is
|
|
semantic proof; a failed or inconclusive owned reconcile becomes a retryable
|
|
failed update instead of permanently blocking the composer.
|
|
|
|
Capability references describe historical submission provenance; they are not
|
|
current activation and are not a workflow state machine. AgentSessionEngine
|
|
normalizes the independent `TuttiModeActivation` read projection separately
|
|
from Session entities, and uses an engine-owned pending draft intent before a
|
|
Session exists. Tutti Mode Plan state is created only by an Agent CLI invocation
|
|
and lives in daemon-owned workspace workflow entities. Renderer code must not
|
|
infer provider Plan intent, Tutti activation, or a Tutti workflow from the
|
|
latest Turn, transcript ordering, arbitrary assistant text, or compatibility
|
|
markers. The schema-first event definitions remain the authority for realtime
|
|
updates; OpenAPI-generated HTTP DTOs stay confined to API projections.
|
|
|
|
Runtime acceptance is not itself a durable submit receipt. The API, Agent
|
|
service, and runtime adapter carry `ClientSubmitID` through typed inputs; submit
|
|
claims and durability decisions never recover it from diagnostics metadata.
|
|
After `Exec` reports acceptance, the Agent service calls the required
|
|
`RuntimeController.DurablyReportSubmitProvenance` method before accepting the
|
|
Tutti snapshot or submit claim. The runtime reporter FIFO first persists the
|
|
ordinary submitted Turn, then executes that uncoalesced barrier. The barrier
|
|
atomically writes the enriched Session projection and a stable
|
|
`client-submit:user:<clientSubmitId>` user message that references the exact
|
|
canonical Turn; it requires the Turn to exist and never replays or regresses
|
|
its lifecycle. The service accepts a submit claim only after
|
|
`FindTurnByClientSubmitID` reads that same Turn back. Runtime hosts must provide
|
|
the required `DurableActivityReporter`; plain `ActivityReporter` compatibility
|
|
adapters do not satisfy this contract because state and message writes may be
|
|
separate transactions. Reporter decorators preserve the required interface by
|
|
construction instead of probing and forwarding an optional capability.
|
|
|
|
Source-session deletion is coordinated by the Tutti Mode Plan service, not by
|
|
the workspace data store. `DeleteSession`, `DeleteSessionsBatch`, and
|
|
`ClearSessions` delegate to that use-case boundary in production. The service
|
|
chooses which workflow, checkpoint, and operation states are cancellable and
|
|
supplies the actor, reason, target states, and transition time. The workspace
|
|
store only executes that explicit command, atomically removing the Agent
|
|
Session closure and Tutti activation/Turn snapshots while applying the
|
|
authorized workflow transitions. A persistence-only deletion must never infer
|
|
or repeat the policy.
|
|
|
|
The transaction result reports every removed Session and each affected
|
|
workflow/checkpoint identity. After commit, the Tutti Mode Plan service emits
|
|
the canonical workflow invalidations and the Agent service emits one
|
|
`session_deleted` invalidation per reported Session. The activity projection
|
|
does not perform a second deletion on that path. Persistence-only deleters and
|
|
standalone activation cleanup remain test/legacy-orphan fallbacks only; normal
|
|
daemon composition wires the coordinator so retries cannot split ownership or
|
|
publish duplicate deletion events.
|
|
|
|
Before a session exists, composer options carry the same typed capability
|
|
descriptor. The active session descriptor takes precedence once available.
|
|
For an existing active Turn, a null Session capability snapshot is unresolved
|
|
admission input rather than a negative capability result. Activity Core keeps
|
|
an accepted send-now prompt in its queue and resolves the pending delivery
|
|
decision when the complete Session snapshot arrives. Guidance and interruption
|
|
remain exact capability-driven paths; a complete snapshot supporting neither
|
|
degrades only the delivery mode to ordinary queued send and never drops the
|
|
accepted prompt. Deferred decisions are keyed by prompt and bound to the active
|
|
Turn observed at admission; they never steer or cancel a successor Turn.
|
|
Model composer options keep the selected value and provider-resolved value as
|
|
separate facts. An inherited `default` remains the selected value used for
|
|
future mutations; `effectiveModel` is presentation-only runtime evidence for
|
|
describing what Default currently resolves to. Activity adapters and AgentGUI
|
|
must not replace the selection with that resolved value or infer it from the
|
|
catalog's Default entry.
|
|
Optional model `consumptionMultiplier` metadata is likewise typed before it
|
|
reaches activity-core. AgentGUI may render that field but must not reconstruct
|
|
quota or cost semantics from provider-owned model descriptions.
|
|
An omitted pre-session descriptor means the connected daemon predates the
|
|
typed composer capability contract and must remain an unknown/loading state.
|
|
Core capability booleans must not be reconstructed from private
|
|
`runtimeContext` fields or represented as plugin/tool entries in the composer
|
|
capability catalog.
|
|
The activity snapshot also exposes the composer-options request lifecycle per
|
|
opaque target key and per independent section. `core` contains model,
|
|
reasoning, speed, permission, and effective-settings data; `capabilities`
|
|
contains skills, commands, and capability-catalog data. The provider's typed
|
|
slash-command policy belongs to `core`; a `capabilities` response that omits it
|
|
or projects it as null must not erase the last successful core value. Every
|
|
section merge updates only the fields owned by that section and preserves
|
|
fields owned by the other sections. Desktop requests
|
|
`core` for the model/reasoning/speed consumer and requests `capabilities` only
|
|
when a capability surface is opened or used; neither capability discovery nor
|
|
its eight-second provider timeout blocks the model controls. `full` remains the
|
|
compatibility request for callers that need the combined response. Consumers use
|
|
`loading` only for the initial
|
|
request when no cached options exist; background refreshes keep rendering the
|
|
last successful catalog, and failures transition to `error` instead of leaving
|
|
indefinite loading UI. Desktop and Mobile request options through the semantic
|
|
`AgentSessionEngine.loadComposerOptions` method. It owns request identity,
|
|
section-scoped signature-aware cache reuse, identical in-flight joining,
|
|
supersession, exact settlement, caller abort, and engine disposal. The host
|
|
extension adapter owns only the transport call and DTO mapping; hosts must not
|
|
reconstruct this protocol with raw `composerOptions/loadRequested` dispatch
|
|
plus snapshot subscriptions.
|
|
The daemon also persists the last successful credential-bound model catalog.
|
|
After restart it may serve that catalog as stale and refresh in the background;
|
|
the explicit model-picker-open request sets `waitForFreshModelCatalog` and waits
|
|
for the provider refresh before returning.
|
|
Provider context-window and quota updates enter the daemon at the runtime
|
|
adapter boundary, are split into typed durable session metadata, and reach
|
|
Agent GUI through the protocol-v2 `usage` field. GUI projections must not read
|
|
provider-private runtime context to render usage. Legacy Desktop-owned account
|
|
probes may enrich current account limits before or without a Session and keep
|
|
their provider adapters in Electron main. Agent Extension account probes have a
|
|
different ownership boundary: the signed Extension declares a provider-owned
|
|
helper, `tuttid` executes its independently installed companion, and Electron
|
|
main only validates and maps the provider-neutral result. Both paths project
|
|
only provider-neutral billing mode, quota windows, and stable error codes.
|
|
Credentials and raw provider responses must never enter AgentGUI, renderer IPC,
|
|
or logs. A
|
|
provider-owned access-token snapshot is not runtime authentication authority:
|
|
an account probe may retry a newer credential, but an unchanged expiry or
|
|
authorization rejection degrades only account limits. Existing session control
|
|
state is read from the daemon; pre-session edits remain in the engine-owned
|
|
activation/draft record until the daemon confirms the session.
|
|
`AgentHostWorkspaceAgent*` types may only appear in compatibility or projection
|
|
layers while the legacy Agent GUI internals are being migrated. Production read
|
|
paths must not call `workspaceAgents.list`,
|
|
`workspaceAgents.listSessionMessages`, `agentSessions.retainEventStream`, or
|
|
`agentSessions.subscribeEvents` directly. Production write paths must not call
|
|
`agentSessions.exec`, `agentSessions.cancel`,
|
|
`agentSessions.submitInteractive`, or `agentSessions.pinSession`; lifecycle
|
|
writes use the workspace `AgentSessionEngine`, while metadata actions still
|
|
exposed to AgentGUI use `AgentGUIRuntime`. Legacy host DTOs are allowlisted only in the
|
|
host API contract, explicit projection helpers, and message merge/page-loading
|
|
helpers that accept runtime-shaped adapters.
|
|
|
|
The desktop activity diagnostics module is the only narrow consumer allowed to
|
|
serialize legacy lifecycle fields while comparing old host events with the
|
|
canonical model. Those values are diagnostic evidence only and must never feed
|
|
session, turn, submit, or rendering decisions.
|
|
|
|
Slash command behavior is descriptor-authoritative. The provider catalog's
|
|
typed slash policy owns fallback commands and command effects; a missing policy
|
|
produces no provider slash commands or local command effects. Agent GUI must not
|
|
infer Cursor, Codex, Claude, or universal command behavior from provider names.
|
|
|
|
Runtime `provider` is open execution metadata. Agent GUI and Workbench must
|
|
preserve an unknown valid provider string and use `agentTargetId` for selection,
|
|
launch, grouping, and persisted composer state. They must not coerce an
|
|
extension provider to Codex. Verified Agent Extension presentation assets come
|
|
from the Agent Target contract; renderer packages do not add extension-specific
|
|
icon catalogs or provider branches.
|
|
|
|
The synthesized `plan-implementation` / `implement` decision crosses the
|
|
desktop boundary as one semantic, turn-and-request-scoped daemon command with
|
|
a caller-stable idempotency key. Desktop transport must not expand that command
|
|
into local settings or send operations. `tuttid` prepares a leased
|
|
`plan_decision` operation, checkpoints the idempotent plan-mode target write,
|
|
persists `send_dispatched` before provider execution, and confirms the result
|
|
only from a different durable turn/message carrying the operation's stable
|
|
`clientSubmitId`; an unknown send result is never blindly replayed. Completion
|
|
and its outbox event commit atomically. The `send_dispatched` checkpoint also
|
|
persists a session-level `agent_system_notice` with notice kind
|
|
`plan_implementation_pending_confirmation` and its message-update outbox event
|
|
in the same transaction, so an open client can observe the unknown window even
|
|
if the provider call hangs or the process exits. Completion upgrades the same
|
|
message to `plan_implementation_completed`, and its outbox publishes both the
|
|
confirmed turn and notice update. These payloads contain semantic IDs only;
|
|
user-visible copy belongs to consumer i18n. Provider-originated exit-plan
|
|
prompts remain ordinary durable interaction responses and use the existing
|
|
`interactive_response` operation rather than this synthetic-plan endpoint.
|
|
Until the user confirms or dismisses the plan, AgentGUI projects that wait
|
|
through one shared awaiting predicate into conversation-rail
|
|
`needsUserAction`, conversation-list awaiting sets, and Message Center
|
|
attention; it must not invent an active-session-only overlay for the same fact.
|
|
|
|
Provider interaction lifecycle is an explicit entity stream, independent of
|
|
transcript projection and runtime session snapshots:
|
|
|
|
```text
|
|
provider request
|
|
-> interaction.requested
|
|
-> runtime state report InteractionTransition(pending)
|
|
-> durable Interaction(pending)
|
|
-> interaction_update
|
|
-> AgentSessionEngine selectors
|
|
```
|
|
|
|
`call.started` / `call.completed` / `call.failed` continue to own historical
|
|
tool-call messages, but they never create or restore an actionable Interaction.
|
|
When acknowledging an interactive response can make the provider immediately
|
|
settle its Turn, the adapter must serialize that acknowledgement and the
|
|
matching call-resolution event with Turn finalization. The terminal Turn event
|
|
must not overtake `call.completed` or `call.failed` and close the event stream
|
|
before the historical call row resolves.
|
|
Likewise, a runtime session snapshot may describe provider-local execution
|
|
state but must not enrich a report with an Interaction transition. Runtime
|
|
reports may submit only `pending` and `superseded`; `answered` belongs solely to
|
|
the durable `interactive_response` operation. Preparing that operation
|
|
atomically claims the interaction with a `pending` to `answered` transition and
|
|
stores the requested action, option, and payload. Completion still records the
|
|
typed runtime disposition (`pending`, `resolving`, `answered`, `superseded`, or
|
|
`interrupted`) and commits the completed operation and outbox event. Competing
|
|
responders compare against the claimed output and normalize to `answered` or
|
|
`superseded`; absence from an in-memory request map is not evidence of success.
|
|
The claim's provisional Interaction status must not overwrite the terminal
|
|
runtime disposition: the completed operation result follows the runtime even
|
|
when the claim already moved the Interaction to `answered`.
|
|
|
|
Activity compatibility projection uses a causal, segmented write barrier. It
|
|
scans state patches in provider order. Non-terminal state first creates the
|
|
session, Turn, and Interaction required by message foreign keys; immediately
|
|
before each terminal patch, it flushes that Turn's messages and the session
|
|
audits, then commits the terminal state. Later non-terminal patches are never
|
|
moved ahead of an earlier settlement. A completed root-provider transition is
|
|
also terminal because, when no child remains active, SQLite uses it to settle
|
|
the canonical root Turn. During the first SQLite settlement transaction, the
|
|
store selects the latest already persisted assistant text message for that Turn
|
|
and freezes its ID in the existing completed-command payload. A same-report
|
|
anchor is only a validated fast path; cross-report provider event batches
|
|
derive the same watermark from durable messages. The payload also records an
|
|
explicit resolution marker when settlement found no assistant text, so a late
|
|
message cannot become the result of an already settled Turn. Result readers use
|
|
the exact frozen message, return no message for a resolved-empty watermark, and
|
|
reserve the bounded fallback scan for legacy turns without resolution metadata.
|
|
|
|
Root-provider settlement notifications are a separate compatibility path
|
|
because provider adapters do not emit the legacy terminal Turn state patch.
|
|
Production composition must register every session-state observer through
|
|
`ConfigureSessionStateObservers` and explicitly choose whether it observes
|
|
canonical root-Turn settlements. An omitted choice is a configuration error.
|
|
Settlement delivery is at-least-once, so opted-in consumers must use the exact
|
|
canonical Turn ID for durable deduplication or otherwise serialize an
|
|
idempotent terminal transition. Choosing to ignore settlements requires an
|
|
explicit lifecycle ruling for that consumer.
|
|
|
|
Cancellation of the caller waiting on an interactive-response operation is not
|
|
a provider outcome and must not terminalize the runtime request. Before a
|
|
response is dispatched it remains `pending` for durable retry; after dispatch it
|
|
remains `resolving` until the provider response transport reports success,
|
|
failure, or an explicit provider-side interruption.
|
|
|
|
Runtime request identity is the full
|
|
`(workspace/session, turnId, requestId)` tuple. The turn ID must cross the
|
|
coordinator, runtime controller, provider adapter, live request registry, and
|
|
disposition lookup; a request ID alone is never sufficient because providers
|
|
may reuse it in a later turn. Live registries contain only `pending` and
|
|
`resolving` requests. The first terminal disposition is copied to a bounded
|
|
tombstone registry before the live request or provider session is removed, so a
|
|
durable retry can still distinguish `answered`, `superseded`, and `interrupted`
|
|
from `unknown`. Provider command transports that expose an acknowledgment (for
|
|
example a sidecar `ok`/`error` response) must consume it before reporting
|
|
success; writing bytes to the transport is not acceptance. A missing
|
|
acknowledgment is not an explicit provider rejection: Claude SDK interactive
|
|
submissions remain `resolving` while the daemon queries the sidecar's bounded,
|
|
idempotent disposition registry by `(turnId, requestId)`. Only an authoritative
|
|
`answered` or `superseded` result may terminalize the request; an identical
|
|
answered replay is accepted without resolving the provider promise twice, and
|
|
a changed replay is a conflict. A disposition-query error remains `resolving`,
|
|
while an authoritative `pending` result releases the claim back to `pending` so
|
|
the durable operation can retry. Once the provider session itself is confirmed
|
|
dead, both pending and resolving requests become `superseded` because they are
|
|
no longer actionable; preserving an exact applied result across process death
|
|
would require a persistent provider-side journal rather than an in-memory
|
|
tombstone. Provider session cleanup first detaches the exact adapter-session
|
|
object under the registry lock and only then terminalizes its pending requests
|
|
outside the lock, so a stale reader or close path cannot delete a concurrently
|
|
installed replacement session. Resume rollback restores a previous session
|
|
only when no replacement is current and the previous session has not been
|
|
marked failed or closed.
|
|
|
|
Interaction persistence returns `applied`, `already_applied`, or `conflict`.
|
|
Exact replays and late transitions after the first terminal state are
|
|
`already_applied`; a changed immutable identity (`kind`, `toolName`, `input`, or
|
|
`metadata`) is a hard `conflict` for the whole state report. A terminal state
|
|
never transitions back to `pending`. A settled owning Turn cannot acquire a new
|
|
pending Interaction: persistence treats that late provider report as an
|
|
idempotent stale transition and stores no actionable row. Terminal reports may
|
|
still be recorded for replay and reconciliation evidence.
|
|
|
|
Protocol-v2 session responses expose `activeTurnId` (required and nullable),
|
|
`pendingInteractions` (required and never null), independent `activeTurn` /
|
|
`latestTurn` projections, typed capabilities/usage/goal/import and child-session
|
|
fields, and Unix-millisecond timestamps. They do not expose legacy session
|
|
status, turn lifecycle, submit availability, last error, ISO timestamps, or
|
|
the raw runtime context. SQLite migrations split typed session metadata from
|
|
provider-private recovery context, remove the legacy status/current-phase/
|
|
last-error/runtime-context columns, and enforce nullable exact
|
|
`active_turn_id` ownership plus Turn/Interaction/message foreign keys. Public
|
|
activity events are version 2: full Turn and Interaction entities use
|
|
`turn_update`/`interaction_update`; a session invalidation that requires an
|
|
authoritative read is explicitly named `session_reconcile_required` and must
|
|
never be applied as a partial Session entity. The old public `state_patch` and
|
|
storage message row id are removed.
|
|
|
|
Message `turnId` is explicitly nullable. Runtime execution messages use the
|
|
exact durable Turn id. An external transcript importer may reconstruct stable
|
|
historical Turn ids only from trustworthy provider evidence: each retained real
|
|
user message starts one Turn and the following assistant/tool messages keep
|
|
that identity until the next retained user message. Persistence creates those
|
|
Turns as settled backfills in the same transaction as their messages. A
|
|
forward-only store migration repairs legacy imported turnless rows from the
|
|
same retained-user boundary; re-import applies the same stable identity.
|
|
Content before the first trustworthy boundary stays session-scoped
|
|
(`turnId = null`). AgentGUI never reconstructs canonical Turn ownership from
|
|
the currently loaded page, and import must not manufacture one Turn per
|
|
transcript message.
|
|
|
|
It should not know how a host connects to `tuttid`, subscribes to the
|
|
business-event WebSocket, resolves workspace paths, or talks to Electron.
|
|
|
|
### `apps/desktop`
|
|
|
|
The desktop app owns the concrete adapter from `tuttid` and Electron runtime
|
|
capabilities into `agent-activity-core`.
|
|
|
|
It owns:
|
|
|
|
- `tuttid` client calls
|
|
- business-event WebSocket connection implementation
|
|
- backend base URL and authentication details
|
|
- preload/runtime/file adapters
|
|
- `IWorkspaceAgentActivityService` and the desktop `AgentGUIRuntime` wrapper
|
|
- workspace chrome placement
|
|
- workbench contribution wiring
|
|
- desktop i18n overrides
|
|
|
|
`WorkspaceAgentActivityService` is the desktop renderer source for workspace
|
|
agent activity snapshots. Desktop chrome MessageCenter and AgentGUI workbench
|
|
nodes must subscribe to the same service instance for the same workspace.
|
|
|
|
## Core Engine And Adapter Shape
|
|
|
|
The host creates one engine for each workspace and runtime origin and supplies
|
|
its external command port. The adapter remains a transport boundary owned by
|
|
the host; it is not another state owner:
|
|
|
|
```ts
|
|
createAgentSessionEngine({
|
|
identity: { workspaceId, origin },
|
|
clock,
|
|
scheduler,
|
|
commandPort
|
|
});
|
|
```
|
|
|
|
`plan/submitDecision` uses the dedicated
|
|
`EngineTypedCommandPort.executePlanDecision` method. Its public
|
|
`PlanSubmitDecisionResult` contains the durable operation identity returned by
|
|
the Host. It must not pass through the generic `execute(): Promise<unknown>`
|
|
path or manufacture an operation id from a command id.
|
|
|
|
Durable daemon message pages and `message_update` payloads use
|
|
`AgentActivityDurableMessage`, whose immutable `sequence` is required. Local
|
|
optimistic and session-audit projections may use the separate transient
|
|
message shape; this must not weaken the durable page or realtime contract.
|
|
The generated daemon Session DTO also requires `messageVersion`. The shared
|
|
`@tutti-os/agent-activity-tuttid-adapter` validates and preserves that high-water
|
|
cursor for Desktop and Mobile; consumers must not replace a missing value with
|
|
zero or add an old-daemon fallback because both binaries upgrade together.
|
|
Every HTTP field in this cursor domain (`messageVersion`, message `version`,
|
|
page `latestVersion`, and `afterVersion`/`beforeVersion`) is bounded by
|
|
JavaScript's maximum safe integer. Durable message `sequence` has the same
|
|
transport bound. Store and service layers retain `uint64`; the daemon API owns
|
|
checked conversion and must fail response projection instead of emitting an
|
|
inexact JSON number.
|
|
|
|
### Ephemeral conversation projection
|
|
|
|
`agent-activity-core` also exports
|
|
`createAgentActivityEphemeralConversationProjector` for provider-neutral,
|
|
surface-owned lanes such as live Side. It is a small React-free projector, not
|
|
a workspace `AgentSessionEngine`: callers seed one exact ephemeral identity,
|
|
normalize raw provider events at their adapter boundary, and receive the
|
|
standard snapshot, Turn, and Interaction vocabulary needed by shared
|
|
conversation projections.
|
|
|
|
The projector enforces monotonic sequence and exact
|
|
`(workspaceId, agentSessionId, sourceAgentSessionId)` identity. Identity
|
|
mismatch, a forward sequence gap, or a terminal Session patch expires the
|
|
projection. It has no adapter, timers, pagination, persistence, authoritative
|
|
reconcile, or fallback to a durable Session. Its caller owns transport
|
|
subscription and cleanup.
|
|
|
|
The adapter exposes the HTTP operations used by that command port and by the
|
|
desktop reconcile bridge:
|
|
|
|
```ts
|
|
export interface AgentActivityAdapter {
|
|
listSessions(input: {
|
|
workspaceId: string;
|
|
signal?: AbortSignal;
|
|
}): Promise<AgentActivitySessionList>;
|
|
|
|
listSessionMessages(input: {
|
|
workspaceId: string;
|
|
agentSessionId: string;
|
|
afterVersion?: number;
|
|
beforeVersion?: number;
|
|
limit?: number;
|
|
order?: AgentActivityMessageOrder;
|
|
signal?: AbortSignal;
|
|
}): Promise<AgentActivityMessagePage>;
|
|
|
|
loadComposerOptions(
|
|
input: AgentActivityLoadComposerOptionsInput
|
|
): Promise<AgentActivityComposerOptions>;
|
|
|
|
createSession(
|
|
input: AgentActivityCreateSessionInput
|
|
): Promise<AgentActivitySession>;
|
|
sendInput(
|
|
input: AgentActivitySendInput
|
|
): Promise<AgentActivitySendInputResult>;
|
|
goalControl(
|
|
input: AgentActivityGoalControlInput
|
|
): Promise<AgentActivityGoalControlResult>;
|
|
submitInteractive(
|
|
input: AgentActivitySubmitInteractiveInput
|
|
): Promise<AgentActivitySubmitInteractiveResult>;
|
|
deleteSession(
|
|
input: AgentActivityDeleteSessionInput
|
|
): Promise<AgentActivityDeleteSessionResult>;
|
|
deleteSessions(
|
|
input: AgentActivityDeleteSessionsInput
|
|
): Promise<AgentActivityDeleteSessionsResult>;
|
|
renameSession(
|
|
input: AgentActivityRenameSessionInput
|
|
): Promise<AgentActivitySession>;
|
|
setSessionPinned(
|
|
input: AgentActivitySetSessionPinnedInput
|
|
): Promise<AgentActivitySession>;
|
|
}
|
|
```
|
|
|
|
`AgentActivitySendInputResult` contains the authoritative canonical `turn` in
|
|
addition to its session and turn id. Desktop adapters must reject a successful
|
|
transport response that omits that turn; they must not reconstruct it from the
|
|
deprecated session-level lifecycle or submit-availability fields.
|
|
|
|
`AgentActivitySendInput.targetTurnId` is the exact canonical active Turn target
|
|
for guidance. AgentGUI captures it from the active Turn at the interaction
|
|
boundary; Activity Core preserves it through queued intents (and captures the
|
|
current active id when promoting an existing queued prompt to send-now) and
|
|
emits it only on the guidance request. The Tutti adapter maps it to the daemon's
|
|
`turnId`; ordinary new-Turn submits clear it. The daemon service, Host, and
|
|
runtime Controller fail closed when guidance has no target or when the target
|
|
is no longer active. That rejection is known `NotDispatched` provider
|
|
delivery, so no provider call occurs and any prepared submit claim is cleaned
|
|
up. A caller must not recover a target by reading a newer Session snapshot.
|
|
|
|
`AgentSessionActivateEffectInput` requires `agentTargetId` for
|
|
`mode: "new"`. Shared UI passes it through unchanged; trusted host or daemon code
|
|
resolves it against `agent_targets`, validates enabled state and launch ref
|
|
shape, and derives the execution `provider` and runtime `providerTargetRef`
|
|
from the resolved target. Target-backed create requests may omit `provider`; if
|
|
both fields are present, the daemon rejects provider mismatches. Client-provided
|
|
`providerTargetRef` is not allowed to override the daemon-derived runtime ref
|
|
when `agentTargetId` is present. The resulting
|
|
`AgentActivitySession` and session events should preserve `agentTargetId` when
|
|
present. State patch reducers must update the session when an event includes
|
|
`agentTargetId`, but a patch that omits the field must not clear an existing
|
|
target id because older runtimes and historical imports are provider-only.
|
|
|
|
Composer options use one cache key space: the resolved `agentTargetId` is passed
|
|
to activity-core as an opaque `targetKey`, round-tripped verbatim, and forwarded
|
|
to the daemon as `agentTargetId`. Activity-core must not parse or rewrite the
|
|
key. There is no provider-keyed fallback cache: two targets under the same
|
|
provider remain isolated. Provider-based invalidation filters on the provider
|
|
recorded for the active or most recent request rather than deriving provider
|
|
identity from the key or from possibly stale cached options. Invalidation
|
|
clears cache validity but must not detach an in-flight command from its caller:
|
|
that caller still receives a terminal result, and the next request performs a
|
|
fresh load.
|
|
While a live session refreshes its catalog, UI may continue presenting an
|
|
already loaded target snapshot, but a genuinely missing target snapshot remains
|
|
loading until target-scoped options arrive.
|
|
|
|
Each composer-options snapshot also carries its effective pre-session settings;
|
|
AgentGUI resolves displayed settings field by field in this order:
|
|
authoritative session settings, optimistic first-create settings, preloaded
|
|
effective settings, then home defaults. A partial session projection must not
|
|
erase a usable preloaded model or reasoning selection while live metadata is
|
|
still arriving. Because the effective settings are request-dependent,
|
|
composer-options cache freshness and in-flight reuse include normalized `cwd`
|
|
and normalized requested settings in addition to the target key.
|
|
Ordinary home-target and project switches use that signature-aware cache and
|
|
must not force a second transport request from the click handler. Forced loads
|
|
are reserved for explicit catalog invalidation, activation/creation settlement,
|
|
provider-declared draft-session prewarming, or a validated settings result whose
|
|
current target options declare `refreshModelOptionsAfterSettings`. The Engine
|
|
uses the authoritative returned Session to issue that target-scoped refresh;
|
|
Desktop and Mobile adapters must not upsert the Session or choose reload policy
|
|
themselves. Options refresh is independent of the current prompt continuation
|
|
and never blocks its send.
|
|
|
|
Composer-options loading may be suppressed while a new-session activation is
|
|
pending, but that guard follows the current engine state rather than a
|
|
mount-time snapshot. The transition from creating to settled must trigger a
|
|
fresh target-scoped load so model, reasoning, skill, and slash-command metadata
|
|
cannot remain absent for the lifetime of the node. Before the first
|
|
target-scoped composer-options snapshot arrives, configurable-setting support is
|
|
unknown rather than unsupported: the composer footer renders disabled loading
|
|
controls for permission and model/reasoning selection, then replaces or removes
|
|
them according to the authoritative snapshot. Slash command fallback and effect
|
|
policy remain provider-descriptor-owned; every supported provider that exposes
|
|
local fallback commands declares them in its registry descriptor.
|
|
|
|
`AgentActivityCreateSessionInput.providerTargetRef` is an optional opaque
|
|
host-owned legacy reference for selecting which target under the real provider
|
|
should launch the session. It is not authority, a credential, or an invocation
|
|
plan. New runtime launches must provide `agentTargetId`; `providerTargetRef`
|
|
must not be used as a provider-only launch fallback. Target-backed launches use
|
|
the daemon-derived ref shape from `agent_targets` instead. Adapters and trusted
|
|
launchers must re-authenticate and resolve it before using any concrete provider
|
|
invocation. UI packages must keep `provider` as the real provider identity and
|
|
must not synthesize providers for shared or remote targets.
|
|
|
|
The desktop service owns one business-event WebSocket at `/v1/events/ws`.
|
|
Canonical `message_update` snapshots remain the hydration, terminal
|
|
confirmation, and cloud-reconcile contract. During normalized provider
|
|
text/reasoning streaming, and for provider events that explicitly carry
|
|
ordered appendable tool output, the same `agent.activity.updated` topic carries
|
|
schema-backed `message_delta` events produced by the provider normalizer; the
|
|
transport must never derive deltas by comparing snapshots. The runtime
|
|
publishes each delta to the business-event bridge before its per-Session
|
|
runtime fan-out and before enqueueing the durable report, so a later committed
|
|
terminal confirmation cannot overtake the optimistic prefix. The post-commit
|
|
projection suppresses only a redundant nonterminal text/reasoning snapshot or
|
|
running tool-output snapshot already represented by that delta. Tool anchors,
|
|
structured tool mutations, audit, imported, unprojected, and terminal updates
|
|
retain their existing canonical publication semantics. Every `message_delta`
|
|
is scoped to a real persisted Turn; session-level notices continue to use
|
|
explicit audit or state semantics.
|
|
|
|
Provider normalizers must retain enough per-message state to preserve semantic
|
|
operations. The first cumulative text/reasoning snapshot uses `set`; a later
|
|
snapshot with the previous value as an exact prefix uses `append_text` with only
|
|
the suffix. Duplicate snapshots are dropped, while a rewrite or backtrack that
|
|
cannot prove the prefix relation uses `set`. Transport and renderer layers must
|
|
not rediscover this relationship by diffing full snapshots.
|
|
|
|
Tool output uses a separate `toolOutput` operation that mutates only the
|
|
provider-neutral `payload.output.text` display projection. A provider adapter
|
|
may emit it only from an explicit ordered output-delta event, or from a
|
|
cumulative textual output snapshot whose exact prefix relationship the adapter
|
|
has verified. It must not classify by tool name or inspect arbitrary structured
|
|
tool results. Every output operation uses the exact `messageId` of the
|
|
canonical tool-call anchor; provider-normalizer event ids are internal
|
|
lifecycle identities and must not create a second optimistic row. The first
|
|
output uses `set`; later `append_text` operations carry the prior UTF-8 byte
|
|
length as `offsetBytes`. A missing anchor or offset mismatch triggers canonical
|
|
reconciliation instead of guessed concatenation. When an explicit provider
|
|
output notification races immediately ahead of its `item/started`, the
|
|
provider normalizer may retain that prefix in a bounded pre-anchor buffer, but
|
|
it must emit nothing until the real anchor arrives; it then publishes the
|
|
anchor first and the retained prefix as `set`. Output beyond the canonical
|
|
1 MiB field budget becomes a valid UTF-8 prefix plus `[Output truncated]`; the
|
|
live projection expresses that bounded snapshot as one `set` followed by
|
|
contiguous `append_text` operations that fit the transport envelope. An
|
|
unmatched prefix, or one rejected by the aggregate pre-anchor call or byte
|
|
budget, is dropped with diagnostics rather than inventing a tool row.
|
|
Completed, failed, canceled, and rewritten tool results remain authoritative,
|
|
bounded canonical `message_update` snapshots.
|
|
|
|
Durable tool snapshots contain the business projection, not a provider-result
|
|
archive. Before `workspace_agent_messages.payload_json` is written, the
|
|
canonical projection promotes readable text to `output.text` or `error.text`,
|
|
tool references to `output.matches`, generated-image paths to
|
|
`output.savedPath`/`output.savedPaths`, and file mutations to `fileChanges`.
|
|
Provider envelopes and duplicate representations such as top-level `content`,
|
|
`output.content`, `rawInput`/`rawOutput`, Claude `toolResponse`, adapter
|
|
metadata, image base64, and unknown provider-only result keys are not retained.
|
|
Provider adapters may continue accepting those wire shapes, but shared
|
|
business code consumes only the explicit canonical fields. Each canonical
|
|
`output` or `error` `text`, `stdout`, and `stderr` field, including nested tool
|
|
steps, first receives a 1 MiB per-field bound by retaining a valid UTF-8 prefix
|
|
and the fixed `[Output truncated]` marker. Durable tool-call payloads then fit
|
|
those formal output streams into a 768 KiB aggregate JSON budget, leaving
|
|
deterministic headroom below the 1 MiB replication request limit for mutation
|
|
identity, scope, and batch framing. Inputs and structured results are never
|
|
discarded merely to satisfy that output budget.
|
|
|
|
A terminal command snapshot omits `text` only when it is exactly
|
|
reconstructible as `strings.TrimSpace(stdout)` or
|
|
`strings.TrimSpace(stderr)`; the raw stream remains canonical. The Reporter
|
|
recognizes that alias before per-field truncation and emits a tombstone so a
|
|
terminal snapshot also clears any `text` retained from its running snapshot.
|
|
Nested steps use their own lifecycle status, even beneath a running parent.
|
|
Explicit non-command tool identity wins over command-shaped input, so MCP and
|
|
other non-command tools retain `text` as their provider-neutral display
|
|
contract. Running command output retains `text` for ordered live deltas. Agent
|
|
reference/session output uses the same canonical projection rather than
|
|
depending on a retained raw tool result.
|
|
|
|
Canonical compaction is normally a forward-write rule. One bounded migration
|
|
also repairs retained terminal command rows, including deleted/tombstoned rows
|
|
that remain replication inputs, whose payload reaches the 768 KiB safe budget.
|
|
It removes only reconstructible aliases and refits formal output streams without
|
|
changing message versions or timestamps. Rows whose structured data cannot fit
|
|
without semantic loss remain unchanged for explicit handling.
|
|
|
|
The Go live-protocol adapter owns the complete fast-lane envelope on both sides
|
|
of a device link: schema validation, recipient identity projection,
|
|
protobuf-wire framing, batching, replay/resume, sequence-gap detection, and
|
|
typed rejection. Recipient projection rewrites only the closed workspace,
|
|
Session, and Turn identity fields. Opaque business values remain
|
|
`json.RawMessage`, including file paths and large JSON integers, so transport
|
|
does not sanitize or reinterpret renderer data. The exact protocol revision
|
|
covers the event schema, protobuf field numbers, delivery-kind values, and JSON
|
|
control shapes. A revision mismatch is an explicit rejection followed by
|
|
canonical reconciliation; it is not a compatibility conversion path.
|
|
|
|
`StreamReady` is transport-only and must not be interpreted as canonical
|
|
catch-up. `AttachmentChanged` starts a baseline for one positive attachment
|
|
revision; hosts publish their canonical baseline and then
|
|
`AttachmentCaughtUp` with the exact same binding, workspace, Session,
|
|
authorization set, explicit `currentInteractionRootTurnId`, caller-Turn, and
|
|
revision identity. `canonicalTurnIds` is an authorization set, never an ordered
|
|
current-Turn pointer. Every complete `interaction_snapshot` carries its
|
|
required exact `rootTurnId`, including an empty collection. Consumers reject a
|
|
snapshot whose root does not resolve to the controlled root and reject a
|
|
missing or mismatched barrier. The protocol carries this fence but does not
|
|
choose recovery state: the host adapter must reread its canonical store, which
|
|
remains the lifecycle authority after a runtime or host-process restart.
|
|
|
|
Replay resumes the same epoch, so replayed attachment or caught-up controls may
|
|
precede the replacement RPC's newly emitted `StreamReady`. Consumers persist
|
|
the attachment projection together with the resume cursor. If disconnection
|
|
occurred after `AttachmentChanged` but before `AttachmentCaughtUp`, the
|
|
projection remains explicitly not caught up until the matching barrier is
|
|
replayed. A host must not publish `AttachmentCaughtUp` for a replacement
|
|
attachment until it has rerun the complete canonical baseline.
|
|
|
|
Frames are bounded but not fragmented. Before publication, a canonical
|
|
tool-output operation that would consume the delivery budget is represented as
|
|
one `set` followed by contiguous `append_text` operations; this reuses the
|
|
existing semantic operation contract rather than introducing transport-frame
|
|
fragment assembly. The publisher may coalesce only adjacent pure `append_text`
|
|
operations for the same message and Turn; tool-output operations additionally
|
|
require contiguous byte offsets. Status, payload, semantic, or lifecycle
|
|
mutations remain separate deliveries. A single delivery over the configured
|
|
safe limit (1 MiB by default, with a 2 MiB encoded-frame ceiling and an 8 MiB
|
|
replay-byte budget) is replaced with a `delivery_too_large` discontinuity
|
|
carrying reconcile keys, and the caller falls back to canonical data. This
|
|
ensures that the final oversized event is not silently lost.
|
|
|
|
The publisher also keeps a bounded FIFO of the most recent settled Turn ids.
|
|
Those fences convert late text or tool deltas into scoped discontinuities
|
|
without allowing a long-lived stream to accumulate one permanent map entry per
|
|
historical Turn. The retention bound is independent of canonical history:
|
|
evicted Turns remain durable, and the activity-core overlay independently
|
|
rejects a nonterminal delta against known terminal message truth so any later
|
|
uncertainty still converges through authoritative reconciliation.
|
|
|
|
A host creates one activity-core workspace event coordinator per Engine. The
|
|
coordinator materializes accepted deltas in its optimistic overlay, projects
|
|
that overlay over the latest canonical message base, applies continuous inline
|
|
messages, owns Session deletion tombstones, admits only explicit
|
|
`session_restored` events through those tombstones, and schedules authoritative
|
|
reconciliation after a restore, gap, discontinuity, recovered connection,
|
|
invalid payload, or unanchored append. The Tutti desktop receives local deltas
|
|
through the business-event WebSocket; shared-device hosts receive the same live
|
|
subset through the framed Go protocol. UI consumers never retain transport
|
|
epoch/sequence state or distinguish local from shared activity sources.
|
|
|
|
For Personal paired devices, `services/tuttid/service/mobileremote` owns the
|
|
`agent_live` application-stream adapter. It subscribes to
|
|
`agent.activity.updated` with the requested workspace scope, projects only the
|
|
closed live event variants into `liveprotocol.Publisher`, converts canonical
|
|
message/reconcile variants into scoped discontinuities, and preserves an
|
|
explicit `session_deleted` or `session_restored` reason plus Session reconcile
|
|
key for the Mobile adapter to normalize into the shared coordinator's lifecycle
|
|
path. Those semantic reasons participate in
|
|
`AGENT_ACTIVITY_LIVE_PROTOCOL_REVISION`; mismatched builds are rejected instead
|
|
of silently degrading deletion or restore into an ordinary reconcile. The
|
|
adapter establishes the workspace subscription before publishing
|
|
`stream_ready`, so events produced during ready-frame delivery are already
|
|
buffered instead of falling through a subscribe gap. The Android
|
|
bridge keeps one long-lived DeviceLink stream and delegates frame decoding and
|
|
continuity checks to the Agent-owned Go mobile Subscriber before emitting
|
|
accepted deliveries to React Native. Each bridge subscription also carries a
|
|
caller-owned local generation that is independent of the wire epoch and
|
|
sequence cursor. Native stops the live stream immediately when the App enters
|
|
the background, and both the Native-to-JS adapter and workspace live lane reject
|
|
queued deliveries from a closed generation after foreground resume. The
|
|
underlying DeviceLink may remain open for its background grace interval. The
|
|
Mobile Android host co-links that Subscriber and DeviceLink into its own
|
|
composite AAR; the transport package's AAR and Java namespace remain Agent-free.
|
|
Mobile disables its message and Rail pollers after `stream_ready`; those pollers
|
|
are disconnected-transport fallback only.
|
|
|
|
After either transport is normalized, Desktop and Mobile call the same
|
|
`AgentActivityWorkspaceEventCoordinator`. Its package-internal rules derive
|
|
inline messages, validate envelope/data/message identity, require every
|
|
advertised message to parse, check the advertised count and latest-version
|
|
cursor, and validate version continuity. Any disagreement fails closed to
|
|
authoritative reconciliation. The coordinator emits Engine intents and owns
|
|
the optimistic projection, rather than exporting those leaf mechanisms for
|
|
hosts to assemble independently. Mobile does not widen its four-variant framed
|
|
live protocol to mirror Desktop: canonical `message_update` and
|
|
`session_reconcile_required` events converge through scoped discontinuities,
|
|
while `session_deleted` and `session_restored` retain typed lifecycle semantics
|
|
across the adapter.
|
|
|
|
Desktop and Mobile also call the same
|
|
`AgentActivitySessionReconcileExecutor` for authoritative Session reads. The
|
|
executor accepts only mapped activity-core detail aggregates and message pages;
|
|
it owns the three reconcile scopes, cancellation and deletion fences,
|
|
conversation-versus-durable cursors, pagination, the two-detail race closure,
|
|
and atomic Engine dispatch. A host selects either requested-Session or
|
|
Session-hierarchy message hydration according to the transcript surface it
|
|
renders. The executor labels its first combined read as `message_hydration`;
|
|
tuttid serves that projection without resolving provider-backed lifecycle
|
|
capabilities. The final read remains authoritative and resolves those
|
|
capabilities once. Hosts must preserve this distinction: caching provider
|
|
capabilities or removing the final race-closing read can return stale actions,
|
|
while using the full projection for discovery can launch an otherwise unused
|
|
provider capability probe. Each HTTP response echoes its projection and whether
|
|
lifecycle capability projection ran; clients fail closed on a mismatch, and
|
|
values from a deliberately unprojected hydration response must never clear
|
|
authoritative actions. Full projection remains fail-closed when a provider
|
|
probe fails, preserving existing detail availability while making the action
|
|
unavailable for that response. HTTP execution, generated DTO mapping,
|
|
absent/error interpretation, logging, polling, and legacy event fanout stay in
|
|
the host. The canonical detail aggregate type belongs to activity-core; the
|
|
tuttid adapter only maps the generated response into it.
|
|
|
|
Focused transcript paging is the adjacent AgentGUI application boundary.
|
|
Desktop and Mobile construct the same
|
|
`@tutti-os/agent-gui/conversation-message-controller`. It routes initial and
|
|
latest hydration through Engine reconcile intents, reads older history only
|
|
from the canonical Engine message window, fences in-flight pages when focus or
|
|
host availability changes, and dispatches accepted mapped pages into that
|
|
Engine. Mobile's disconnected poller and app lifecycle, Desktop diagnostics
|
|
and WebSocket integration, and both renderers' scroll behavior remain host
|
|
concerns. Hosts must not add a second older-message store or a host-local
|
|
cursor/retry state machine. Desktop conversation selection owns activation
|
|
guards and Rail projection coordination, then asks this controller to ensure
|
|
initial detail hydration. Automatic selection restoration is idempotent: it
|
|
must not reinterpret a repeated selection as a forced refresh or enqueue a
|
|
second reconcile while the first is pending. Explicit refresh remains a
|
|
separate command. Message paging adapters do not call back into selection or
|
|
Rail orchestration, and hosts do not maintain a second messages-only reconcile
|
|
entrypoint.
|
|
|
|
The focused conversation controller also owns the optional host synchronization
|
|
lease for its exact active Session. It acquires the lease when focus changes,
|
|
keeps repeated selection idempotent, and releases the previous lease on switch,
|
|
clear, or disposal. A host may use that lease to retain a per-Session event
|
|
stream; React surfaces must not retain that transport independently.
|
|
|
|
Event-stream continuity and command reachability are separate host facts.
|
|
`eventStreamConnectionChanged` belongs to the coordinator and triggers
|
|
authoritative reconnect hydration; `engine/connectionChanged` belongs to the
|
|
host command transport. Desktop's business-event WebSocket may drive both
|
|
because it shares the local service boundary. Mobile drives Engine connection
|
|
from application/service command reachability and coordinator connection from
|
|
`stream_ready`, disconnect, attachment rejection, and stream shutdown. Neither
|
|
signal may synthesize the other.
|
|
|
|
Realtime Turn provenance travels on the Engine-owned `session/reconcile`
|
|
command. If a live reconcile fails, the Engine retains that provenance for the
|
|
next exact retry. Both Desktop and Mobile preserve it on
|
|
`session/detailSnapshotReceived`, so an uncached completed Turn is replayed
|
|
after Session identity exists and produces the same attention/read semantics.
|
|
Hosts must not keep parallel “next reconcile is live” marker sets.
|
|
|
|
Hosts may accept older provider/runtime reports with missing transcript
|
|
ownership or ordering fields, but those gaps must be filled before events enter
|
|
`agent-activity-core` or `@tutti-os/agent-gui`. Session-level notices and
|
|
statuses should use canonical lifecycle events or explicit notice semantics;
|
|
they should not be published as ordinary assistant transcript messages without
|
|
a turn scope.
|
|
Activity reports may carry a host-defined user id before they reach the engine.
|
|
The local desktop adapter injects its stable local AgentGUI identity so
|
|
attention/read state has a deterministic partition without consulting account
|
|
login state. Cloud collaboration hosts may inject real account user ids so
|
|
downstream views can distinguish self-owned and peer-owned sessions. Identity
|
|
enrichment must use host-provided local state; it must not call account refresh
|
|
or user-info APIs that perform network round-trips or write refreshed auth
|
|
state.
|
|
|
|
## Event And Reconcile Lifecycle
|
|
|
|
Realtime transport lifecycle belongs to the host. Engine semantics define how
|
|
the normalized event is applied:
|
|
|
|
- keep one workspace event-stream subscription independent of mounted panels
|
|
- hydrate canonical `message_update` snapshots into the host-owned base
|
|
- materialize optimistic `message_delta` events before dispatching ordinary
|
|
messages into the engine; never replay an append operation log over a newer
|
|
canonical base
|
|
- scope every optimistic overlay operation by workspace and Session; a
|
|
successful authoritative message read clears nonterminal optimistic state for
|
|
that scope before accepting later deltas
|
|
- keep a newer optimistic terminal projection over a cloud nonterminal, and
|
|
clear it when canonical terminal truth arrives
|
|
- after a gap, discontinuity, or reconnect, complete an authoritative read and
|
|
reconcile the overlay; use an unconditional overlay reset only when removing
|
|
or rebinding the Session
|
|
- validate each `turn_update` envelope and dispatch one atomic Engine
|
|
projection that updates the Turn and the cached Session's `activeTurnId`
|
|
together; a settled Turn may clear only its own active reference, so delayed
|
|
events cannot clear a newer Turn
|
|
- after the host has fenced transport identity and ordering, it may explicitly
|
|
mark settlement of that same immutable Turn as absorbing when the cached
|
|
nonterminal projection belongs to another source version domain; unmarked
|
|
realtime projections and generic Turn and Session upserts retain their
|
|
canonical version guards
|
|
- use canonical Turn versions as the Engine-local fence against stale Session
|
|
snapshots; event-envelope `occurredAtUnixMs` is transport metadata and must
|
|
not advance canonical Session timestamps or participate in entity ordering
|
|
- reject inconsistent Turn projections without partially updating canonical
|
|
state, then converge through a state-only session pull
|
|
- reconcile `turn_update` and `interaction_update` through a session pull to
|
|
fill fields outside their realtime projections and hydrate an uncached
|
|
Session; the pull is not the consistency boundary for a cached Turn and
|
|
Session
|
|
- preserve whether a reconcile was realtime-triggered until its authoritative
|
|
session is applied; if the authoritative fetch fails, restore that provenance
|
|
for the retry rather than silently downgrading it to historical
|
|
- let the atomic realtime Turn projection drive attention immediately when
|
|
Session identity is cached, but only from the Turn accepted by canonical
|
|
lifecycle monotonicity; after hydrating an uncached Session, replay its latest
|
|
Turn with realtime provenance so attention can resolve identity
|
|
- apply historical list pulls through `session/snapshotReceived`, which never
|
|
creates a new unread completion
|
|
- let identity-dependent reducers observe both authoritative shapes: a pending
|
|
activation is confirmed by either `session/snapshotReceived` or
|
|
`session/upserted`, and message buckets are canonicalized as soon as either
|
|
shape reveals a provider-session alias
|
|
- when a session is removed, use its pre-removal identity to delete both the
|
|
canonical message bucket and any provider-session alias bucket
|
|
- deduplicate messages by stable message identity and version
|
|
- treat transcript `message_update` messages as normalized input: each message
|
|
must have `messageId`, positive `version`/`seq`, nullable `turnId`, and
|
|
`occurredAtUnixMs` before core merges it
|
|
|
|
The host owns:
|
|
|
|
- URL construction
|
|
- token or cookie usage
|
|
- `EventSource`, `fetch`, IPC, or another transport
|
|
- raw protocol decoding
|
|
- host-specific retry capability
|
|
|
|
When command reachability differs by Session, the host also projects
|
|
`session/runtimeAvailabilityChanged` into the shared engine. This state is
|
|
ephemeral command coordination, not a canonical Session field. In addition to
|
|
transport and capability reasons, a shared host may project
|
|
`agent_sharing_revoked` with the owner display label. The engine gates
|
|
runtime-dependent commands and AgentGUI presents the same blocked interaction
|
|
for every host; history remains canonical and readable. A host must not map one
|
|
Session's transport loss or revoked sharing relationship to the workspace-wide
|
|
engine connection state.
|
|
|
|
An interaction-scoped collaborative write gate is narrower than Session
|
|
runtime availability. A Host projects it through AgentGUI's optional
|
|
`interactionReadinessSource`, keyed by the exact canonical Interaction tuple,
|
|
and rechecks the same Host authority at submission. AgentGUI owns only the
|
|
generic blocked presentation and early event-time admission. The projection
|
|
must not be copied into activity-core, canonical Interaction lifecycle, or a
|
|
renderer transport-health store; a missing record from a supplied source is a
|
|
fail-closed synchronizing result.
|
|
|
|
## Needs Attention Contract
|
|
|
|
Agent Message Center counts user-actionable items, not all session messages.
|
|
|
|
The initial selector surface is:
|
|
|
|
```ts
|
|
selectNeedsAttentionCount(snapshot): number;
|
|
selectNeedsAttentionItems(snapshot): AgentActivityNeedsAttentionItem[];
|
|
```
|
|
|
|
`AgentActivityNeedsAttentionItem` should contain:
|
|
|
|
```ts
|
|
export interface AgentActivityNeedsAttentionItem {
|
|
id: string;
|
|
workspaceId: string;
|
|
agentSessionId: string;
|
|
provider: string;
|
|
title: string;
|
|
cwd: string;
|
|
kind: "permission" | "question" | "constraint" | "other";
|
|
summary: string;
|
|
occurredAtUnixMs: number;
|
|
}
|
|
```
|
|
|
|
The selector should count pending actionable prompts such as permission
|
|
approvals, ask-user questions, and constraint confirmations. Completed,
|
|
canceled, superseded, or already answered prompts must not be counted.
|
|
|
|
Failed sessions are not automatically needs-attention items unless they expose a
|
|
specific user action that can resolve the failure.
|
|
|
|
## Validation
|
|
|
|
For `agent-activity-core`:
|
|
|
|
- unit tests for message merge ordering and deduplication
|
|
- unit tests for retained stream lifecycle
|
|
- unit tests for needs-attention selectors
|
|
- package typecheck
|
|
|
|
For desktop adapter integration:
|
|
|
|
- existing desktop workspace-agent tests
|
|
- adapter tests for `tuttid` response normalization
|
|
- live event merge tests using a fake subscription adapter
|
|
|
|
For Agent GUI behavior:
|
|
|
|
- existing Agent GUI component and projection tests
|
|
- focused tests for working, waiting, completed, failed, and needs-attention
|
|
states
|
|
- tests that AgentGUI list/detail and write operations use
|
|
`AgentGUIRuntime` when provided, with lifecycle writes asserted through the
|
|
workspace Engine
|
|
|
|
For runtime boundary enforcement:
|
|
|
|
- `pnpm check:agent-activity-runtime-boundaries`
|
|
- `pnpm check:agent-provider-strategy-boundaries`
|
|
- `pnpm check:agent-gui-degradation`
|
|
- these checks are included in `pnpm check:full`
|
|
|
|
## Non-Goals
|
|
|
|
- Do not move desktop transport into a package.
|
|
- Do not create a vague `shared`, `common`, or `utils` package.
|
|
- Do not change daemon HTTP contracts without first updating
|
|
`services/tuttid/api/openapi/tuttid.v1.yaml`.
|
|
|
|
## Review Rules
|
|
|
|
- New public exports in `agent-activity-core` should be stable contracts, not
|
|
convenience exports for one host.
|
|
- A selector belongs in core when Agent GUI and another host-agnostic consumer can
|
|
use it without knowing host details.
|
|
- A React hook belongs in `agent-gui` rather than in core.
|
|
- A pure generated-`tuttid` DTO projection shared by Desktop and Mobile belongs
|
|
in `agent-activity-tuttid-adapter`. Host identity injection, transport,
|
|
retries, event wiring, and orchestration remain in each application adapter.
|
|
- Root detail DTOs use the adapter's aggregate mapper so root Session, child
|
|
Sessions, and Turns enter the Engine through one
|
|
`session/detailSnapshotReceived` intent. The mapper verifies the requested
|
|
Session identity, child hierarchy, and Turn ownership; a malformed nested
|
|
entity rejects the aggregate instead of publishing a partial hierarchy.
|
|
- Engine prompt commands use the activity-core prompt state machine. Required
|
|
settings enter the same serialized per-Session settings lane as direct
|
|
updates and post-activation persistence. Settings from different owners form
|
|
queue barriers and are not coalesced together; validated Session truth is
|
|
applied before send, and a failed or timed-out settings write prevents
|
|
delivery. Transport request mapping remains host-owned.
|
|
- External repository adoption should require implementing the adapter, not
|
|
copying session merge or needs-attention logic.
|