@kolisachint/hoocode-agent 0.4.110 → 0.4.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +107 -0
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +8 -3
- package/dist/cli/args.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +10 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +17 -2
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/plugins/marketplace.d.ts +36 -5
- package/dist/core/extensions/plugins/marketplace.d.ts.map +1 -1
- package/dist/core/extensions/plugins/marketplace.js +63 -17
- package/dist/core/extensions/plugins/marketplace.js.map +1 -1
- package/dist/core/format-duration.d.ts +7 -0
- package/dist/core/format-duration.d.ts.map +1 -0
- package/dist/core/format-duration.js +16 -0
- package/dist/core/format-duration.js.map +1 -0
- package/dist/core/keybindings.d.ts +10 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +10 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/lifeguard.d.ts +15 -0
- package/dist/core/lifeguard.d.ts.map +1 -1
- package/dist/core/lifeguard.js +48 -18
- package/dist/core/lifeguard.js.map +1 -1
- package/dist/core/scheduler.d.ts +8 -2
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +9 -3
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +1 -1
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +2 -2
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/subagent-inbox.d.ts +1 -1
- package/dist/core/subagent-inbox.d.ts.map +1 -1
- package/dist/core/subagent-inbox.js +2 -0
- package/dist/core/subagent-inbox.js.map +1 -1
- package/dist/core/subagent-pool-instance.d.ts.map +1 -1
- package/dist/core/subagent-pool-instance.js +18 -11
- package/dist/core/subagent-pool-instance.js.map +1 -1
- package/dist/core/subagent-pool.d.ts +25 -3
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +120 -25
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/subagent-result.d.ts.map +1 -1
- package/dist/core/subagent-result.js +6 -4
- package/dist/core/subagent-result.js.map +1 -1
- package/dist/core/task-store.d.ts +45 -3
- package/dist/core/task-store.d.ts.map +1 -1
- package/dist/core/task-store.js +51 -5
- package/dist/core/task-store.js.map +1 -1
- package/dist/core/team-view.d.ts.map +1 -1
- package/dist/core/team-view.js +2 -0
- package/dist/core/team-view.js.map +1 -1
- package/dist/core/token-budget.d.ts +13 -1
- package/dist/core/token-budget.d.ts.map +1 -1
- package/dist/core/token-budget.js +18 -1
- package/dist/core/token-budget.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +16 -4
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +211 -67
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/core/tools/todo.d.ts.map +1 -1
- package/dist/core/tools/todo.js +39 -7
- package/dist/core/tools/todo.js.map +1 -1
- package/dist/extensions/core/loop.d.ts.map +1 -1
- package/dist/extensions/core/loop.js +4 -1
- package/dist/extensions/core/loop.js.map +1 -1
- package/dist/extensions/core/marketplace.d.ts +5 -3
- package/dist/extensions/core/marketplace.d.ts.map +1 -1
- package/dist/extensions/core/marketplace.js +37 -15
- package/dist/extensions/core/marketplace.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts +10 -0
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +27 -1
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts +22 -1
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +302 -80
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/interactive/components/team-attach-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/team-attach-panel.js +15 -5
- package/dist/modes/interactive/components/team-attach-panel.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +8 -1
- package/dist/modes/interactive/theme/light.json +8 -1
- package/dist/modes/interactive/theme/theme-schema.json +28 -1
- package/dist/modes/interactive/theme/theme.d.ts +12 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +52 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/interactive/voice/voice-panel.d.ts.map +1 -1
- package/dist/modes/interactive/voice/voice-panel.js +5 -2
- package/dist/modes/interactive/voice/voice-panel.js.map +1 -1
- package/dist/modes/rpc/jsonl.d.ts +16 -1
- package/dist/modes/rpc/jsonl.d.ts.map +1 -1
- package/dist/modes/rpc/jsonl.js +53 -8
- package/dist/modes/rpc/jsonl.js.map +1 -1
- package/dist/utils/atomic-file.d.ts +13 -0
- package/dist/utils/atomic-file.d.ts.map +1 -0
- package/dist/utils/atomic-file.js +26 -0
- package/dist/utils/atomic-file.js.map +1 -0
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagent-result.d.ts","sourceRoot":"","sources":["../../src/core/subagent-result.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"subagent-result.d.ts","sourceRoot":"","sources":["../../src/core/subagent-result.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAIpE,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEpE,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACb;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;IACnB,oFAAoF;IACpF,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC1C,4FAA4F;IAC5F,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB;;;;OAIG;IACH,SAAS,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,GAAG,gBAAgB,EAAE,CAsB1E;AAiED,gFAAgF;AAChF,MAAM,WAAW,0BAA0B;IAC1C,iGAAiG;IACjG,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,SAAS,YAAY,EAAE,EACjC,KAAK,CAAC,EAAE,aAAa,EACrB,OAAO,CAAC,EAAE,0BAA0B,GAClC,kBAAkB,CAkCpB;AAED,iEAAiE;AACjE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAUjG","sourcesContent":["/**\n * Writes a subagent's `result.json` audit file.\n *\n * When a subagent runs as a spawned child process (`--task-id <id>`), the parent\n * SubagentPool verifies `.hoocode/dispatch/<task_id>/result.json` against a fixed\n * schema (see OutputVerifier). This module derives that file deterministically\n * from the finished session so subagents never have to write it themselves.\n */\n\nimport { join } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage } from \"@kolisachint/hoocode-ai\";\nimport { getDispatchTaskDir } from \"../config.js\";\nimport { writeFileAtomicSync } from \"../utils/atomic-file.js\";\nimport type { Task, TaskSource, TaskStatus } from \"./task-store.js\";\n\nexport interface SubagentUsage {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\tcost: number;\n}\n\n/**\n * One node in a subagent's task subtree, propagated to the parent so nested\n * delegation (a subagent that spawns a subagent) is visible above the process\n * boundary. Serialized from the child's task store at session end and merged\n * under the dispatching task by the parent (see finalizeDispatchResult). The\n * `children` are the child's own delegations/MCP calls, recursively.\n */\nexport interface SubagentTaskNode {\n\tid: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent/mcp; unset = the subagent's own TodoWrite plan). */\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tusage?: SubagentUsage;\n\tchildren: SubagentTaskNode[];\n}\n\nexport interface SubagentResultFile {\n\tsummary: string;\n\tfiles_changed: string[];\n\tconfidence: number;\n\tstatus: \"complete\" | \"partial\" | \"failed\";\n\t/** Token and cost usage for the subagent session (extra field; ignored by the verifier). */\n\tusage?: SubagentUsage;\n\t/**\n\t * The child's own task subtree (its TodoWrite plan, delegations, and MCP calls)\n\t * so the parent can render nested work below the dispatching task. Extra field;\n\t * ignored by the verifier.\n\t */\n\ttask_tree?: SubagentTaskNode[];\n}\n\n/**\n * Build the nested task forest from a flat task list, linking children to\n * parents via `parentTaskId`. Roots are tasks with no `parentTaskId`; each\n * node's children preserve store order. Used to serialize a subagent's task\n * store into its `result.json` for the parent to merge.\n */\nexport function buildTaskForest(tasks: readonly Task[]): SubagentTaskNode[] {\n\tconst childrenByParent = new Map<number, Task[]>();\n\tconst roots: Task[] = [];\n\tfor (const task of tasks) {\n\t\tif (task.parentTaskId === undefined) {\n\t\t\troots.push(task);\n\t\t\tcontinue;\n\t\t}\n\t\tconst siblings = childrenByParent.get(task.parentTaskId);\n\t\tif (siblings) siblings.push(task);\n\t\telse childrenByParent.set(task.parentTaskId, [task]);\n\t}\n\tconst toNode = (task: Task): SubagentTaskNode => ({\n\t\tid: task.id,\n\t\ttitle: task.title,\n\t\tstatus: task.status,\n\t\tsource: task.source,\n\t\tsubagentMode: task.subagentMode,\n\t\tusage: task.usage,\n\t\tchildren: (childrenByParent.get(task.id) ?? []).map(toNode),\n\t});\n\treturn roots.map(toNode);\n}\n\n/** Tool names that mutate files. Their `path`/`file_path` argument is a changed file. */\nconst MUTATING_TOOLS = new Set([\"edit\", \"write\"]);\n\n/** Collect distinct file paths touched by edit/write tool calls across the session. */\nfunction collectChangedFiles(messages: readonly AgentMessage[]): string[] {\n\tconst files = new Set<string>();\n\tfor (const message of messages) {\n\t\tif (message.role !== \"assistant\") continue;\n\t\tfor (const content of (message as AssistantMessage).content) {\n\t\t\tif (content.type !== \"toolCall\") continue;\n\t\t\tif (!MUTATING_TOOLS.has(content.name)) continue;\n\t\t\tconst args = content.arguments ?? {};\n\t\t\tconst path =\n\t\t\t\ttypeof args.path === \"string\" ? args.path : typeof args.file_path === \"string\" ? args.file_path : null;\n\t\t\tif (path) files.add(path);\n\t\t}\n\t}\n\treturn [...files];\n}\n\n/** Last assistant text, trimmed, as the summary. */\nfunction deriveSummary(messages: readonly AgentMessage[]): string {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst message = messages[i];\n\t\tif (message.role !== \"assistant\") continue;\n\t\tconst assistant = message as AssistantMessage;\n\t\tlet text = \"\";\n\t\tfor (const content of assistant.content) {\n\t\t\tif (content.type === \"text\") text += content.text;\n\t\t}\n\t\tconst trimmed = text.trim();\n\t\tif (trimmed) return trimmed;\n\t}\n\treturn \"\";\n}\n\n/** Derive the terminal status from the final assistant message. */\nfunction deriveStatus(messages: readonly AgentMessage[]): \"complete\" | \"failed\" {\n\tconst last = messages[messages.length - 1];\n\tif (last?.role === \"assistant\") {\n\t\tconst assistant = last as AssistantMessage;\n\t\tif (assistant.stopReason === \"error\" || assistant.stopReason === \"aborted\") return \"failed\";\n\t}\n\treturn \"complete\";\n}\n\n/**\n * Concrete error text from the final assistant message when the run ended in an\n * error (e.g. a provider usage/quota or rate-limit message). Surfacing this in\n * the summary lets the parent report the real cause instead of \"subagent failed\".\n */\nfunction deriveErrorMessage(messages: readonly AgentMessage[]): string | undefined {\n\tconst last = messages[messages.length - 1];\n\tif (last?.role === \"assistant\") {\n\t\tconst assistant = last as AssistantMessage;\n\t\tif (assistant.stopReason === \"error\" && assistant.errorMessage) {\n\t\t\tconst trimmed = assistant.errorMessage.trim();\n\t\t\tif (trimmed) return trimmed;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/** Extra build options that override the status derived from the transcript. */\nexport interface BuildSubagentResultOptions {\n\t/** The run was stopped at its turn cap. Report a usable partial result rather than a failure. */\n\treachedMaxTurns?: boolean;\n}\n\n/**\n * Build the `result.json` payload for a finished subagent session.\n *\n * The verifier requires a non-empty summary and confidence >= 0.5, so a\n * successful run with no assistant text still yields a usable summary.\n *\n * When the run was stopped at its turn cap (`reachedMaxTurns`), the result is\n * reported as `partial` with whatever summary the agent managed to produce,\n * instead of the `failed` status an aborted final message would otherwise yield.\n */\nexport function buildSubagentResult(\n\tmessages: readonly AgentMessage[],\n\tusage?: SubagentUsage,\n\toptions?: BuildSubagentResultOptions,\n): SubagentResultFile {\n\tif (options?.reachedMaxTurns) {\n\t\treturn {\n\t\t\tsummary: deriveSummary(messages) || \"Reached the turn limit before completing. Returning partial findings.\",\n\t\t\tfiles_changed: collectChangedFiles(messages),\n\t\t\tconfidence: 0.6,\n\t\t\tstatus: \"partial\",\n\t\t\tusage,\n\t\t};\n\t}\n\n\tconst status = deriveStatus(messages);\n\tif (status === \"failed\") {\n\t\t// Prefer the provider/model error over any partial assistant text so the\n\t\t// caller sees the real cause (e.g. \"usage limit reached\").\n\t\tconst errorMessage = deriveErrorMessage(messages);\n\t\tconst summary = errorMessage ? `Task failed: ${errorMessage}` : deriveSummary(messages) || \"Task failed.\";\n\t\treturn {\n\t\t\tsummary,\n\t\t\tfiles_changed: collectChangedFiles(messages),\n\t\t\tconfidence: 0.5,\n\t\t\tstatus,\n\t\t\tusage,\n\t\t};\n\t}\n\n\tconst summary = deriveSummary(messages) || \"Task completed with no textual summary.\";\n\treturn {\n\t\tsummary,\n\t\tfiles_changed: collectChangedFiles(messages),\n\t\tconfidence: 0.9,\n\t\tstatus,\n\t\tusage,\n\t};\n}\n\n/** Write `result.json` for a task. Best-effort: never throws. */\nexport function writeSubagentResult(cwd: string, taskId: string, result: SubagentResultFile): void {\n\tconst path = join(getDispatchTaskDir(cwd, taskId), \"result.json\");\n\ttry {\n\t\t// Atomic (temp + same-dir rename): the parent may read this file the moment\n\t\t// it appears, or SIGKILL the child mid-write — a torn result.json would be\n\t\t// parsed as a verification failure and turn a genuine success into a failure.\n\t\twriteFileAtomicSync(path, JSON.stringify(result, null, 2));\n\t} catch {\n\t\t// Audit file is best-effort; the parent treats a missing file as a failure.\n\t}\n}\n"]}
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* schema (see OutputVerifier). This module derives that file deterministically
|
|
7
7
|
* from the finished session so subagents never have to write it themselves.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import { dirname, join } from "node:path";
|
|
9
|
+
import { join } from "node:path";
|
|
11
10
|
import { getDispatchTaskDir } from "../config.js";
|
|
11
|
+
import { writeFileAtomicSync } from "../utils/atomic-file.js";
|
|
12
12
|
/**
|
|
13
13
|
* Build the nested task forest from a flat task list, linking children to
|
|
14
14
|
* parents via `parentTaskId`. Roots are tasks with no `parentTaskId`; each
|
|
@@ -153,8 +153,10 @@ export function buildSubagentResult(messages, usage, options) {
|
|
|
153
153
|
export function writeSubagentResult(cwd, taskId, result) {
|
|
154
154
|
const path = join(getDispatchTaskDir(cwd, taskId), "result.json");
|
|
155
155
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
// Atomic (temp + same-dir rename): the parent may read this file the moment
|
|
157
|
+
// it appears, or SIGKILL the child mid-write — a torn result.json would be
|
|
158
|
+
// parsed as a verification failure and turn a genuine success into a failure.
|
|
159
|
+
writeFileAtomicSync(path, JSON.stringify(result, null, 2));
|
|
158
160
|
}
|
|
159
161
|
catch {
|
|
160
162
|
// Audit file is best-effort; the parent treats a missing file as a failure.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagent-result.js","sourceRoot":"","sources":["../../src/core/subagent-result.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Writes a subagent's `result.json` audit file.\n *\n * When a subagent runs as a spawned child process (`--task-id <id>`), the parent\n * SubagentPool verifies `.hoocode/dispatch/<task_id>/result.json` against a fixed\n * schema (see OutputVerifier). This module derives that file deterministically\n * from the finished session so subagents never have to write it themselves.\n */\n\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage } from \"@kolisachint/hoocode-ai\";\nimport { getDispatchTaskDir } from \"../config.js\";\nimport type { Task, TaskSource, TaskStatus } from \"./task-store.js\";\n\nexport interface SubagentUsage {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\tcost: number;\n}\n\n/**\n * One node in a subagent's task subtree, propagated to the parent so nested\n * delegation (a subagent that spawns a subagent) is visible above the process\n * boundary. Serialized from the child's task store at session end and merged\n * under the dispatching task by the parent (see finalizeDispatchResult). The\n * `children` are the child's own delegations/MCP calls, recursively.\n */\nexport interface SubagentTaskNode {\n\tid: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent/mcp; unset = the subagent's own TodoWrite plan). */\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tusage?: SubagentUsage;\n\tchildren: SubagentTaskNode[];\n}\n\nexport interface SubagentResultFile {\n\tsummary: string;\n\tfiles_changed: string[];\n\tconfidence: number;\n\tstatus: \"complete\" | \"partial\" | \"failed\";\n\t/** Token and cost usage for the subagent session (extra field; ignored by the verifier). */\n\tusage?: SubagentUsage;\n\t/**\n\t * The child's own task subtree (its TodoWrite plan, delegations, and MCP calls)\n\t * so the parent can render nested work below the dispatching task. Extra field;\n\t * ignored by the verifier.\n\t */\n\ttask_tree?: SubagentTaskNode[];\n}\n\n/**\n * Build the nested task forest from a flat task list, linking children to\n * parents via `parentTaskId`. Roots are tasks with no `parentTaskId`; each\n * node's children preserve store order. Used to serialize a subagent's task\n * store into its `result.json` for the parent to merge.\n */\nexport function buildTaskForest(tasks: readonly Task[]): SubagentTaskNode[] {\n\tconst childrenByParent = new Map<number, Task[]>();\n\tconst roots: Task[] = [];\n\tfor (const task of tasks) {\n\t\tif (task.parentTaskId === undefined) {\n\t\t\troots.push(task);\n\t\t\tcontinue;\n\t\t}\n\t\tconst siblings = childrenByParent.get(task.parentTaskId);\n\t\tif (siblings) siblings.push(task);\n\t\telse childrenByParent.set(task.parentTaskId, [task]);\n\t}\n\tconst toNode = (task: Task): SubagentTaskNode => ({\n\t\tid: task.id,\n\t\ttitle: task.title,\n\t\tstatus: task.status,\n\t\tsource: task.source,\n\t\tsubagentMode: task.subagentMode,\n\t\tusage: task.usage,\n\t\tchildren: (childrenByParent.get(task.id) ?? []).map(toNode),\n\t});\n\treturn roots.map(toNode);\n}\n\n/** Tool names that mutate files. Their `path`/`file_path` argument is a changed file. */\nconst MUTATING_TOOLS = new Set([\"edit\", \"write\"]);\n\n/** Collect distinct file paths touched by edit/write tool calls across the session. */\nfunction collectChangedFiles(messages: readonly AgentMessage[]): string[] {\n\tconst files = new Set<string>();\n\tfor (const message of messages) {\n\t\tif (message.role !== \"assistant\") continue;\n\t\tfor (const content of (message as AssistantMessage).content) {\n\t\t\tif (content.type !== \"toolCall\") continue;\n\t\t\tif (!MUTATING_TOOLS.has(content.name)) continue;\n\t\t\tconst args = content.arguments ?? {};\n\t\t\tconst path =\n\t\t\t\ttypeof args.path === \"string\" ? args.path : typeof args.file_path === \"string\" ? args.file_path : null;\n\t\t\tif (path) files.add(path);\n\t\t}\n\t}\n\treturn [...files];\n}\n\n/** Last assistant text, trimmed, as the summary. */\nfunction deriveSummary(messages: readonly AgentMessage[]): string {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst message = messages[i];\n\t\tif (message.role !== \"assistant\") continue;\n\t\tconst assistant = message as AssistantMessage;\n\t\tlet text = \"\";\n\t\tfor (const content of assistant.content) {\n\t\t\tif (content.type === \"text\") text += content.text;\n\t\t}\n\t\tconst trimmed = text.trim();\n\t\tif (trimmed) return trimmed;\n\t}\n\treturn \"\";\n}\n\n/** Derive the terminal status from the final assistant message. */\nfunction deriveStatus(messages: readonly AgentMessage[]): \"complete\" | \"failed\" {\n\tconst last = messages[messages.length - 1];\n\tif (last?.role === \"assistant\") {\n\t\tconst assistant = last as AssistantMessage;\n\t\tif (assistant.stopReason === \"error\" || assistant.stopReason === \"aborted\") return \"failed\";\n\t}\n\treturn \"complete\";\n}\n\n/**\n * Concrete error text from the final assistant message when the run ended in an\n * error (e.g. a provider usage/quota or rate-limit message). Surfacing this in\n * the summary lets the parent report the real cause instead of \"subagent failed\".\n */\nfunction deriveErrorMessage(messages: readonly AgentMessage[]): string | undefined {\n\tconst last = messages[messages.length - 1];\n\tif (last?.role === \"assistant\") {\n\t\tconst assistant = last as AssistantMessage;\n\t\tif (assistant.stopReason === \"error\" && assistant.errorMessage) {\n\t\t\tconst trimmed = assistant.errorMessage.trim();\n\t\t\tif (trimmed) return trimmed;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/** Extra build options that override the status derived from the transcript. */\nexport interface BuildSubagentResultOptions {\n\t/** The run was stopped at its turn cap. Report a usable partial result rather than a failure. */\n\treachedMaxTurns?: boolean;\n}\n\n/**\n * Build the `result.json` payload for a finished subagent session.\n *\n * The verifier requires a non-empty summary and confidence >= 0.5, so a\n * successful run with no assistant text still yields a usable summary.\n *\n * When the run was stopped at its turn cap (`reachedMaxTurns`), the result is\n * reported as `partial` with whatever summary the agent managed to produce,\n * instead of the `failed` status an aborted final message would otherwise yield.\n */\nexport function buildSubagentResult(\n\tmessages: readonly AgentMessage[],\n\tusage?: SubagentUsage,\n\toptions?: BuildSubagentResultOptions,\n): SubagentResultFile {\n\tif (options?.reachedMaxTurns) {\n\t\treturn {\n\t\t\tsummary: deriveSummary(messages) || \"Reached the turn limit before completing. Returning partial findings.\",\n\t\t\tfiles_changed: collectChangedFiles(messages),\n\t\t\tconfidence: 0.6,\n\t\t\tstatus: \"partial\",\n\t\t\tusage,\n\t\t};\n\t}\n\n\tconst status = deriveStatus(messages);\n\tif (status === \"failed\") {\n\t\t// Prefer the provider/model error over any partial assistant text so the\n\t\t// caller sees the real cause (e.g. \"usage limit reached\").\n\t\tconst errorMessage = deriveErrorMessage(messages);\n\t\tconst summary = errorMessage ? `Task failed: ${errorMessage}` : deriveSummary(messages) || \"Task failed.\";\n\t\treturn {\n\t\t\tsummary,\n\t\t\tfiles_changed: collectChangedFiles(messages),\n\t\t\tconfidence: 0.5,\n\t\t\tstatus,\n\t\t\tusage,\n\t\t};\n\t}\n\n\tconst summary = deriveSummary(messages) || \"Task completed with no textual summary.\";\n\treturn {\n\t\tsummary,\n\t\tfiles_changed: collectChangedFiles(messages),\n\t\tconfidence: 0.9,\n\t\tstatus,\n\t\tusage,\n\t};\n}\n\n/** Write `result.json` for a task. Best-effort: never throws. */\nexport function writeSubagentResult(cwd: string, taskId: string, result: SubagentResultFile): void {\n\tconst path = join(getDispatchTaskDir(cwd, taskId), \"result.json\");\n\ttry {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t\twriteFileSync(path, JSON.stringify(result, null, 2));\n\t} catch {\n\t\t// Audit file is best-effort; the parent treats a missing file as a failure.\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"subagent-result.js","sourceRoot":"","sources":["../../src/core/subagent-result.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Writes a subagent's `result.json` audit file.\n *\n * When a subagent runs as a spawned child process (`--task-id <id>`), the parent\n * SubagentPool verifies `.hoocode/dispatch/<task_id>/result.json` against a fixed\n * schema (see OutputVerifier). This module derives that file deterministically\n * from the finished session so subagents never have to write it themselves.\n */\n\nimport { join } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage } from \"@kolisachint/hoocode-ai\";\nimport { getDispatchTaskDir } from \"../config.js\";\nimport { writeFileAtomicSync } from \"../utils/atomic-file.js\";\nimport type { Task, TaskSource, TaskStatus } from \"./task-store.js\";\n\nexport interface SubagentUsage {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\tcost: number;\n}\n\n/**\n * One node in a subagent's task subtree, propagated to the parent so nested\n * delegation (a subagent that spawns a subagent) is visible above the process\n * boundary. Serialized from the child's task store at session end and merged\n * under the dispatching task by the parent (see finalizeDispatchResult). The\n * `children` are the child's own delegations/MCP calls, recursively.\n */\nexport interface SubagentTaskNode {\n\tid: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent/mcp; unset = the subagent's own TodoWrite plan). */\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tusage?: SubagentUsage;\n\tchildren: SubagentTaskNode[];\n}\n\nexport interface SubagentResultFile {\n\tsummary: string;\n\tfiles_changed: string[];\n\tconfidence: number;\n\tstatus: \"complete\" | \"partial\" | \"failed\";\n\t/** Token and cost usage for the subagent session (extra field; ignored by the verifier). */\n\tusage?: SubagentUsage;\n\t/**\n\t * The child's own task subtree (its TodoWrite plan, delegations, and MCP calls)\n\t * so the parent can render nested work below the dispatching task. Extra field;\n\t * ignored by the verifier.\n\t */\n\ttask_tree?: SubagentTaskNode[];\n}\n\n/**\n * Build the nested task forest from a flat task list, linking children to\n * parents via `parentTaskId`. Roots are tasks with no `parentTaskId`; each\n * node's children preserve store order. Used to serialize a subagent's task\n * store into its `result.json` for the parent to merge.\n */\nexport function buildTaskForest(tasks: readonly Task[]): SubagentTaskNode[] {\n\tconst childrenByParent = new Map<number, Task[]>();\n\tconst roots: Task[] = [];\n\tfor (const task of tasks) {\n\t\tif (task.parentTaskId === undefined) {\n\t\t\troots.push(task);\n\t\t\tcontinue;\n\t\t}\n\t\tconst siblings = childrenByParent.get(task.parentTaskId);\n\t\tif (siblings) siblings.push(task);\n\t\telse childrenByParent.set(task.parentTaskId, [task]);\n\t}\n\tconst toNode = (task: Task): SubagentTaskNode => ({\n\t\tid: task.id,\n\t\ttitle: task.title,\n\t\tstatus: task.status,\n\t\tsource: task.source,\n\t\tsubagentMode: task.subagentMode,\n\t\tusage: task.usage,\n\t\tchildren: (childrenByParent.get(task.id) ?? []).map(toNode),\n\t});\n\treturn roots.map(toNode);\n}\n\n/** Tool names that mutate files. Their `path`/`file_path` argument is a changed file. */\nconst MUTATING_TOOLS = new Set([\"edit\", \"write\"]);\n\n/** Collect distinct file paths touched by edit/write tool calls across the session. */\nfunction collectChangedFiles(messages: readonly AgentMessage[]): string[] {\n\tconst files = new Set<string>();\n\tfor (const message of messages) {\n\t\tif (message.role !== \"assistant\") continue;\n\t\tfor (const content of (message as AssistantMessage).content) {\n\t\t\tif (content.type !== \"toolCall\") continue;\n\t\t\tif (!MUTATING_TOOLS.has(content.name)) continue;\n\t\t\tconst args = content.arguments ?? {};\n\t\t\tconst path =\n\t\t\t\ttypeof args.path === \"string\" ? args.path : typeof args.file_path === \"string\" ? args.file_path : null;\n\t\t\tif (path) files.add(path);\n\t\t}\n\t}\n\treturn [...files];\n}\n\n/** Last assistant text, trimmed, as the summary. */\nfunction deriveSummary(messages: readonly AgentMessage[]): string {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst message = messages[i];\n\t\tif (message.role !== \"assistant\") continue;\n\t\tconst assistant = message as AssistantMessage;\n\t\tlet text = \"\";\n\t\tfor (const content of assistant.content) {\n\t\t\tif (content.type === \"text\") text += content.text;\n\t\t}\n\t\tconst trimmed = text.trim();\n\t\tif (trimmed) return trimmed;\n\t}\n\treturn \"\";\n}\n\n/** Derive the terminal status from the final assistant message. */\nfunction deriveStatus(messages: readonly AgentMessage[]): \"complete\" | \"failed\" {\n\tconst last = messages[messages.length - 1];\n\tif (last?.role === \"assistant\") {\n\t\tconst assistant = last as AssistantMessage;\n\t\tif (assistant.stopReason === \"error\" || assistant.stopReason === \"aborted\") return \"failed\";\n\t}\n\treturn \"complete\";\n}\n\n/**\n * Concrete error text from the final assistant message when the run ended in an\n * error (e.g. a provider usage/quota or rate-limit message). Surfacing this in\n * the summary lets the parent report the real cause instead of \"subagent failed\".\n */\nfunction deriveErrorMessage(messages: readonly AgentMessage[]): string | undefined {\n\tconst last = messages[messages.length - 1];\n\tif (last?.role === \"assistant\") {\n\t\tconst assistant = last as AssistantMessage;\n\t\tif (assistant.stopReason === \"error\" && assistant.errorMessage) {\n\t\t\tconst trimmed = assistant.errorMessage.trim();\n\t\t\tif (trimmed) return trimmed;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/** Extra build options that override the status derived from the transcript. */\nexport interface BuildSubagentResultOptions {\n\t/** The run was stopped at its turn cap. Report a usable partial result rather than a failure. */\n\treachedMaxTurns?: boolean;\n}\n\n/**\n * Build the `result.json` payload for a finished subagent session.\n *\n * The verifier requires a non-empty summary and confidence >= 0.5, so a\n * successful run with no assistant text still yields a usable summary.\n *\n * When the run was stopped at its turn cap (`reachedMaxTurns`), the result is\n * reported as `partial` with whatever summary the agent managed to produce,\n * instead of the `failed` status an aborted final message would otherwise yield.\n */\nexport function buildSubagentResult(\n\tmessages: readonly AgentMessage[],\n\tusage?: SubagentUsage,\n\toptions?: BuildSubagentResultOptions,\n): SubagentResultFile {\n\tif (options?.reachedMaxTurns) {\n\t\treturn {\n\t\t\tsummary: deriveSummary(messages) || \"Reached the turn limit before completing. Returning partial findings.\",\n\t\t\tfiles_changed: collectChangedFiles(messages),\n\t\t\tconfidence: 0.6,\n\t\t\tstatus: \"partial\",\n\t\t\tusage,\n\t\t};\n\t}\n\n\tconst status = deriveStatus(messages);\n\tif (status === \"failed\") {\n\t\t// Prefer the provider/model error over any partial assistant text so the\n\t\t// caller sees the real cause (e.g. \"usage limit reached\").\n\t\tconst errorMessage = deriveErrorMessage(messages);\n\t\tconst summary = errorMessage ? `Task failed: ${errorMessage}` : deriveSummary(messages) || \"Task failed.\";\n\t\treturn {\n\t\t\tsummary,\n\t\t\tfiles_changed: collectChangedFiles(messages),\n\t\t\tconfidence: 0.5,\n\t\t\tstatus,\n\t\t\tusage,\n\t\t};\n\t}\n\n\tconst summary = deriveSummary(messages) || \"Task completed with no textual summary.\";\n\treturn {\n\t\tsummary,\n\t\tfiles_changed: collectChangedFiles(messages),\n\t\tconfidence: 0.9,\n\t\tstatus,\n\t\tusage,\n\t};\n}\n\n/** Write `result.json` for a task. Best-effort: never throws. */\nexport function writeSubagentResult(cwd: string, taskId: string, result: SubagentResultFile): void {\n\tconst path = join(getDispatchTaskDir(cwd, taskId), \"result.json\");\n\ttry {\n\t\t// Atomic (temp + same-dir rename): the parent may read this file the moment\n\t\t// it appears, or SIGKILL the child mid-write — a torn result.json would be\n\t\t// parsed as a verification failure and turn a genuine success into a failure.\n\t\twriteFileAtomicSync(path, JSON.stringify(result, null, 2));\n\t} catch {\n\t\t// Audit file is best-effort; the parent treats a missing file as a failure.\n\t}\n}\n"]}
|
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
* creates tasks and the footer that renders them live in the same process and
|
|
7
7
|
* there is no cross-process boundary to cross.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* `cancelled` is a user-initiated stop (Esc/abort mid-run) — deliberately
|
|
11
|
+
* distinct from `failed` so the panel doesn't paint an intentional interrupt
|
|
12
|
+
* as an error.
|
|
13
|
+
*/
|
|
14
|
+
export type TaskStatus = "pending" | "in_progress" | "done" | "failed" | "cancelled";
|
|
10
15
|
/**
|
|
11
16
|
* What kind of background work owns a task, surfaced as a source glyph in the
|
|
12
17
|
* pane. Unset means the main agent. A "team" origin is reserved for the
|
|
@@ -20,7 +25,7 @@ export type TaskSource = "subagent" | "mcp";
|
|
|
20
25
|
*/
|
|
21
26
|
export type TaskAgentKind = "main" | "subagent" | "role";
|
|
22
27
|
/** Lifecycle word shown as the agent's `[state]` tag in grouped views. */
|
|
23
|
-
export type TaskAgentState = "active" | "running" | "done" | "queued" | "idle" | "waiting" | "failed";
|
|
28
|
+
export type TaskAgentState = "active" | "running" | "done" | "queued" | "idle" | "waiting" | "failed" | "cancelled";
|
|
24
29
|
/**
|
|
25
30
|
* An agent that owns tasks, rendered as a group header in the pane's
|
|
26
31
|
* `subagents` / `teams` views. Subagent dispatches register themselves here;
|
|
@@ -72,12 +77,33 @@ export interface Task {
|
|
|
72
77
|
* subtree is merged into the parent (see finalizeDispatchResult).
|
|
73
78
|
*/
|
|
74
79
|
parentTaskId?: number;
|
|
80
|
+
/**
|
|
81
|
+
* Id of the main-agent (TodoWrite) task this run is working on, set at
|
|
82
|
+
* dispatch time when exactly one plan item is in_progress. Purely a display
|
|
83
|
+
* link: the flat ("tasks") lens nests the run under its plan item so the
|
|
84
|
+
* plan and the agents executing it read as one picture. Deliberately NOT
|
|
85
|
+
* `parentTaskId` — that field drives the subagents tree and the
|
|
86
|
+
* cross-process subtree merge, and overloading it would move runs between
|
|
87
|
+
* lenses. A dangling link (the todo was removed/replaced) is simply not
|
|
88
|
+
* rendered.
|
|
89
|
+
*/
|
|
90
|
+
linkedTaskId?: number;
|
|
75
91
|
/**
|
|
76
92
|
* Short warning note surfaced as a ⚠ cue in the task pane (e.g. the subagent
|
|
77
93
|
* fell back to the inherited model, or was skipped because the provider was
|
|
78
94
|
* exhausted). Kept terse so it fits the row's right column.
|
|
79
95
|
*/
|
|
80
96
|
note?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Canonical TodoWrite item content for main-plan tasks. `title` is the
|
|
99
|
+
* *display* form and legitimately changes between `content` and `activeForm`
|
|
100
|
+
* as the item's status moves, so it cannot identify an item across TodoWrite
|
|
101
|
+
* calls. This field can: the tool reconciles incoming items against it by
|
|
102
|
+
* identity first (position only as a fallback), keeping task ids — and the
|
|
103
|
+
* subagent runs linked to them — attached to the same plan item when the
|
|
104
|
+
* list is reordered or shrunk.
|
|
105
|
+
*/
|
|
106
|
+
todoContent?: string;
|
|
81
107
|
readonly createdAt: number;
|
|
82
108
|
updatedAt: number;
|
|
83
109
|
/** Token and cost usage attributed to this task (e.g. from a subagent session). */
|
|
@@ -94,8 +120,9 @@ export interface CreateTaskOptions {
|
|
|
94
120
|
subagentMode?: string;
|
|
95
121
|
agent?: string;
|
|
96
122
|
parentTaskId?: number;
|
|
123
|
+
linkedTaskId?: number;
|
|
97
124
|
}
|
|
98
|
-
export type TaskPatch = Partial<Pick<Task, "title" | "status" | "source" | "subagentMode" | "agent" | "usage" | "note" | "parentTaskId">>;
|
|
125
|
+
export type TaskPatch = Partial<Pick<Task, "title" | "status" | "source" | "subagentMode" | "agent" | "usage" | "note" | "parentTaskId" | "linkedTaskId" | "todoContent">>;
|
|
99
126
|
export type TaskAgentPatch = Partial<Omit<TaskAgent, "id">>;
|
|
100
127
|
/**
|
|
101
128
|
* Owner group for a task when no explicit agent is set: subagent-sourced work
|
|
@@ -111,6 +138,13 @@ declare class TaskStore {
|
|
|
111
138
|
private nextId;
|
|
112
139
|
private readonly listeners;
|
|
113
140
|
private batchDepth;
|
|
141
|
+
private mutationCount;
|
|
142
|
+
/**
|
|
143
|
+
* Monotonic counter bumped on every mutation (including those inside a
|
|
144
|
+
* batch). Lets renderers cache derived state and invalidate it cheaply by
|
|
145
|
+
* comparing versions instead of deep-diffing tasks/agents.
|
|
146
|
+
*/
|
|
147
|
+
version(): number;
|
|
114
148
|
/**
|
|
115
149
|
* Run a series of mutations without notifying listeners until the batch
|
|
116
150
|
* completes, so the TUI renders once instead of per-item. Nested batches are
|
|
@@ -138,6 +172,14 @@ declare class TaskStore {
|
|
|
138
172
|
agents(): readonly TaskAgent[];
|
|
139
173
|
private applyAgentPatch;
|
|
140
174
|
remove(id: number): void;
|
|
175
|
+
/**
|
|
176
|
+
* Arrange the given tasks (by id) into the specified relative order, keeping
|
|
177
|
+
* every other task fixed: the matched tasks permute among their existing
|
|
178
|
+
* array slots. Lets TodoWrite render the plan in list order while ids stay
|
|
179
|
+
* pinned to their items (its identity-based reconcile). Ignored unless every
|
|
180
|
+
* id resolves to a task.
|
|
181
|
+
*/
|
|
182
|
+
arrange(ids: readonly number[]): void;
|
|
141
183
|
/**
|
|
142
184
|
* Drop finished tasks and restart numbering from #1 once the pane is empty.
|
|
143
185
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-store.d.ts","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\";\n\n/**\n * What kind of background work owns a task, surfaced as a source glyph in the\n * pane. Unset means the main agent. A \"team\" origin is reserved for the\n * hooteams integration and stays unwired until that lands.\n */\nexport type TaskSource = \"subagent\" | \"mcp\";\n\n/**\n * Kind of agent that can own tasks in the pane's grouped views:\n * the main session (orchestrator), a spawned subagent, or a named\n * team role-agent (e.g. fed by a hooteams bridge).\n */\nexport type TaskAgentKind = \"main\" | \"subagent\" | \"role\";\n\n/** Lifecycle word shown as the agent's `[state]` tag in grouped views. */\nexport type TaskAgentState = \"active\" | \"running\" | \"done\" | \"queued\" | \"idle\" | \"waiting\" | \"failed\";\n\n/**\n * An agent that owns tasks, rendered as a group header in the pane's\n * `subagents` / `teams` views. Subagent dispatches register themselves here;\n * external orchestrators (hooteams) can upsert role-agents with handoffs.\n */\nexport interface TaskAgent {\n\treadonly id: string;\n\tname: string;\n\t/** Short descriptor after the name: \"orchestrator\", \"subagent\", or a team role like \"architect\". */\n\trole?: string;\n\tkind: TaskAgentKind;\n\tstate?: TaskAgentState;\n\t/** Handoff arrow text for team views (e.g. \"→ reviewer\", \"← builder\"). */\n\thandoff?: string;\n\t/**\n\t * Live activity descriptor for a running subagent (e.g. the tool it is\n\t * currently executing), fed by the pool's `task_progress` events. Empty string\n\t * means \"no current activity\" — `applyAgentPatch` skips `undefined`, so callers\n\t * clear it by patching `\"\"`, not `undefined`.\n\t */\n\tactivity?: string;\n\t/** Per-agent token + cost totals, shown right-aligned on the group header. */\n\tstats?: { input: number; output: number; cost: number };\n}\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent delegation vs MCP tool call; unset = main agent); drives the pane's source glyph. */\n\tsource?: TaskSource;\n\t/**\n\t * Origin label shown as the row's `[tag]` in the task pane: the subagent\n\t * type for delegations (e.g. \"explore\"), the MCP server name for MCP tasks\n\t * (e.g. \"github\").\n\t */\n\tsubagentMode?: string;\n\t/** Id of the owning TaskAgent; drives grouping in the pane's subagents/teams views. */\n\tagent?: string;\n\t/**\n\t * Id of the task that spawned this one, linking a dispatched subagent (and its\n\t * own delegations) back to the Task call that created it. Root tasks omit it.\n\t * Drives the subagents lens's recursive task tree: a node's children are the\n\t * tasks whose `parentTaskId` is its id, so nesting deeper than one level (a\n\t * subagent that spawns a subagent) is visible. Set when a child subagent's task\n\t * subtree is merged into the parent (see finalizeDispatchResult).\n\t */\n\tparentTaskId?: number;\n\t/**\n\t * Short warning note surfaced as a ⚠ cue in the task pane (e.g. the subagent\n\t * fell back to the inherited model, or was skipped because the provider was\n\t * exhausted). Kept terse so it fits the row's right column.\n\t */\n\tnote?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\nexport interface CreateTaskOptions {\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tagent?: string;\n\tparentTaskId?: number;\n}\n\nexport type TaskPatch = Partial<\n\tPick<Task, \"title\" | \"status\" | \"source\" | \"subagentMode\" | \"agent\" | \"usage\" | \"note\" | \"parentTaskId\">\n>;\n\nexport type TaskAgentPatch = Partial<Omit<TaskAgent, \"id\">>;\n\n/**\n * Owner group for a task when no explicit agent is set: subagent-sourced work\n * falls into a generic \"subagent\" group, everything else belongs to main.\n * Shared by the store's reset() and the pane's grouped views so the two never\n * disagree about which agents still own live tasks.\n */\nexport function taskOwnerId(task: Pick<Task, \"agent\" | \"source\">): string {\n\tif (task.agent) return task.agent;\n\treturn task.source === \"subagent\" ? \"subagent\" : \"main\";\n}\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate taskAgents: TaskAgent[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\tprivate batchDepth = 0;\n\n\t/**\n\t * Run a series of mutations without notifying listeners until the batch\n\t * completes, so the TUI renders once instead of per-item. Nested batches are\n\t * supported: only the outermost batch flushes.\n\t */\n\tbatch(fn: () => void): void {\n\t\tthis.batchDepth++;\n\t\ttry {\n\t\t\tfn();\n\t\t} finally {\n\t\t\tthis.batchDepth--;\n\t\t\tif (this.batchDepth === 0) this.emit();\n\t\t}\n\t}\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsource: options.source,\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tagent: options.agent,\n\t\t\tparentTaskId: options.parentTaskId,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) {\n\t\t\tconsole.warn(`[task-store] update: unknown task id ${id}`);\n\t\t\treturn;\n\t\t}\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.source !== undefined) task.source = patch.source;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.agent !== undefined) task.agent = patch.agent;\n\t\tif (patch.parentTaskId !== undefined) task.parentTaskId = patch.parentTaskId;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\tif (patch.note !== undefined) task.note = patch.note;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Register or update an agent for the grouped task views. Merges into an\n\t * existing entry with the same id (accumulated stats survive a re-dispatch);\n\t * creates it otherwise.\n\t */\n\tupsertAgent(agent: { id: string } & TaskAgentPatch & Pick<TaskAgent, \"name\" | \"kind\">): TaskAgent {\n\t\tconst existing = this.taskAgents.find((a) => a.id === agent.id);\n\t\tif (existing) {\n\t\t\tthis.applyAgentPatch(existing, agent);\n\t\t\tthis.emit();\n\t\t\treturn existing;\n\t\t}\n\t\tconst created: TaskAgent = {\n\t\t\tid: agent.id,\n\t\t\tname: agent.name,\n\t\t\trole: agent.role,\n\t\t\tkind: agent.kind,\n\t\t\tstate: agent.state,\n\t\t\thandoff: agent.handoff,\n\t\t\tactivity: agent.activity,\n\t\t\tstats: agent.stats,\n\t\t};\n\t\tthis.taskAgents.push(created);\n\t\tthis.emit();\n\t\treturn created;\n\t}\n\n\t/** Patch an existing agent (state/handoff/stats…). Unknown ids are ignored. */\n\tpatchAgent(id: string, patch: TaskAgentPatch): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) return;\n\t\tthis.applyAgentPatch(agent, patch);\n\t\tthis.emit();\n\t}\n\n\t/** Add a usage delta to an agent's running totals (creating them at zero). */\n\taddAgentStats(id: string, delta: { input?: number; output?: number; cost?: number }): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) {\n\t\t\tconsole.warn(`[task-store] addAgentStats: unknown agent id \"${id}\"`);\n\t\t\treturn;\n\t\t}\n\t\tconst stats = agent.stats ?? { input: 0, output: 0, cost: 0 };\n\t\tstats.input += delta.input ?? 0;\n\t\tstats.output += delta.output ?? 0;\n\t\tstats.cost += delta.cost ?? 0;\n\t\tagent.stats = stats;\n\t\tthis.emit();\n\t}\n\n\tagents(): readonly TaskAgent[] {\n\t\treturn this.taskAgents;\n\t}\n\n\tprivate applyAgentPatch(agent: TaskAgent, patch: TaskAgentPatch): void {\n\t\tif (patch.name !== undefined) agent.name = patch.name;\n\t\tif (patch.role !== undefined) agent.role = patch.role;\n\t\tif (patch.kind !== undefined) agent.kind = patch.kind;\n\t\tif (patch.state !== undefined) agent.state = patch.state;\n\t\tif (patch.activity !== undefined) agent.activity = patch.activity;\n\t\tif (patch.handoff !== undefined) agent.handoff = patch.handoff;\n\t\tif (patch.stats !== undefined) agent.stats = patch.stats;\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Drop finished tasks and restart numbering from #1 once the pane is empty.\n\t *\n\t * Called when a new user message arrives: finished tasks from the previous turn\n\t * stay visible (with their final status) for the whole turn and are wiped only\n\t * when the user starts the next turn, so the next turn opens with an empty pane\n\t * and its first task is #1 again. Active (pending/in_progress) tasks are kept —\n\t * a follow-up/steer message can arrive while a subagent is still running, and\n\t * dropping its task here would orphan the live work (its later status update\n\t * would target a removed id and silently vanish). Numbering only restarts once\n\t * no active task survives, so ids never collide with a kept task.\n\t * Agents with accumulated stats are preserved across resets so cross-turn cost\n\t * accounting survives.\n\t */\n\treset(): void {\n\t\tconst active = this.tasks.filter((t) => t.status === \"pending\" || t.status === \"in_progress\");\n\t\tif (active.length === this.tasks.length && this.nextId === 1) return;\n\t\tthis.tasks = active;\n\t\tif (active.length === 0) {\n\t\t\tthis.nextId = 1;\n\t\t\t// Keep agents with non-zero accumulated stats so cross-turn cost accounting\n\t\t\t// survives; drop everyone else (fresh turn opens with a clean roster).\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0),\n\t\t\t);\n\t\t} else {\n\t\t\tconst liveOwners = new Set(active.map(taskOwnerId));\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => liveOwners.has(a.id) || (a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0)),\n\t\t\t);\n\t\t}\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\t/** Wipe all tasks and restart numbering. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.taskAgents = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tif (this.batchDepth > 0) return;\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
|
|
1
|
+
{"version":3,"file":"task-store.d.ts","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\n/**\n * `cancelled` is a user-initiated stop (Esc/abort mid-run) — deliberately\n * distinct from `failed` so the panel doesn't paint an intentional interrupt\n * as an error.\n */\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\" | \"cancelled\";\n\n/**\n * What kind of background work owns a task, surfaced as a source glyph in the\n * pane. Unset means the main agent. A \"team\" origin is reserved for the\n * hooteams integration and stays unwired until that lands.\n */\nexport type TaskSource = \"subagent\" | \"mcp\";\n\n/**\n * Kind of agent that can own tasks in the pane's grouped views:\n * the main session (orchestrator), a spawned subagent, or a named\n * team role-agent (e.g. fed by a hooteams bridge).\n */\nexport type TaskAgentKind = \"main\" | \"subagent\" | \"role\";\n\n/** Lifecycle word shown as the agent's `[state]` tag in grouped views. */\nexport type TaskAgentState = \"active\" | \"running\" | \"done\" | \"queued\" | \"idle\" | \"waiting\" | \"failed\" | \"cancelled\";\n\n/**\n * An agent that owns tasks, rendered as a group header in the pane's\n * `subagents` / `teams` views. Subagent dispatches register themselves here;\n * external orchestrators (hooteams) can upsert role-agents with handoffs.\n */\nexport interface TaskAgent {\n\treadonly id: string;\n\tname: string;\n\t/** Short descriptor after the name: \"orchestrator\", \"subagent\", or a team role like \"architect\". */\n\trole?: string;\n\tkind: TaskAgentKind;\n\tstate?: TaskAgentState;\n\t/** Handoff arrow text for team views (e.g. \"→ reviewer\", \"← builder\"). */\n\thandoff?: string;\n\t/**\n\t * Live activity descriptor for a running subagent (e.g. the tool it is\n\t * currently executing), fed by the pool's `task_progress` events. Empty string\n\t * means \"no current activity\" — `applyAgentPatch` skips `undefined`, so callers\n\t * clear it by patching `\"\"`, not `undefined`.\n\t */\n\tactivity?: string;\n\t/** Per-agent token + cost totals, shown right-aligned on the group header. */\n\tstats?: { input: number; output: number; cost: number };\n}\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent delegation vs MCP tool call; unset = main agent); drives the pane's source glyph. */\n\tsource?: TaskSource;\n\t/**\n\t * Origin label shown as the row's `[tag]` in the task pane: the subagent\n\t * type for delegations (e.g. \"explore\"), the MCP server name for MCP tasks\n\t * (e.g. \"github\").\n\t */\n\tsubagentMode?: string;\n\t/** Id of the owning TaskAgent; drives grouping in the pane's subagents/teams views. */\n\tagent?: string;\n\t/**\n\t * Id of the task that spawned this one, linking a dispatched subagent (and its\n\t * own delegations) back to the Task call that created it. Root tasks omit it.\n\t * Drives the subagents lens's recursive task tree: a node's children are the\n\t * tasks whose `parentTaskId` is its id, so nesting deeper than one level (a\n\t * subagent that spawns a subagent) is visible. Set when a child subagent's task\n\t * subtree is merged into the parent (see finalizeDispatchResult).\n\t */\n\tparentTaskId?: number;\n\t/**\n\t * Id of the main-agent (TodoWrite) task this run is working on, set at\n\t * dispatch time when exactly one plan item is in_progress. Purely a display\n\t * link: the flat (\"tasks\") lens nests the run under its plan item so the\n\t * plan and the agents executing it read as one picture. Deliberately NOT\n\t * `parentTaskId` — that field drives the subagents tree and the\n\t * cross-process subtree merge, and overloading it would move runs between\n\t * lenses. A dangling link (the todo was removed/replaced) is simply not\n\t * rendered.\n\t */\n\tlinkedTaskId?: number;\n\t/**\n\t * Short warning note surfaced as a ⚠ cue in the task pane (e.g. the subagent\n\t * fell back to the inherited model, or was skipped because the provider was\n\t * exhausted). Kept terse so it fits the row's right column.\n\t */\n\tnote?: string;\n\t/**\n\t * Canonical TodoWrite item content for main-plan tasks. `title` is the\n\t * *display* form and legitimately changes between `content` and `activeForm`\n\t * as the item's status moves, so it cannot identify an item across TodoWrite\n\t * calls. This field can: the tool reconciles incoming items against it by\n\t * identity first (position only as a fallback), keeping task ids — and the\n\t * subagent runs linked to them — attached to the same plan item when the\n\t * list is reordered or shrunk.\n\t */\n\ttodoContent?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\nexport interface CreateTaskOptions {\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tagent?: string;\n\tparentTaskId?: number;\n\tlinkedTaskId?: number;\n}\n\nexport type TaskPatch = Partial<\n\tPick<\n\t\tTask,\n\t\t| \"title\"\n\t\t| \"status\"\n\t\t| \"source\"\n\t\t| \"subagentMode\"\n\t\t| \"agent\"\n\t\t| \"usage\"\n\t\t| \"note\"\n\t\t| \"parentTaskId\"\n\t\t| \"linkedTaskId\"\n\t\t| \"todoContent\"\n\t>\n>;\n\nexport type TaskAgentPatch = Partial<Omit<TaskAgent, \"id\">>;\n\n/**\n * Owner group for a task when no explicit agent is set: subagent-sourced work\n * falls into a generic \"subagent\" group, everything else belongs to main.\n * Shared by the store's reset() and the pane's grouped views so the two never\n * disagree about which agents still own live tasks.\n */\nexport function taskOwnerId(task: Pick<Task, \"agent\" | \"source\">): string {\n\tif (task.agent) return task.agent;\n\treturn task.source === \"subagent\" ? \"subagent\" : \"main\";\n}\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate taskAgents: TaskAgent[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\tprivate batchDepth = 0;\n\tprivate mutationCount = 0;\n\n\t/**\n\t * Monotonic counter bumped on every mutation (including those inside a\n\t * batch). Lets renderers cache derived state and invalidate it cheaply by\n\t * comparing versions instead of deep-diffing tasks/agents.\n\t */\n\tversion(): number {\n\t\treturn this.mutationCount;\n\t}\n\n\t/**\n\t * Run a series of mutations without notifying listeners until the batch\n\t * completes, so the TUI renders once instead of per-item. Nested batches are\n\t * supported: only the outermost batch flushes.\n\t */\n\tbatch(fn: () => void): void {\n\t\tthis.batchDepth++;\n\t\ttry {\n\t\t\tfn();\n\t\t} finally {\n\t\t\tthis.batchDepth--;\n\t\t\tif (this.batchDepth === 0) this.emit();\n\t\t}\n\t}\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsource: options.source,\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tagent: options.agent,\n\t\t\tparentTaskId: options.parentTaskId,\n\t\t\tlinkedTaskId: options.linkedTaskId,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) {\n\t\t\tconsole.warn(`[task-store] update: unknown task id ${id}`);\n\t\t\treturn;\n\t\t}\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.source !== undefined) task.source = patch.source;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.agent !== undefined) task.agent = patch.agent;\n\t\tif (patch.parentTaskId !== undefined) task.parentTaskId = patch.parentTaskId;\n\t\tif (patch.linkedTaskId !== undefined) task.linkedTaskId = patch.linkedTaskId;\n\t\tif (patch.todoContent !== undefined) task.todoContent = patch.todoContent;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\t// `note` is clearable: passing `note: undefined` explicitly removes a stale\n\t\t// ⚠ cue (e.g. a warning from a previous run of the same task row). Callers\n\t\t// that don't mention `note` leave it untouched.\n\t\tif (\"note\" in patch) task.note = patch.note;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Register or update an agent for the grouped task views. Merges into an\n\t * existing entry with the same id (accumulated stats survive a re-dispatch);\n\t * creates it otherwise.\n\t */\n\tupsertAgent(agent: { id: string } & TaskAgentPatch & Pick<TaskAgent, \"name\" | \"kind\">): TaskAgent {\n\t\tconst existing = this.taskAgents.find((a) => a.id === agent.id);\n\t\tif (existing) {\n\t\t\tthis.applyAgentPatch(existing, agent);\n\t\t\tthis.emit();\n\t\t\treturn existing;\n\t\t}\n\t\tconst created: TaskAgent = {\n\t\t\tid: agent.id,\n\t\t\tname: agent.name,\n\t\t\trole: agent.role,\n\t\t\tkind: agent.kind,\n\t\t\tstate: agent.state,\n\t\t\thandoff: agent.handoff,\n\t\t\tactivity: agent.activity,\n\t\t\tstats: agent.stats,\n\t\t};\n\t\tthis.taskAgents.push(created);\n\t\tthis.emit();\n\t\treturn created;\n\t}\n\n\t/** Patch an existing agent (state/handoff/stats…). Unknown ids are ignored. */\n\tpatchAgent(id: string, patch: TaskAgentPatch): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) return;\n\t\tthis.applyAgentPatch(agent, patch);\n\t\tthis.emit();\n\t}\n\n\t/** Add a usage delta to an agent's running totals (creating them at zero). */\n\taddAgentStats(id: string, delta: { input?: number; output?: number; cost?: number }): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) {\n\t\t\tconsole.warn(`[task-store] addAgentStats: unknown agent id \"${id}\"`);\n\t\t\treturn;\n\t\t}\n\t\tconst stats = agent.stats ?? { input: 0, output: 0, cost: 0 };\n\t\tstats.input += delta.input ?? 0;\n\t\tstats.output += delta.output ?? 0;\n\t\tstats.cost += delta.cost ?? 0;\n\t\tagent.stats = stats;\n\t\tthis.emit();\n\t}\n\n\tagents(): readonly TaskAgent[] {\n\t\treturn this.taskAgents;\n\t}\n\n\tprivate applyAgentPatch(agent: TaskAgent, patch: TaskAgentPatch): void {\n\t\tif (patch.name !== undefined) agent.name = patch.name;\n\t\tif (patch.role !== undefined) agent.role = patch.role;\n\t\tif (patch.kind !== undefined) agent.kind = patch.kind;\n\t\tif (patch.state !== undefined) agent.state = patch.state;\n\t\tif (patch.activity !== undefined) agent.activity = patch.activity;\n\t\tif (patch.handoff !== undefined) agent.handoff = patch.handoff;\n\t\tif (patch.stats !== undefined) agent.stats = patch.stats;\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Arrange the given tasks (by id) into the specified relative order, keeping\n\t * every other task fixed: the matched tasks permute among their existing\n\t * array slots. Lets TodoWrite render the plan in list order while ids stay\n\t * pinned to their items (its identity-based reconcile). Ignored unless every\n\t * id resolves to a task.\n\t */\n\tarrange(ids: readonly number[]): void {\n\t\tconst byId = new Map(this.tasks.map((t) => [t.id, t]));\n\t\tconst ordered = ids.map((id) => byId.get(id)).filter((t): t is Task => t !== undefined);\n\t\tif (ordered.length !== ids.length) return;\n\t\tconst idSet = new Set(ids);\n\t\tconst slots: number[] = [];\n\t\tfor (let i = 0; i < this.tasks.length; i++) {\n\t\t\tif (idSet.has(this.tasks[i]!.id)) slots.push(i);\n\t\t}\n\t\tif (slots.length !== ordered.length) return;\n\t\tfor (let k = 0; k < slots.length; k++) {\n\t\t\tthis.tasks[slots[k]!] = ordered[k]!;\n\t\t}\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Drop finished tasks and restart numbering from #1 once the pane is empty.\n\t *\n\t * Called when a new user message arrives: finished tasks from the previous turn\n\t * stay visible (with their final status) for the whole turn and are wiped only\n\t * when the user starts the next turn, so the next turn opens with an empty pane\n\t * and its first task is #1 again. Active (pending/in_progress) tasks are kept —\n\t * a follow-up/steer message can arrive while a subagent is still running, and\n\t * dropping its task here would orphan the live work (its later status update\n\t * would target a removed id and silently vanish). Numbering only restarts once\n\t * no active task survives, so ids never collide with a kept task.\n\t * Agents with accumulated stats are preserved across resets so cross-turn cost\n\t * accounting survives.\n\t */\n\treset(): void {\n\t\tconst active = this.tasks.filter((t) => t.status === \"pending\" || t.status === \"in_progress\");\n\t\tif (active.length === this.tasks.length && this.nextId === 1) return;\n\t\tthis.tasks = active;\n\t\t// Subagent rows are per-run (one roster entry per dispatch), so a settled\n\t\t// run whose task was just dropped is dead weight — keeping it would grow\n\t\t// the roster without bound across turns. Role/main agents with non-zero\n\t\t// accumulated stats survive so cross-turn team cost accounting holds.\n\t\tconst hasStats = (a: TaskAgent) => a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0);\n\t\tif (active.length === 0) {\n\t\t\tthis.nextId = 1;\n\t\t\tthis.taskAgents = this.taskAgents.filter((a) => a.kind !== \"subagent\" && hasStats(a));\n\t\t} else {\n\t\t\tconst liveOwners = new Set(active.map(taskOwnerId));\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => liveOwners.has(a.id) || (a.kind !== \"subagent\" && hasStats(a)),\n\t\t\t);\n\t\t}\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\t/** Wipe all tasks and restart numbering. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.taskAgents = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tthis.mutationCount++;\n\t\tif (this.batchDepth > 0) return;\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
|
package/dist/core/task-store.js
CHANGED
|
@@ -23,6 +23,15 @@ class TaskStore {
|
|
|
23
23
|
nextId = 1;
|
|
24
24
|
listeners = new Set();
|
|
25
25
|
batchDepth = 0;
|
|
26
|
+
mutationCount = 0;
|
|
27
|
+
/**
|
|
28
|
+
* Monotonic counter bumped on every mutation (including those inside a
|
|
29
|
+
* batch). Lets renderers cache derived state and invalidate it cheaply by
|
|
30
|
+
* comparing versions instead of deep-diffing tasks/agents.
|
|
31
|
+
*/
|
|
32
|
+
version() {
|
|
33
|
+
return this.mutationCount;
|
|
34
|
+
}
|
|
26
35
|
/**
|
|
27
36
|
* Run a series of mutations without notifying listeners until the batch
|
|
28
37
|
* completes, so the TUI renders once instead of per-item. Nested batches are
|
|
@@ -49,6 +58,7 @@ class TaskStore {
|
|
|
49
58
|
subagentMode: options.subagentMode,
|
|
50
59
|
agent: options.agent,
|
|
51
60
|
parentTaskId: options.parentTaskId,
|
|
61
|
+
linkedTaskId: options.linkedTaskId,
|
|
52
62
|
createdAt: now,
|
|
53
63
|
updatedAt: now,
|
|
54
64
|
};
|
|
@@ -74,9 +84,16 @@ class TaskStore {
|
|
|
74
84
|
task.agent = patch.agent;
|
|
75
85
|
if (patch.parentTaskId !== undefined)
|
|
76
86
|
task.parentTaskId = patch.parentTaskId;
|
|
87
|
+
if (patch.linkedTaskId !== undefined)
|
|
88
|
+
task.linkedTaskId = patch.linkedTaskId;
|
|
89
|
+
if (patch.todoContent !== undefined)
|
|
90
|
+
task.todoContent = patch.todoContent;
|
|
77
91
|
if (patch.usage !== undefined)
|
|
78
92
|
task.usage = patch.usage;
|
|
79
|
-
|
|
93
|
+
// `note` is clearable: passing `note: undefined` explicitly removes a stale
|
|
94
|
+
// ⚠ cue (e.g. a warning from a previous run of the same task row). Callers
|
|
95
|
+
// that don't mention `note` leave it untouched.
|
|
96
|
+
if ("note" in patch)
|
|
80
97
|
task.note = patch.note;
|
|
81
98
|
task.updatedAt = Date.now();
|
|
82
99
|
this.emit();
|
|
@@ -155,6 +172,31 @@ class TaskStore {
|
|
|
155
172
|
this.tasks.splice(idx, 1);
|
|
156
173
|
this.emit();
|
|
157
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Arrange the given tasks (by id) into the specified relative order, keeping
|
|
177
|
+
* every other task fixed: the matched tasks permute among their existing
|
|
178
|
+
* array slots. Lets TodoWrite render the plan in list order while ids stay
|
|
179
|
+
* pinned to their items (its identity-based reconcile). Ignored unless every
|
|
180
|
+
* id resolves to a task.
|
|
181
|
+
*/
|
|
182
|
+
arrange(ids) {
|
|
183
|
+
const byId = new Map(this.tasks.map((t) => [t.id, t]));
|
|
184
|
+
const ordered = ids.map((id) => byId.get(id)).filter((t) => t !== undefined);
|
|
185
|
+
if (ordered.length !== ids.length)
|
|
186
|
+
return;
|
|
187
|
+
const idSet = new Set(ids);
|
|
188
|
+
const slots = [];
|
|
189
|
+
for (let i = 0; i < this.tasks.length; i++) {
|
|
190
|
+
if (idSet.has(this.tasks[i].id))
|
|
191
|
+
slots.push(i);
|
|
192
|
+
}
|
|
193
|
+
if (slots.length !== ordered.length)
|
|
194
|
+
return;
|
|
195
|
+
for (let k = 0; k < slots.length; k++) {
|
|
196
|
+
this.tasks[slots[k]] = ordered[k];
|
|
197
|
+
}
|
|
198
|
+
this.emit();
|
|
199
|
+
}
|
|
158
200
|
/**
|
|
159
201
|
* Drop finished tasks and restart numbering from #1 once the pane is empty.
|
|
160
202
|
*
|
|
@@ -174,15 +216,18 @@ class TaskStore {
|
|
|
174
216
|
if (active.length === this.tasks.length && this.nextId === 1)
|
|
175
217
|
return;
|
|
176
218
|
this.tasks = active;
|
|
219
|
+
// Subagent rows are per-run (one roster entry per dispatch), so a settled
|
|
220
|
+
// run whose task was just dropped is dead weight — keeping it would grow
|
|
221
|
+
// the roster without bound across turns. Role/main agents with non-zero
|
|
222
|
+
// accumulated stats survive so cross-turn team cost accounting holds.
|
|
223
|
+
const hasStats = (a) => a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0);
|
|
177
224
|
if (active.length === 0) {
|
|
178
225
|
this.nextId = 1;
|
|
179
|
-
|
|
180
|
-
// survives; drop everyone else (fresh turn opens with a clean roster).
|
|
181
|
-
this.taskAgents = this.taskAgents.filter((a) => a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0));
|
|
226
|
+
this.taskAgents = this.taskAgents.filter((a) => a.kind !== "subagent" && hasStats(a));
|
|
182
227
|
}
|
|
183
228
|
else {
|
|
184
229
|
const liveOwners = new Set(active.map(taskOwnerId));
|
|
185
|
-
this.taskAgents = this.taskAgents.filter((a) => liveOwners.has(a.id) || (a.
|
|
230
|
+
this.taskAgents = this.taskAgents.filter((a) => liveOwners.has(a.id) || (a.kind !== "subagent" && hasStats(a)));
|
|
186
231
|
}
|
|
187
232
|
this.emit();
|
|
188
233
|
}
|
|
@@ -203,6 +248,7 @@ class TaskStore {
|
|
|
203
248
|
};
|
|
204
249
|
}
|
|
205
250
|
emit() {
|
|
251
|
+
this.mutationCount++;
|
|
206
252
|
if (this.batchDepth > 0)
|
|
207
253
|
return;
|
|
208
254
|
for (const listener of this.listeners) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-store.js","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAoGH;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,IAAoC,EAAU;IACzE,IAAI,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC;IAClC,OAAO,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC;AAAA,CACxD;AAID,MAAM,SAAS;IACN,KAAK,GAAW,EAAE,CAAC;IACnB,UAAU,GAAgB,EAAE,CAAC;IAC7B,MAAM,GAAG,CAAC,CAAC;IACF,SAAS,GAAG,IAAI,GAAG,EAAY,CAAC;IACzC,UAAU,GAAG,CAAC,CAAC;IAEvB;;;;OAIG;IACH,KAAK,CAAC,EAAc,EAAQ;QAC3B,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,IAAI,CAAC;YACJ,EAAE,EAAE,CAAC;QACN,CAAC;gBAAS,CAAC;YACV,IAAI,CAAC,UAAU,EAAE,CAAC;YAClB,IAAI,IAAI,CAAC,UAAU,KAAK,CAAC;gBAAE,IAAI,CAAC,IAAI,EAAE,CAAC;QACxC,CAAC;IAAA,CACD;IAED,MAAM,CAAC,KAAa,EAAE,OAAO,GAAsB,EAAE,EAAQ;QAC5D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,IAAI,GAAS;YAClB,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE;YACjB,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,IAAI,iBAAiB;YACxC,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,GAAG;SACd,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IAAA,CACZ;IAED,MAAM,CAAC,EAAU,EAAE,KAAgB,EAAQ;QAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACjD,IAAI,CAAC,IAAI,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,wCAAwC,EAAE,EAAE,CAAC,CAAC;YAC3D,OAAO;QACR,CAAC;QACD,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACxD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;YAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAC3D,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;YAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAC3D,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS;YAAE,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;QAC7E,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACxD,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS;YAAE,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;QAC7E,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACxD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;YAAE,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACrD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED;;;;OAIG;IACH,WAAW,CAAC,KAAyE,EAAa;QACjG,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,EAAE,CAAC,CAAC;QAChE,IAAI,QAAQ,EAAE,CAAC;YACd,IAAI,CAAC,eAAe,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YACtC,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,OAAO,QAAQ,CAAC;QACjB,CAAC;QACD,MAAM,OAAO,GAAc;YAC1B,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,KAAK,EAAE,KAAK,CAAC,KAAK;SAClB,CAAC;QACF,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9B,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,OAAO,OAAO,CAAC;IAAA,CACf;IAED,iFAA+E;IAC/E,UAAU,CAAC,EAAU,EAAE,KAAqB,EAAQ;QACnD,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACvD,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,8EAA8E;IAC9E,aAAa,CAAC,EAAU,EAAE,KAAyD,EAAQ;QAC1F,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACvD,IAAI,CAAC,KAAK,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,iDAAiD,EAAE,GAAG,CAAC,CAAC;YACrE,OAAO;QACR,CAAC;QACD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAC9D,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;QAChC,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;QAClC,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;QAC9B,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,MAAM,GAAyB;QAC9B,OAAO,IAAI,CAAC,UAAU,CAAC;IAAA,CACvB;IAEO,eAAe,CAAC,KAAgB,EAAE,KAAqB,EAAQ;QACtE,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACtD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACtD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACtD,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACzD,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;YAAE,KAAK,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;QAClE,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;YAAE,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QAC/D,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAAA,CACzD;IAED,MAAM,CAAC,EAAU,EAAQ;QACxB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACrD,IAAI,GAAG,KAAK,CAAC,CAAC;YAAE,OAAO;QACvB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED;;;;;;;;;;;;;OAaG;IACH,KAAK,GAAS;QACb,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,KAAK,aAAa,CAAC,CAAC;QAC9F,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACrE,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC;QACpB,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;YAChB,4EAA4E;YAC5E,uEAAuE;YACvE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CACvC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAC/E,CAAC;QACH,CAAC;aAAM,CAAC;YACP,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC;YACpD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CACvC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CACzG,CAAC;QACH,CAAC;QACD,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,IAAI,GAAoB;QACvB,OAAO,IAAI,CAAC,KAAK,CAAC;IAAA,CAClB;IAED,8EAA8E;IAC9E,KAAK,GAAS;QACb,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QAChB,IAAI,CAAC,UAAU,GAAG,EAAE,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QAChB,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,SAAS,CAAC,QAAkB,EAAc;QACzC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE,CAAC;YACZ,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAAA,CAChC,CAAC;IAAA,CACF;IAEO,IAAI,GAAS;QACpB,IAAI,IAAI,CAAC,UAAU,GAAG,CAAC;YAAE,OAAO;QAChC,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACvC,QAAQ,EAAE,CAAC;QACZ,CAAC;IAAA,CACD;CACD;AAED,uCAAuC;AACvC,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\";\n\n/**\n * What kind of background work owns a task, surfaced as a source glyph in the\n * pane. Unset means the main agent. A \"team\" origin is reserved for the\n * hooteams integration and stays unwired until that lands.\n */\nexport type TaskSource = \"subagent\" | \"mcp\";\n\n/**\n * Kind of agent that can own tasks in the pane's grouped views:\n * the main session (orchestrator), a spawned subagent, or a named\n * team role-agent (e.g. fed by a hooteams bridge).\n */\nexport type TaskAgentKind = \"main\" | \"subagent\" | \"role\";\n\n/** Lifecycle word shown as the agent's `[state]` tag in grouped views. */\nexport type TaskAgentState = \"active\" | \"running\" | \"done\" | \"queued\" | \"idle\" | \"waiting\" | \"failed\";\n\n/**\n * An agent that owns tasks, rendered as a group header in the pane's\n * `subagents` / `teams` views. Subagent dispatches register themselves here;\n * external orchestrators (hooteams) can upsert role-agents with handoffs.\n */\nexport interface TaskAgent {\n\treadonly id: string;\n\tname: string;\n\t/** Short descriptor after the name: \"orchestrator\", \"subagent\", or a team role like \"architect\". */\n\trole?: string;\n\tkind: TaskAgentKind;\n\tstate?: TaskAgentState;\n\t/** Handoff arrow text for team views (e.g. \"→ reviewer\", \"← builder\"). */\n\thandoff?: string;\n\t/**\n\t * Live activity descriptor for a running subagent (e.g. the tool it is\n\t * currently executing), fed by the pool's `task_progress` events. Empty string\n\t * means \"no current activity\" — `applyAgentPatch` skips `undefined`, so callers\n\t * clear it by patching `\"\"`, not `undefined`.\n\t */\n\tactivity?: string;\n\t/** Per-agent token + cost totals, shown right-aligned on the group header. */\n\tstats?: { input: number; output: number; cost: number };\n}\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent delegation vs MCP tool call; unset = main agent); drives the pane's source glyph. */\n\tsource?: TaskSource;\n\t/**\n\t * Origin label shown as the row's `[tag]` in the task pane: the subagent\n\t * type for delegations (e.g. \"explore\"), the MCP server name for MCP tasks\n\t * (e.g. \"github\").\n\t */\n\tsubagentMode?: string;\n\t/** Id of the owning TaskAgent; drives grouping in the pane's subagents/teams views. */\n\tagent?: string;\n\t/**\n\t * Id of the task that spawned this one, linking a dispatched subagent (and its\n\t * own delegations) back to the Task call that created it. Root tasks omit it.\n\t * Drives the subagents lens's recursive task tree: a node's children are the\n\t * tasks whose `parentTaskId` is its id, so nesting deeper than one level (a\n\t * subagent that spawns a subagent) is visible. Set when a child subagent's task\n\t * subtree is merged into the parent (see finalizeDispatchResult).\n\t */\n\tparentTaskId?: number;\n\t/**\n\t * Short warning note surfaced as a ⚠ cue in the task pane (e.g. the subagent\n\t * fell back to the inherited model, or was skipped because the provider was\n\t * exhausted). Kept terse so it fits the row's right column.\n\t */\n\tnote?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\nexport interface CreateTaskOptions {\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tagent?: string;\n\tparentTaskId?: number;\n}\n\nexport type TaskPatch = Partial<\n\tPick<Task, \"title\" | \"status\" | \"source\" | \"subagentMode\" | \"agent\" | \"usage\" | \"note\" | \"parentTaskId\">\n>;\n\nexport type TaskAgentPatch = Partial<Omit<TaskAgent, \"id\">>;\n\n/**\n * Owner group for a task when no explicit agent is set: subagent-sourced work\n * falls into a generic \"subagent\" group, everything else belongs to main.\n * Shared by the store's reset() and the pane's grouped views so the two never\n * disagree about which agents still own live tasks.\n */\nexport function taskOwnerId(task: Pick<Task, \"agent\" | \"source\">): string {\n\tif (task.agent) return task.agent;\n\treturn task.source === \"subagent\" ? \"subagent\" : \"main\";\n}\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate taskAgents: TaskAgent[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\tprivate batchDepth = 0;\n\n\t/**\n\t * Run a series of mutations without notifying listeners until the batch\n\t * completes, so the TUI renders once instead of per-item. Nested batches are\n\t * supported: only the outermost batch flushes.\n\t */\n\tbatch(fn: () => void): void {\n\t\tthis.batchDepth++;\n\t\ttry {\n\t\t\tfn();\n\t\t} finally {\n\t\t\tthis.batchDepth--;\n\t\t\tif (this.batchDepth === 0) this.emit();\n\t\t}\n\t}\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsource: options.source,\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tagent: options.agent,\n\t\t\tparentTaskId: options.parentTaskId,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) {\n\t\t\tconsole.warn(`[task-store] update: unknown task id ${id}`);\n\t\t\treturn;\n\t\t}\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.source !== undefined) task.source = patch.source;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.agent !== undefined) task.agent = patch.agent;\n\t\tif (patch.parentTaskId !== undefined) task.parentTaskId = patch.parentTaskId;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\tif (patch.note !== undefined) task.note = patch.note;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Register or update an agent for the grouped task views. Merges into an\n\t * existing entry with the same id (accumulated stats survive a re-dispatch);\n\t * creates it otherwise.\n\t */\n\tupsertAgent(agent: { id: string } & TaskAgentPatch & Pick<TaskAgent, \"name\" | \"kind\">): TaskAgent {\n\t\tconst existing = this.taskAgents.find((a) => a.id === agent.id);\n\t\tif (existing) {\n\t\t\tthis.applyAgentPatch(existing, agent);\n\t\t\tthis.emit();\n\t\t\treturn existing;\n\t\t}\n\t\tconst created: TaskAgent = {\n\t\t\tid: agent.id,\n\t\t\tname: agent.name,\n\t\t\trole: agent.role,\n\t\t\tkind: agent.kind,\n\t\t\tstate: agent.state,\n\t\t\thandoff: agent.handoff,\n\t\t\tactivity: agent.activity,\n\t\t\tstats: agent.stats,\n\t\t};\n\t\tthis.taskAgents.push(created);\n\t\tthis.emit();\n\t\treturn created;\n\t}\n\n\t/** Patch an existing agent (state/handoff/stats…). Unknown ids are ignored. */\n\tpatchAgent(id: string, patch: TaskAgentPatch): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) return;\n\t\tthis.applyAgentPatch(agent, patch);\n\t\tthis.emit();\n\t}\n\n\t/** Add a usage delta to an agent's running totals (creating them at zero). */\n\taddAgentStats(id: string, delta: { input?: number; output?: number; cost?: number }): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) {\n\t\t\tconsole.warn(`[task-store] addAgentStats: unknown agent id \"${id}\"`);\n\t\t\treturn;\n\t\t}\n\t\tconst stats = agent.stats ?? { input: 0, output: 0, cost: 0 };\n\t\tstats.input += delta.input ?? 0;\n\t\tstats.output += delta.output ?? 0;\n\t\tstats.cost += delta.cost ?? 0;\n\t\tagent.stats = stats;\n\t\tthis.emit();\n\t}\n\n\tagents(): readonly TaskAgent[] {\n\t\treturn this.taskAgents;\n\t}\n\n\tprivate applyAgentPatch(agent: TaskAgent, patch: TaskAgentPatch): void {\n\t\tif (patch.name !== undefined) agent.name = patch.name;\n\t\tif (patch.role !== undefined) agent.role = patch.role;\n\t\tif (patch.kind !== undefined) agent.kind = patch.kind;\n\t\tif (patch.state !== undefined) agent.state = patch.state;\n\t\tif (patch.activity !== undefined) agent.activity = patch.activity;\n\t\tif (patch.handoff !== undefined) agent.handoff = patch.handoff;\n\t\tif (patch.stats !== undefined) agent.stats = patch.stats;\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Drop finished tasks and restart numbering from #1 once the pane is empty.\n\t *\n\t * Called when a new user message arrives: finished tasks from the previous turn\n\t * stay visible (with their final status) for the whole turn and are wiped only\n\t * when the user starts the next turn, so the next turn opens with an empty pane\n\t * and its first task is #1 again. Active (pending/in_progress) tasks are kept —\n\t * a follow-up/steer message can arrive while a subagent is still running, and\n\t * dropping its task here would orphan the live work (its later status update\n\t * would target a removed id and silently vanish). Numbering only restarts once\n\t * no active task survives, so ids never collide with a kept task.\n\t * Agents with accumulated stats are preserved across resets so cross-turn cost\n\t * accounting survives.\n\t */\n\treset(): void {\n\t\tconst active = this.tasks.filter((t) => t.status === \"pending\" || t.status === \"in_progress\");\n\t\tif (active.length === this.tasks.length && this.nextId === 1) return;\n\t\tthis.tasks = active;\n\t\tif (active.length === 0) {\n\t\t\tthis.nextId = 1;\n\t\t\t// Keep agents with non-zero accumulated stats so cross-turn cost accounting\n\t\t\t// survives; drop everyone else (fresh turn opens with a clean roster).\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0),\n\t\t\t);\n\t\t} else {\n\t\t\tconst liveOwners = new Set(active.map(taskOwnerId));\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => liveOwners.has(a.id) || (a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0)),\n\t\t\t);\n\t\t}\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\t/** Wipe all tasks and restart numbering. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.taskAgents = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tif (this.batchDepth > 0) return;\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
|
|
1
|
+
{"version":3,"file":"task-store.js","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\n/**\n * `cancelled` is a user-initiated stop (Esc/abort mid-run) — deliberately\n * distinct from `failed` so the panel doesn't paint an intentional interrupt\n * as an error.\n */\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\" | \"cancelled\";\n\n/**\n * What kind of background work owns a task, surfaced as a source glyph in the\n * pane. Unset means the main agent. A \"team\" origin is reserved for the\n * hooteams integration and stays unwired until that lands.\n */\nexport type TaskSource = \"subagent\" | \"mcp\";\n\n/**\n * Kind of agent that can own tasks in the pane's grouped views:\n * the main session (orchestrator), a spawned subagent, or a named\n * team role-agent (e.g. fed by a hooteams bridge).\n */\nexport type TaskAgentKind = \"main\" | \"subagent\" | \"role\";\n\n/** Lifecycle word shown as the agent's `[state]` tag in grouped views. */\nexport type TaskAgentState = \"active\" | \"running\" | \"done\" | \"queued\" | \"idle\" | \"waiting\" | \"failed\" | \"cancelled\";\n\n/**\n * An agent that owns tasks, rendered as a group header in the pane's\n * `subagents` / `teams` views. Subagent dispatches register themselves here;\n * external orchestrators (hooteams) can upsert role-agents with handoffs.\n */\nexport interface TaskAgent {\n\treadonly id: string;\n\tname: string;\n\t/** Short descriptor after the name: \"orchestrator\", \"subagent\", or a team role like \"architect\". */\n\trole?: string;\n\tkind: TaskAgentKind;\n\tstate?: TaskAgentState;\n\t/** Handoff arrow text for team views (e.g. \"→ reviewer\", \"← builder\"). */\n\thandoff?: string;\n\t/**\n\t * Live activity descriptor for a running subagent (e.g. the tool it is\n\t * currently executing), fed by the pool's `task_progress` events. Empty string\n\t * means \"no current activity\" — `applyAgentPatch` skips `undefined`, so callers\n\t * clear it by patching `\"\"`, not `undefined`.\n\t */\n\tactivity?: string;\n\t/** Per-agent token + cost totals, shown right-aligned on the group header. */\n\tstats?: { input: number; output: number; cost: number };\n}\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent delegation vs MCP tool call; unset = main agent); drives the pane's source glyph. */\n\tsource?: TaskSource;\n\t/**\n\t * Origin label shown as the row's `[tag]` in the task pane: the subagent\n\t * type for delegations (e.g. \"explore\"), the MCP server name for MCP tasks\n\t * (e.g. \"github\").\n\t */\n\tsubagentMode?: string;\n\t/** Id of the owning TaskAgent; drives grouping in the pane's subagents/teams views. */\n\tagent?: string;\n\t/**\n\t * Id of the task that spawned this one, linking a dispatched subagent (and its\n\t * own delegations) back to the Task call that created it. Root tasks omit it.\n\t * Drives the subagents lens's recursive task tree: a node's children are the\n\t * tasks whose `parentTaskId` is its id, so nesting deeper than one level (a\n\t * subagent that spawns a subagent) is visible. Set when a child subagent's task\n\t * subtree is merged into the parent (see finalizeDispatchResult).\n\t */\n\tparentTaskId?: number;\n\t/**\n\t * Id of the main-agent (TodoWrite) task this run is working on, set at\n\t * dispatch time when exactly one plan item is in_progress. Purely a display\n\t * link: the flat (\"tasks\") lens nests the run under its plan item so the\n\t * plan and the agents executing it read as one picture. Deliberately NOT\n\t * `parentTaskId` — that field drives the subagents tree and the\n\t * cross-process subtree merge, and overloading it would move runs between\n\t * lenses. A dangling link (the todo was removed/replaced) is simply not\n\t * rendered.\n\t */\n\tlinkedTaskId?: number;\n\t/**\n\t * Short warning note surfaced as a ⚠ cue in the task pane (e.g. the subagent\n\t * fell back to the inherited model, or was skipped because the provider was\n\t * exhausted). Kept terse so it fits the row's right column.\n\t */\n\tnote?: string;\n\t/**\n\t * Canonical TodoWrite item content for main-plan tasks. `title` is the\n\t * *display* form and legitimately changes between `content` and `activeForm`\n\t * as the item's status moves, so it cannot identify an item across TodoWrite\n\t * calls. This field can: the tool reconciles incoming items against it by\n\t * identity first (position only as a fallback), keeping task ids — and the\n\t * subagent runs linked to them — attached to the same plan item when the\n\t * list is reordered or shrunk.\n\t */\n\ttodoContent?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\nexport interface CreateTaskOptions {\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tagent?: string;\n\tparentTaskId?: number;\n\tlinkedTaskId?: number;\n}\n\nexport type TaskPatch = Partial<\n\tPick<\n\t\tTask,\n\t\t| \"title\"\n\t\t| \"status\"\n\t\t| \"source\"\n\t\t| \"subagentMode\"\n\t\t| \"agent\"\n\t\t| \"usage\"\n\t\t| \"note\"\n\t\t| \"parentTaskId\"\n\t\t| \"linkedTaskId\"\n\t\t| \"todoContent\"\n\t>\n>;\n\nexport type TaskAgentPatch = Partial<Omit<TaskAgent, \"id\">>;\n\n/**\n * Owner group for a task when no explicit agent is set: subagent-sourced work\n * falls into a generic \"subagent\" group, everything else belongs to main.\n * Shared by the store's reset() and the pane's grouped views so the two never\n * disagree about which agents still own live tasks.\n */\nexport function taskOwnerId(task: Pick<Task, \"agent\" | \"source\">): string {\n\tif (task.agent) return task.agent;\n\treturn task.source === \"subagent\" ? \"subagent\" : \"main\";\n}\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate taskAgents: TaskAgent[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\tprivate batchDepth = 0;\n\tprivate mutationCount = 0;\n\n\t/**\n\t * Monotonic counter bumped on every mutation (including those inside a\n\t * batch). Lets renderers cache derived state and invalidate it cheaply by\n\t * comparing versions instead of deep-diffing tasks/agents.\n\t */\n\tversion(): number {\n\t\treturn this.mutationCount;\n\t}\n\n\t/**\n\t * Run a series of mutations without notifying listeners until the batch\n\t * completes, so the TUI renders once instead of per-item. Nested batches are\n\t * supported: only the outermost batch flushes.\n\t */\n\tbatch(fn: () => void): void {\n\t\tthis.batchDepth++;\n\t\ttry {\n\t\t\tfn();\n\t\t} finally {\n\t\t\tthis.batchDepth--;\n\t\t\tif (this.batchDepth === 0) this.emit();\n\t\t}\n\t}\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsource: options.source,\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tagent: options.agent,\n\t\t\tparentTaskId: options.parentTaskId,\n\t\t\tlinkedTaskId: options.linkedTaskId,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) {\n\t\t\tconsole.warn(`[task-store] update: unknown task id ${id}`);\n\t\t\treturn;\n\t\t}\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.source !== undefined) task.source = patch.source;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.agent !== undefined) task.agent = patch.agent;\n\t\tif (patch.parentTaskId !== undefined) task.parentTaskId = patch.parentTaskId;\n\t\tif (patch.linkedTaskId !== undefined) task.linkedTaskId = patch.linkedTaskId;\n\t\tif (patch.todoContent !== undefined) task.todoContent = patch.todoContent;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\t// `note` is clearable: passing `note: undefined` explicitly removes a stale\n\t\t// ⚠ cue (e.g. a warning from a previous run of the same task row). Callers\n\t\t// that don't mention `note` leave it untouched.\n\t\tif (\"note\" in patch) task.note = patch.note;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Register or update an agent for the grouped task views. Merges into an\n\t * existing entry with the same id (accumulated stats survive a re-dispatch);\n\t * creates it otherwise.\n\t */\n\tupsertAgent(agent: { id: string } & TaskAgentPatch & Pick<TaskAgent, \"name\" | \"kind\">): TaskAgent {\n\t\tconst existing = this.taskAgents.find((a) => a.id === agent.id);\n\t\tif (existing) {\n\t\t\tthis.applyAgentPatch(existing, agent);\n\t\t\tthis.emit();\n\t\t\treturn existing;\n\t\t}\n\t\tconst created: TaskAgent = {\n\t\t\tid: agent.id,\n\t\t\tname: agent.name,\n\t\t\trole: agent.role,\n\t\t\tkind: agent.kind,\n\t\t\tstate: agent.state,\n\t\t\thandoff: agent.handoff,\n\t\t\tactivity: agent.activity,\n\t\t\tstats: agent.stats,\n\t\t};\n\t\tthis.taskAgents.push(created);\n\t\tthis.emit();\n\t\treturn created;\n\t}\n\n\t/** Patch an existing agent (state/handoff/stats…). Unknown ids are ignored. */\n\tpatchAgent(id: string, patch: TaskAgentPatch): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) return;\n\t\tthis.applyAgentPatch(agent, patch);\n\t\tthis.emit();\n\t}\n\n\t/** Add a usage delta to an agent's running totals (creating them at zero). */\n\taddAgentStats(id: string, delta: { input?: number; output?: number; cost?: number }): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) {\n\t\t\tconsole.warn(`[task-store] addAgentStats: unknown agent id \"${id}\"`);\n\t\t\treturn;\n\t\t}\n\t\tconst stats = agent.stats ?? { input: 0, output: 0, cost: 0 };\n\t\tstats.input += delta.input ?? 0;\n\t\tstats.output += delta.output ?? 0;\n\t\tstats.cost += delta.cost ?? 0;\n\t\tagent.stats = stats;\n\t\tthis.emit();\n\t}\n\n\tagents(): readonly TaskAgent[] {\n\t\treturn this.taskAgents;\n\t}\n\n\tprivate applyAgentPatch(agent: TaskAgent, patch: TaskAgentPatch): void {\n\t\tif (patch.name !== undefined) agent.name = patch.name;\n\t\tif (patch.role !== undefined) agent.role = patch.role;\n\t\tif (patch.kind !== undefined) agent.kind = patch.kind;\n\t\tif (patch.state !== undefined) agent.state = patch.state;\n\t\tif (patch.activity !== undefined) agent.activity = patch.activity;\n\t\tif (patch.handoff !== undefined) agent.handoff = patch.handoff;\n\t\tif (patch.stats !== undefined) agent.stats = patch.stats;\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Arrange the given tasks (by id) into the specified relative order, keeping\n\t * every other task fixed: the matched tasks permute among their existing\n\t * array slots. Lets TodoWrite render the plan in list order while ids stay\n\t * pinned to their items (its identity-based reconcile). Ignored unless every\n\t * id resolves to a task.\n\t */\n\tarrange(ids: readonly number[]): void {\n\t\tconst byId = new Map(this.tasks.map((t) => [t.id, t]));\n\t\tconst ordered = ids.map((id) => byId.get(id)).filter((t): t is Task => t !== undefined);\n\t\tif (ordered.length !== ids.length) return;\n\t\tconst idSet = new Set(ids);\n\t\tconst slots: number[] = [];\n\t\tfor (let i = 0; i < this.tasks.length; i++) {\n\t\t\tif (idSet.has(this.tasks[i]!.id)) slots.push(i);\n\t\t}\n\t\tif (slots.length !== ordered.length) return;\n\t\tfor (let k = 0; k < slots.length; k++) {\n\t\t\tthis.tasks[slots[k]!] = ordered[k]!;\n\t\t}\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Drop finished tasks and restart numbering from #1 once the pane is empty.\n\t *\n\t * Called when a new user message arrives: finished tasks from the previous turn\n\t * stay visible (with their final status) for the whole turn and are wiped only\n\t * when the user starts the next turn, so the next turn opens with an empty pane\n\t * and its first task is #1 again. Active (pending/in_progress) tasks are kept —\n\t * a follow-up/steer message can arrive while a subagent is still running, and\n\t * dropping its task here would orphan the live work (its later status update\n\t * would target a removed id and silently vanish). Numbering only restarts once\n\t * no active task survives, so ids never collide with a kept task.\n\t * Agents with accumulated stats are preserved across resets so cross-turn cost\n\t * accounting survives.\n\t */\n\treset(): void {\n\t\tconst active = this.tasks.filter((t) => t.status === \"pending\" || t.status === \"in_progress\");\n\t\tif (active.length === this.tasks.length && this.nextId === 1) return;\n\t\tthis.tasks = active;\n\t\t// Subagent rows are per-run (one roster entry per dispatch), so a settled\n\t\t// run whose task was just dropped is dead weight — keeping it would grow\n\t\t// the roster without bound across turns. Role/main agents with non-zero\n\t\t// accumulated stats survive so cross-turn team cost accounting holds.\n\t\tconst hasStats = (a: TaskAgent) => a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0);\n\t\tif (active.length === 0) {\n\t\t\tthis.nextId = 1;\n\t\t\tthis.taskAgents = this.taskAgents.filter((a) => a.kind !== \"subagent\" && hasStats(a));\n\t\t} else {\n\t\t\tconst liveOwners = new Set(active.map(taskOwnerId));\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => liveOwners.has(a.id) || (a.kind !== \"subagent\" && hasStats(a)),\n\t\t\t);\n\t\t}\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\t/** Wipe all tasks and restart numbering. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.taskAgents = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tthis.mutationCount++;\n\t\tif (this.batchDepth > 0) return;\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
|