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

18 KiB

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:

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:

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:

.
./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.

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 ActionableItems, 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:

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.

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