{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport type { ExtensionAPI, ExtensionContext, SessionEntry, SessionStartEvent } from \"@earendil-works/pi-coding-agent\";\nimport { getAgentDir } from \"@earendil-works/pi-coding-agent\";\nimport { FileTodoStore, TODO_LIST_NOT_FOUND, TodoPersistenceError } from \"./store.ts\";\nimport type { TodoBindingEntry, TodoDefinition, TodoListDocument, TodoListView } from \"./types.ts\";\nimport { updateTodoWidget } from \"./widget.ts\";\n\nexport const TODO_BINDING_ENTRY = \"pi-todo-binding\";\nexport const TODO_DIGEST_MESSAGE = \"pi-todo-digest\";\nexport const AGENT_STATUS_REQUEST_CHANNEL = \"pi:agent:status-request\";\n\nconst OWNER_EVIDENCE_WAIT_MS = 50;\n// Turn-end assertion: while a digest is active, queue a fresh digest at every\n// turn end so the model is reminded (e.g. to reconcile completed workers)\n// instead of every 10 turns. Delivery uses \"nextTurn\" (injected at the start\n// of the next run) rather than \"followUp\": followUp forces a continuation turn\n// whose turn_end would queue another digest, an injection loop while the list\n// stays active. The digestQueueKey queue keeps at most one pending digest and\n// cancelDigest removes it once every task is completed (勾销后不再注入).\nconst PERIODIC_REMINDER_TURNS = 1;\nconst DIGEST_TASKS_PER_SECTION = 5;\nconst DIGEST_DIRECTION_LIMIT = 500;\nconst DIGEST_CHAR_LIMIT = 4000;\n\nexport interface TodoRuntimeOptions {\n\tdataDir?: string;\n}\n\ninterface AgentContext {\n\tagentId?: string;\n\tmetadata: Record<string, unknown>;\n}\n\nexport class TodoRuntime {\n\treadonly store: FileTodoStore;\n\tprivate readonly pi: ExtensionAPI;\n\tprivate binding: TodoBindingEntry | undefined;\n\tprivate unboundListId: string | undefined;\n\tprivate context: ExtensionContext | undefined;\n\tprivate agentContext: AgentContext | undefined;\n\tprivate readonly liveOwners = new Set<string>();\n\tprivate readonly pendingOwnerEvidence = new Set<Promise<void>>();\n\tprivate readonly pendingReconcile = new Map<string, string>();\n\tprivate ownerReconciliationPending = false;\n\tprivate digestActive = false;\n\tprivate turnsSinceReminder = 0;\n\n\tconstructor(pi: ExtensionAPI, options: TodoRuntimeOptions = {}) {\n\t\tthis.pi = pi;\n\t\tthis.store = new FileTodoStore(options.dataDir ?? join(getAgentDir(), \"todo\", \"lists\"));\n\t\tthis.agentContext = parseAgentContext(process.env.PI_AGENT_CONTEXT);\n\t}\n\n\tgetListId(): string | undefined {\n\t\treturn this.binding?.list_id ?? undefined;\n\t}\n\n\tgetTaskContext(): { listId: string; taskId: string; agentId?: string } | undefined {\n\t\tconst metadata = this.agentContext?.metadata;\n\t\tif (!metadata) return undefined;\n\t\tconst listId = metadata[\"pi.todo/list-id\"];\n\t\tconst taskId = metadata[\"pi.todo/task-id\"];\n\t\tif (typeof listId !== \"string\" || typeof taskId !== \"string\") return undefined;\n\t\treturn { listId, taskId, agentId: this.agentContext?.agentId };\n\t}\n\n\tasync initialize(event: SessionStartEvent, ctx: ExtensionContext): Promise<void> {\n\t\tthis.context = ctx;\n\t\tthis.liveOwners.clear();\n\t\tthis.ownerReconciliationPending = false;\n\t\tconst agentTask = this.getTaskContext();\n\t\tif (agentTask) {\n\t\t\tif (agentTask.agentId) this.liveOwners.add(agentTask.agentId);\n\t\t\tconst list = await this.withMissingListUnbind(() => this.store.read(agentTask.listId));\n\t\t\tif (!list) return;\n\t\t\tthis.setBinding(list, true);\n\t\t\tawait this.refresh();\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.reason === \"new\") {\n\t\t\tthis.cancelDigest();\n\t\t\tthis.binding = undefined;\n\t\t\tthis.digestActive = false;\n\t\t\tthis.turnsSinceReminder = 0;\n\t\t\tthis.pendingReconcile.clear();\n\t\t\tupdateTodoWidget(ctx, undefined);\n\t\t\treturn;\n\t\t}\n\n\t\tconst restored = latestBinding(ctx.sessionManager.getBranch());\n\t\tif (!restored?.list_id) {\n\t\t\tthis.cancelDigest();\n\t\t\tthis.binding = undefined;\n\t\t\tthis.digestActive = false;\n\t\t\tthis.turnsSinceReminder = 0;\n\t\t\tthis.pendingReconcile.clear();\n\t\t\tupdateTodoWidget(ctx, undefined);\n\t\t\treturn;\n\t\t}\n\t\tconst restoredListId = restored.list_id;\n\n\t\tif (event.reason === \"fork\") {\n\t\t\tconst cloned = await this.withMissingListUnbind(() => this.store.clone(restoredListId, restored.revision));\n\t\t\tif (!cloned) return;\n\t\t\tthis.setBinding(cloned, true);\n\t\t} else {\n\t\t\tconst list = await this.withMissingListUnbind(() => this.store.read(restoredListId));\n\t\t\tif (!list) return;\n\t\t\tthis.setBinding(list, false);\n\t\t\tconst sessionId = ctx.sessionManager.getSessionId();\n\t\t\tthis.liveOwners.add(sessionId);\n\t\t\tthis.ownerReconciliationPending = true;\n\t\t}\n\t\tawait this.refresh();\n\t}\n\n\tasync restoreTree(ctx: ExtensionContext): Promise<void> {\n\t\tthis.context = ctx;\n\t\tconst restored = latestBinding(ctx.sessionManager.getBranch());\n\t\tif (!restored?.list_id) {\n\t\t\tthis.cancelDigest();\n\t\t\tthis.binding = undefined;\n\t\t\tthis.digestActive = false;\n\t\t\tthis.turnsSinceReminder = 0;\n\t\t\tthis.pendingReconcile.clear();\n\t\t\tupdateTodoWidget(ctx, undefined);\n\t\t\treturn;\n\t\t}\n\t\tconst restoredListId = restored.list_id;\n\t\tconst current = await this.withMissingListUnbind(() => this.store.read(restoredListId));\n\t\tif (!current) return;\n\t\tif (restored.revision < current.revision) {\n\t\t\tconst cloned = await this.withMissingListUnbind(() => this.store.clone(restoredListId, restored.revision));\n\t\t\tif (!cloned) return;\n\t\t\tthis.setBinding(cloned, true);\n\t\t} else {\n\t\t\tthis.setBinding(current, false);\n\t\t}\n\t\tawait this.refresh();\n\t}\n\n\tasync replace(globalDirection: string, items: readonly TodoDefinition[]): Promise<TodoListDocument> {\n\t\tconst document = await this.store.create(globalDirection, items);\n\t\tthis.setBinding(document, true);\n\t\tawait this.refresh();\n\t\treturn document;\n\t}\n\n\tasync add(items: readonly TodoDefinition[], globalDirection?: string): Promise<TodoListDocument> {\n\t\tif (!this.binding?.list_id) {\n\t\t\tif (!globalDirection) throw new Error(\"No todo list is active; provide global_direction to create one\");\n\t\t\treturn this.replace(globalDirection, items);\n\t\t}\n\t\treturn this.record(await this.store.add(this.binding.list_id, items));\n\t}\n\n\tasync update(taskId: string, patch: Parameters<FileTodoStore[\"update\"]>[2]): Promise<TodoListDocument> {\n\t\tconst document = await this.record(await this.store.update(this.requireListId(), taskId, patch));\n\t\tif (document.tasks.length > 0 && document.tasks.every((task) => task.status === \"completed\")) {\n\t\t\tawait this.clear();\n\t\t}\n\t\treturn document;\n\t}\n\n\tasync claim(taskId: string, owner?: string, expectedRevision?: number) {\n\t\tconst claim = await this.store.claim(\n\t\t\tthis.requireListId(),\n\t\t\ttaskId,\n\t\t\towner ?? this.agentContext?.agentId ?? this.context?.sessionManager.getSessionId() ?? \"main\",\n\t\t\texpectedRevision,\n\t\t);\n\t\tawait this.record(await this.store.read(this.requireListId()));\n\t\treturn claim;\n\t}\n\n\tasync release(taskId: string): Promise<TodoListDocument> {\n\t\treturn this.record(await this.store.release(this.requireListId(), taskId));\n\t}\n\n\tasync transfer(taskId: string, owner: string): Promise<TodoListDocument> {\n\t\treturn this.record(await this.store.transfer(this.requireListId(), taskId, owner));\n\t}\n\n\tasync confirmOwnerLive(taskId: string, owner: string): Promise<void> {\n\t\tconst evidence = (async () => {\n\t\t\tconst document = await this.store.transfer(this.requireListId(), taskId, owner);\n\t\t\tthis.liveOwners.add(owner);\n\t\t\tawait this.record(document);\n\t\t})();\n\t\tthis.pendingOwnerEvidence.add(evidence);\n\t\ttry {\n\t\t\tawait evidence;\n\t\t} finally {\n\t\t\tthis.pendingOwnerEvidence.delete(evidence);\n\t\t}\n\t}\n\n\tasync syncCurrent(): Promise<TodoListDocument> {\n\t\treturn this.record(await this.store.read(this.requireListId()));\n\t}\n\n\tasync releaseIfOwned(taskId: string, owner: string): Promise<TodoListDocument> {\n\t\treturn this.record(await this.store.releaseIfOwned(this.requireListId(), taskId, owner));\n\t}\n\n\tasync reconcileOwners(): Promise<boolean> {\n\t\tif (!this.ownerReconciliationPending || this.agentContext) return false;\n\t\tconst listId = this.requireListId();\n\t\tconst sessionId = this.context?.sessionManager.getSessionId();\n\t\tif (!sessionId) return false;\n\t\tthis.pi.events.emit(AGENT_STATUS_REQUEST_CHANNEL, {\n\t\t\tversion: 2,\n\t\t\tparentSessionId: sessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t});\n\t\tawait new Promise((resolve) => setTimeout(resolve, OWNER_EVIDENCE_WAIT_MS));\n\t\tawait this.waitForOwnerEvidence();\n\t\tconst list = await this.store.reconcileOwners(listId, this.liveOwners);\n\t\tthis.setBinding(list, true);\n\t\tthis.ownerReconciliationPending = false;\n\t\tawait this.refresh();\n\t\treturn true;\n\t}\n\n\tasync delete(taskId: string): Promise<TodoListDocument> {\n\t\treturn this.record(await this.store.delete(this.requireListId(), taskId));\n\t}\n\n\tasync clear(): Promise<void> {\n\t\tconst listId = this.binding?.list_id;\n\t\tthis.unbind();\n\t\tif (listId) await this.store.removeList(listId);\n\t}\n\n\tasync view(): Promise<TodoListView | undefined> {\n\t\tconst listId = this.binding?.list_id;\n\t\tif (!listId) return undefined;\n\t\treturn this.withMissingListUnbind(() => this.store.view(listId));\n\t}\n\n\tasync getTask(taskId: string) {\n\t\tconst view = await this.view();\n\t\treturn view?.list.tasks.find((task) => task.id === taskId);\n\t}\n\n\t/**\n\t * Records that a worker reported completion for a task that is not checked\n\t * off yet. Keyed by task id so duplicate lifecycle events do not repeat the\n\t * entry; surfaced in the next digest as a \"Needs reconciliation\" section.\n\t */\n\trecordPendingReconcile(taskId: string, agentId: string): void {\n\t\tthis.pendingReconcile.set(taskId, agentId);\n\t}\n\n\tasync digest(): Promise<string | undefined> {\n\t\tconst view = await this.view();\n\t\tif (!view || view.summary.total === view.summary.completed) return undefined;\n\t\tconst active = view.list.tasks.filter((task) => task.status === \"in_progress\");\n\t\tconst lines = [\n\t\t\t`[PI TODO ACTIVE] ${view.summary.completed}/${view.summary.total} completed.`,\n\t\t\t`Snapshot: list ${view.list.id}, revision ${view.list.revision}. After an interruption or agent event, call todo_list before acting.`,\n\t\t\t`Direction: ${truncate(view.list.global_direction, DIGEST_DIRECTION_LIMIT)}`,\n\t\t];\n\t\tappendDigestSection(lines, \"In progress\", active, (task) => `${task.id} (${task.owner ?? \"owned\"})`);\n\t\tappendDigestSection(lines, \"Ready\", view.ready, (task) => `${task.id}: ${task.subject}`);\n\t\tappendDigestSection(lines, \"Blocked\", view.blocked, (task) => `${task.id} <- ${task.depends_on.join(\",\")}`);\n\t\tif (this.pendingReconcile.size > 0) {\n\t\t\tconst entries = [...this.pendingReconcile.entries()]\n\t\t\t\t.map(([taskId, agentId]) => `worker ${agentId} completed but task ${taskId} is not completed`)\n\t\t\t\t.join(\"; \");\n\t\t\tlines.push(`Needs reconciliation: ${entries}. Call todo_update to complete it after verifying.`);\n\t\t}\n\t\tif (view.ready.length > 1 && hasAgentCapability(this.pi.getActiveTools())) {\n\t\t\tlines.push(\n\t\t\t\t\"Several independent tasks are ready. Claim them, then use one background agent_start batch with worker and each claim's metadata. Use explore only for unbound read-only investigation.\",\n\t\t\t);\n\t\t} else if (view.ready.length) {\n\t\t\tlines.push(\"Claim a ready task and continue it in the current session.\");\n\t\t}\n\t\treturn truncate(lines.join(\"\\n\"), DIGEST_CHAR_LIMIT);\n\t}\n\n\tasync injectDigest(deliverAs: \"followUp\" | \"nextTurn\" = \"nextTurn\"): Promise<void> {\n\t\tconst listId = this.binding?.list_id;\n\t\tif (!listId) return;\n\t\tconst content = await this.digest();\n\t\tif (!content) {\n\t\t\tthis.digestActive = false;\n\t\t\tthis.turnsSinceReminder = 0;\n\t\t\tthis.cancelDigest(listId);\n\t\t\treturn;\n\t\t}\n\t\tthis.pi.sendMessage(\n\t\t\t{ customType: TODO_DIGEST_MESSAGE, content, display: false },\n\t\t\t{ triggerTurn: false, deliverAs },\n\t\t);\n\t}\n\n\tasync onTurnEnd(): Promise<void> {\n\t\tif (!this.digestActive) {\n\t\t\tthis.turnsSinceReminder = 0;\n\t\t\treturn;\n\t\t}\n\t\tthis.turnsSinceReminder++;\n\t\tif (this.turnsSinceReminder < PERIODIC_REMINDER_TURNS) return;\n\t\tthis.turnsSinceReminder = 0;\n\t\tawait this.injectDigest(\"nextTurn\");\n\t}\n\n\tcancelDigest(_listId = this.binding?.list_id): void {\n\t\t// Official Pi has no retractable message queue; future digests stop when inactive.\n\t}\n\n\tasync refresh(): Promise<void> {\n\t\tif (!this.context) return;\n\t\tconst view = await this.view();\n\t\tupdateTodoWidget(this.context, view);\n\t\tconst diagnostic = this.store.takeDiagnostic();\n\t\tif (diagnostic) this.context.ui.notify(diagnostic, \"warning\");\n\t}\n\n\tprivate async withMissingListUnbind<T>(operation: () => Promise<T>): Promise<T | undefined> {\n\t\ttry {\n\t\t\treturn await operation();\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof TodoPersistenceError) || error.code !== TODO_LIST_NOT_FOUND) throw error;\n\t\t\tthis.unbind(error instanceof Error ? error.message : String(error));\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate unbind(reason?: string): void {\n\t\tconst listId = this.binding?.list_id;\n\t\tif (listId) this.cancelDigest(listId);\n\t\tthis.binding = undefined;\n\t\tthis.digestActive = false;\n\t\tthis.turnsSinceReminder = 0;\n\t\tthis.pendingReconcile.clear();\n\t\tif (listId !== undefined && this.unboundListId === listId) return;\n\t\tthis.unboundListId = listId ?? undefined;\n\t\tthis.pi.appendEntry<TodoBindingEntry>(TODO_BINDING_ENTRY, {\n\t\t\tversion: 1,\n\t\t\tlist_id: null,\n\t\t\trevision: 0,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t});\n\t\tif (this.context) {\n\t\t\tupdateTodoWidget(this.context, undefined);\n\t\t\tif (reason) this.context.ui.notify(reason, \"warning\");\n\t\t}\n\t}\n\n\tprivate async record(document: TodoListDocument): Promise<TodoListDocument> {\n\t\tthis.setBinding(document, true);\n\t\tthis.prunePendingReconcile(document);\n\t\tawait this.refresh();\n\t\treturn document;\n\t}\n\n\tprivate prunePendingReconcile(document: TodoListDocument): void {\n\t\tif (this.pendingReconcile.size === 0) return;\n\t\tfor (const taskId of [...this.pendingReconcile.keys()]) {\n\t\t\tconst task = document.tasks.find((task) => task.id === taskId);\n\t\t\t// Checked off (completed), released, or deleted: nothing left to reconcile.\n\t\t\tif (!task || task.status !== \"in_progress\") this.pendingReconcile.delete(taskId);\n\t\t}\n\t}\n\n\tprivate setBinding(document: TodoListDocument, persist: boolean): void {\n\t\tthis.unboundListId = undefined;\n\t\tconst previousListId = this.binding?.list_id;\n\t\tif (previousListId && previousListId !== document.id) {\n\t\t\tthis.cancelDigest(previousListId);\n\t\t\tthis.turnsSinceReminder = 0;\n\t\t\tthis.pendingReconcile.clear();\n\t\t}\n\t\tthis.binding = {\n\t\t\tversion: 1,\n\t\t\tlist_id: document.id,\n\t\t\trevision: document.revision,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t};\n\t\tthis.digestActive = document.tasks.some((task) => task.status !== \"completed\");\n\t\tif (!this.digestActive) {\n\t\t\tthis.turnsSinceReminder = 0;\n\t\t\tthis.cancelDigest(document.id);\n\t\t}\n\t\tif (persist) this.pi.appendEntry<TodoBindingEntry>(TODO_BINDING_ENTRY, this.binding);\n\t}\n\n\tprivate requireListId(): string {\n\t\tif (!this.binding?.list_id) throw new Error(\"No todo list is active\");\n\t\treturn this.binding.list_id;\n\t}\n\n\tprivate async waitForOwnerEvidence(): Promise<void> {\n\t\twhile (this.pendingOwnerEvidence.size > 0) {\n\t\t\tawait Promise.allSettled([...this.pendingOwnerEvidence]);\n\t\t}\n\t}\n}\n\nfunction latestBinding(entries: readonly SessionEntry[]): TodoBindingEntry | undefined {\n\tfor (let index = entries.length - 1; index >= 0; index--) {\n\t\tconst entry = entries[index];\n\t\tif (entry?.type !== \"custom\" || entry.customType !== TODO_BINDING_ENTRY) continue;\n\t\tif (isBinding(entry.data)) return entry.data;\n\t}\n\treturn undefined;\n}\n\nfunction isBinding(value: unknown): value is TodoBindingEntry {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tconst record = value as Record<string, unknown>;\n\treturn (\n\t\trecord.version === 1 &&\n\t\t(record.list_id === null || typeof record.list_id === \"string\") &&\n\t\ttypeof record.revision === \"number\" &&\n\t\ttypeof record.timestamp === \"string\"\n\t);\n}\n\nfunction parseAgentContext(raw: string | undefined): AgentContext | undefined {\n\tif (!raw) return undefined;\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(raw);\n\t\tif (typeof parsed !== \"object\" || parsed === null) return undefined;\n\t\tconst record = parsed as Record<string, unknown>;\n\t\tconst metadataValue = record.metadata ?? record;\n\t\tif (typeof metadataValue !== \"object\" || metadataValue === null) return undefined;\n\t\treturn {\n\t\t\tagentId: typeof record.agentId === \"string\" ? record.agentId : undefined,\n\t\t\tmetadata: metadataValue as Record<string, unknown>,\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction hasAgentCapability(activeTools: readonly string[]): boolean {\n\treturn activeTools.includes(\"agent_start\");\n}\n\nfunction appendDigestSection(\n\tlines: string[],\n\tlabel: string,\n\ttasks: readonly TodoListDocument[\"tasks\"][number][],\n\tformat: (task: TodoListDocument[\"tasks\"][number]) => string,\n): void {\n\tif (tasks.length === 0) return;\n\tconst visible = tasks.slice(0, DIGEST_TASKS_PER_SECTION).map(format);\n\tif (tasks.length > visible.length) visible.push(`... ${tasks.length - visible.length} more`);\n\tlines.push(`${label}: ${visible.join(\"; \")}`);\n}\n\nfunction truncate(value: string, limit: number): string {\n\treturn value.length <= limit ? value : `${value.slice(0, Math.max(0, limit - 3))}...`;\n}\n"]}