Files
tutti/docs/architecture/workspace-issue-manager.md
agent-radar 2ed3415fb7
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
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

365 lines
18 KiB
Markdown

# Workspace Issue Manager
This document records the reusable issue-manager architecture shared by
`tutti` and `tsh`.
The TypeScript package `packages/workspace/issue-manager` is the current public
frontend/package surface for this boundary. Complementary daemon-side or Go
domain packaging may continue to evolve behind the same host-adapter model.
## Direction
The issue manager is a workspace-domain capability that should be implemented
as reusable packages plus host adapters.
The shared domain model is:
```text
Issue -> Task -> Run
```
- `Issue` is the top-level work item or theme inside one workspace.
- `Task` is an executable unit under an issue.
- `Run` is one agent execution attempt for a task.
Workbench integration should expose this capability as an issue manager node.
The node kind should be `issueManager`, and React-facing names should use
`IssueManagerNode` / `IssueManagerWorkbenchFeature`.
## Package Split
Use two thick packages:
```text
packages/workspace/issues
packages/workspace/issue-manager
```
### `packages/workspace/issues`
This Go package owns transport-agnostic issue-manager behavior:
- issue, task, run, output, and context-reference models
- status, priority, pagination, and search normalization
- title-only issue and task list search; content does not produce list matches
- task status projection onto issue summaries
- run lifecycle orchestration
- a `Store` interface for concrete persistence adapters
It must not own:
- HTTP, gRPC, OpenAPI, or protobuf contracts
- Electron IPC or desktop preload details
- `tsh` room membership, visitor share tokens, or invite capability
- `tuttid` process wiring
- concrete MySQL or SQLite queries
`tuttid` should adapt this package to local SQLite-backed storage. `tsh-server`
should adapt it to the existing remote control-plane storage and room policy
surface.
### `packages/workspace/issue-manager`
This TypeScript package owns the reusable workbench feature surface:
- shared DTOs and service adapter interfaces
- reusable OpenAPI fragment under `openapi/issue-manager.v1.yaml`
- issue-manager view state and workbench node registration helpers
- React-facing issue manager node components and controller logic
- default i18n resources and UI-system-backed Tailwind utility styling
- host-agnostic rich-text context reference handling
- host-agnostic run lifecycle UI flow
It must not read product globals such as `window.tutti`, `legacy TSH preload globals`, or
any host-specific preload surface. Hosts provide explicit adapters instead.
Expected exports:
```text
.
./contracts
./core
./i18n
./services
./ui
./workbench
```
`./core` owns stateless primitives and feature foundation. `./services` should
be the default dependency-injection entry for hosts. It owns service/session
creation plus shared state orchestration. `./ui` should consume those services
and stay focused on rendering, DOM interaction, and imperative UI bridges.
Issue and task content editors consume host-provided `@` rich-text trigger
providers through the workbench node seam. The default issue-manager picker uses
the shared rich-text mention palette with top-level Agent and App tabs,
aligned with Agent GUI mention behavior: Agent mentions resolve
`agent-target` providers, App mentions resolve `workspace-app` providers, and
the menu opens as soon as the `@` trigger is typed, before a search term exists.
Files and issue references should stay on their explicit reference actions
instead of appearing in this `@` picker, and the Task Center app itself should
be excluded from issue-manager app mentions.
Issue run creation is target-first. UI, CLI, and AgentGUI sidecar flows pass
`agentTargetId`/`--agent-target-id` as the launch authority; the daemon derives
and persists the provider for display, filtering, and legacy compatibility.
An explicit manual create-and-run operation commits its `Run` and prepared
launch intent in one SQLite transaction. `PrepareRun` performs validation and
constructs any required implicit top-level Task without writing it. The SQLite
admission adapter inserts that Task, claims it, writes the Run and intent, and
updates Issue projections atomically. If intent admission fails, no implicit
Task, Run, or projection may survive. The intent carries stable Agent
session/client-submit identities and an immutable prompt plus attachment-path
snapshot, and is leased before external delivery. Prepared and leased payloads
pin their managed files independently of mutable ContextRefs. Confirmed
delivery marks the intent dispatched; authoritative evidence that no Agent turn
started settles the intent, Run, task/Issue projections, and topic activity in
one transaction. A delivery-unknown result returns it to `prepared`, and
startup/periodic Issue execution recovery retries the same payload and
identities. Deleting the Issue or claimed Task is rejected while its intent is
`prepared` or `leased`, so cascade deletion cannot remove the recovery record
or attachment pin during Agent delivery. These launch-intent rows, lease
helpers, and recovery semantics belong to the generic workspace Issue boundary;
Tutti Mode adds its execution fences and checkpoint policy on top of them.
Daemon-managed Issue attachments follow the same atomicity rule at the
aggregate boundary: bytes are staged first, while the Issue, image
ContextRefs, and topic activity commit together. Startup file reconciliation
executes before Agent Host and background workers start, then removes only
managed files that never gained a committed ContextRef or were left by a failed
cleanup. File contents are synced on every platform; directory-entry syncing is
used where the operating system exposes a portable directory `fsync` and is a
no-op on Windows. ContextRef removal shares the Issue mutation lock with Run
admission. Automatic Runs also hold a transient source-file pin from their
durable claim until the Agent adapter finishes copying the image; an explicit
prepared or leased launch intent provides the corresponding durable pin for
retryable manual delivery.
## Plan Conversion And Execution Orchestration
Issue Manager accepts two distinct planning origins:
- A provider-native Plan can still be converted through the traditional
AgentGUI **Break into an Issue** flow. Its source is
`traditional_plan`.
- An accepted Tutti Mode Plan task graph is materialized by the daemon-owned
workspace workflow service. Its source is `tutti_mode_plan`.
Tutti Mode Plan does not masquerade as a provider interaction and does not
send an ephemeral renderer draft through the traditional conversion path.
The Agent creates immutable `tutti-mode-plan/v1` revisions through the Tutti
CLI; the user decides daemon-owned checkpoints; and tuttid projects tasks only
from the accepted current task-graph revision. See
[Workspace Workflows And Tutti Mode Plan](./workspace-workflows.md).
For the Tutti-owned path, the Markdown revision already contains the
Issue-level reasoning/orchestration profile, auto/fixed token budget, task
assignments, model choices, execution directories, and dependencies. The
workflow service derives read-only `ActionableItem`s, then invokes Issue
Manager with one atomic Issue-and-task-graph request. The SQLite adapter commits
the Issue, all Tasks, and topic activity atomically; task failure rolls the
Issue back, and update events are published only after commit. A deterministic
workflow operation and Issue ID make repeated decisions and waiters
idempotent. The reserved Issue namespace is owned by the daemon
`workspaceworkflow` business model rather than the reusable workspace Issue
package, so generic Issue consumers cannot become a second Tutti workflow
authority. AgentGUI neither parses an Agent message into this graph nor calls
Issue creation itself.
After materialization, `tutti_mode_plan` graphs are read-only through every
generic Issue Manager mutation surface. Issue/task/context/run/dispatch,
profile, and budget writers return the typed conflict reason
`tutti_issue_managed`, including the source Session recovery target. Manual
and `traditional_plan` Issues keep their existing mutation behavior. Only the
source Agent may use `tutti plan issue mutate`, with the exact active
checkpoint, graph revision, and stable request ID.
Source graph changes are transactional and revisioned. A successful mutation
increments `graphRevision`, rebinds the active checkpoint to that revision,
retires any prepared or dispatched Goal Review and reviewer wake for the stale
graph, and returns the revision required by the next exact-set schedule.
Reusing the same request ID and payload replays the committed result; different
content is a conflict. Obsolete tasks are logically superseded rather than deleted:
their task row, Runs, outputs, and audit history remain queryable, while active
projections and scheduling exclude them. Completed tasks are immutable, and a
running task must first reach a terminal settlement before supersession.
`sourceSessionId` is the durable planning link for both origins. Issue and Task
headers can open that source Session, while successful atomic creation projects
one idempotent, credential-free workspace-Issue mention back into the source
timeline. Tutti Mode Plan additionally keeps its durable workflow, immutable
revisions, checkpoints, and the operation's `issueId`; those records represent
review provenance and do not compete with the Issue as the execution entity.
A `tutti_mode_plan` Issue is a managed read model only when it also carries a
non-empty `sourceSessionId`; the UI fails closed when either marker is missing.
Managed Issue and Task surfaces expose no generic create, edit, delete, move,
run, acceptance, or context-reference mutations. Their only modification
entrypoint opens the exact source Session, appends a localized Issue/Task
mention to the existing composer draft, and focuses the composer without
submitting. The Issue Manager host adapter owns that exact-Session launch; the
shared package never reaches into AgentGUI or replaces an existing draft.
Completion uses a three-step acceptance ladder:
```text
agent_claimed -> auto_checked -> user_accepted
```
That ladder and the generic dispatch frontier remain authoritative for manual
and `traditional_plan` Issues. A `tutti_mode_plan` Issue instead belongs to its
daemon execution aggregate: materialization creates no Run, `autoAccept` has no
dispatch authority, and only the source Agent's checkpoint/revision-fenced
exact-set schedule command may admit Runs. The admission transaction creates
the selected Runs, task projections, and recoverable launch intents together;
it never silently fills capacity with another eligible task. Budget capacity
uses the same shared evaluator as generic dispatch and reserves estimated
allowance for same-Issue active Runs.
Issue runs settle live from the canonical root-turn settlement fan-out: the
Issue-run observer sits on the opt-in settle list (root-provider-lifecycle
adapters report no settled state patch, so the general session-state observers
never see live settles) and matches the settled turn against the run's
initiating `issue-run:<runID>` submit, so an unrelated turn settling in the
delegate conversation can never complete the run. The fallback reconciler only
declares a run dead when the session has been event-silent past the grace
window — a streaming session with a lagging (empty) persisted active-turn id
is alive, not abandoned.
A successful Run moves its task to `pending_acceptance`/`agent_claimed`; it is
only the executor's completion claim. For manual and `traditional_plan`
Issues, explicit user acceptance reaches `user_accepted`. For a
`tutti_mode_plan` Issue, the source Agent must review the durable settlement
checkpoint and either schedule the exact next set or explicitly acknowledge
it. Either command accepts the reviewed successful task transactionally;
failed and canceled task projections remain terminal. Repeated terminal
completion, settlement repair, schedule, and acknowledge mutations are
idempotent.
Parallelism stays honest at two boundaries. Materializing a plan normalizes
the durable `parallelizable` flag against dependencies: a task that depends on
a member of its own consecutive parallelizable group can never run alongside
it, so the misleading flag is stripped (dependencies are never touched — they
are the safe side of the contradiction). At dispatch, a successor whose direct
dependencies ran in isolated per-run worktrees receives their exact branch
names in its launch prompt ("Dependency outputs"), so parallel results are
merged forward instead of stranding on `tutti/task/*` branches; the planning
guide pairs this with the convention of an integration task after every
parallel group.
For generic dispatch, a task whose durable `autoAccept` flag is set skips the
human gate: run completion is accepted programmatically through the same
`UpdateTask` path a manual acceptance takes, so dispatch advance and the
whole-issue completion check stay identical. Tutti-owned execution treats that
field as compatibility metadata only.
Sending a `pending_acceptance` task back to `not_started` (rework) resets its
acceptance to `agent_claimed` and immediately re-opens the dispatch frontier —
the daemon re-dispatches without waiting for an unrelated event. Tutti-owned
terminal Runs never enter the generic notifier or auto-dispatch paths. Each
terminal Run appends an ordered durable execution checkpoint for the source
Agent. Once all tasks are terminal and every terminal Run has a settlement
checkpoint, an `all_tasks_terminal` checkpoint advances the execution to
`pending_goal_review`; completion cannot bypass that review boundary.
The npm package name should be `@tutti-os/workspace-issue-manager`.
It participates in the shared public npm release group documented in
[npm Package Release](../conventions/npm-package-release.md).
The OpenAPI fragment is a transport contract for hosts that expose the shared
issue-manager capability over HTTP. Host daemons keep their own aggregate API
entrypoints and compose the fragment instead of duplicating issue-manager paths,
parameters, and schemas. `tuttid` composes it from
`services/tuttid/api/openapi/tuttid.v1.yaml`.
## Scope Model
The shared packages use `workspaceId` as the stable scope.
`tutti` can pass the local workspace id directly. `tsh` keeps `roomId` in its
host adapter for collaboration, authorization, sharing, and visitor access, then
maps requests into the shared workspace-scoped model.
Room-specific behavior must remain outside the shared core.
Examples of host-owned `tsh` behavior:
- room membership checks
- visitor share token validation
- room invite capacity and share links
- room member display names and avatars
## Context References
Context references are first-class domain entities.
Rich text content may render and edit references, but storage and service logic
should not rely on parsing rich text as the only source of truth. This keeps
search, delete, upload, prompt construction, and future audit behavior stable.
The shared model should support references attached to either an issue or a
task. A task-level reference is used for agent execution context. Issue-level
references are available for higher-level planning and task creation flows.
Hosts may accept inline image attachments while creating an Issue and project
them into ContextRefs after validating and durably storing the bytes. Issue Run
launches inherit supported Issue-level image references and append Task-level
image references, preserving text and image blocks as provider-neutral Agent
prompt content. Host adapters own byte storage and safe-path policy; the shared
domain continues to own only the ContextRef relationship.
Managed attachments cross host and VM boundaries as opaque ContextRefs. Public
responses mark them with `accessKind: managed_attachment`, identify them by
workspace, Issue, and ContextRef IDs, and omit the daemon's absolute storage
path. The host-provided ContextRef opener retrieves validated bytes through the
Issue Manager API, materializes them inside its own trusted attachment store,
and opens that local copy. Ordinary project references keep
`accessKind: workspace_path` and continue through the host's workspace file
adapter. This keeps daemon-local paths out of reusable package contracts and
prevents a VM renderer from trying to open a path from another machine.
The service layer classifies access from the attachment file adapter's managed
path policy; transport mappers never infer ownership from caller-controlled IDs
or MIME values. The reusable TypeScript contract is discriminated:
`workspace_path` carries a required path, while `managed_attachment` cannot
carry one. A legacy path reference with no `accessKind` remains accepted as a
workspace path so existing external hosts keep their file-opening behavior.
The daemon exposes a distinct top-level start-and-launch use case for manual
Issues. It resolves managed ContextRefs before creating the durable Run and
then delegates session/turn creation through Agent Host. Attachment lookup
failure must not create or launch an incomplete Run. Planned Issue
materialization intentionally uses a request schema without inline attachments.
## Agent Run Boundary
The shared packages own run lifecycle state, not concrete agent startup.
The TypeScript feature receives an `agentRunner` adapter. The Go package stores
and transitions run records. Hosts decide how to start Codex, Claude Code,
OpenClaw, Hermes, or any other supported provider.
This boundary lets `tutti` and `tsh` share the issue-manager UX while keeping
their runtime/session integration separate.
## Landing Slices
The boundary-setting slice established the reusable package shape. The current
repository already includes the published TypeScript issue-manager package and
this section tracks the broader cross-host rollout model:
1. Add this architecture document.
2. Scaffold `packages/workspace/issues` with models, service, store interface,
and focused service tests against a fake store.
3. Scaffold `packages/workspace/issue-manager` with contracts, adapter
interfaces, i18n defaults, and workbench registration types.
The next local-first slice wires `tuttid` to the shared contracts:
- `tuttid` SQLite adapter and OpenAPI routes under `/v1/workspaces/{workspaceID}`
- generated `@tutti-os/client-tuttid-ts` issue-manager methods
Later slices can add:
- full React node UI migration from `tsh`
- `tsh-server` adapter from room-scoped APIs to the shared package
- `tsh-desktop` adapter from existing IPC to the shared workbench feature