{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD,eAAO,MAAM,uBAAuB,uBAAuB,CAAC;AAG5D,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,GAAG,MAAM,IAAI,CA0DjG","sourcesContent":["import type { EventBus } from \"@earendil-works/pi-coding-agent\";\nimport type { TodoRuntime } from \"./runtime.ts\";\nimport { TodoValidationError } from \"./scheduler.ts\";\nimport type { AgentLifecycleEvent, TodoAgentMetadata } from \"./types.ts\";\n\nexport const AGENT_LIFECYCLE_CHANNEL = \"pi:agent:lifecycle\";\nconst PROTOCOL_STATE_LIMIT = 2000;\n\nexport function registerAgentLifecycleProtocol(events: EventBus, runtime: TodoRuntime): () => void {\n\tconst seen = new Set<string>();\n\tconst terminalRuns = new Set<string>();\n\tconst currentRuns = new Map<string, { agentId: string; runId: string; timestamp: string }>();\n\tconst operations = new Map<string, Promise<void>>();\n\treturn events.on(AGENT_LIFECYCLE_CHANNEL, async (data) => {\n\t\tconst event = parseLifecycleEvent(data);\n\t\tif (!event) return;\n\t\tconst metadata = parseTodoMetadata(event.metadata);\n\t\tif (!metadata || metadata[\"pi.todo/list-id\"] !== runtime.getListId()) return;\n\t\tconst taskKey = `${metadata[\"pi.todo/list-id\"]}\\0${metadata[\"pi.todo/task-id\"]}`;\n\t\tconst runKey = `${taskKey}\\0${event.agentId}\\0${event.runId}`;\n\t\tconst previous = operations.get(taskKey) ?? Promise.resolve();\n\t\tconst operation = previous\n\t\t\t.catch(() => {})\n\t\t\t.then(async () => {\n\t\t\t\tif (seen.has(event.eventId)) return;\n\t\t\t\ttry {\n\t\t\t\t\tconst current = currentRuns.get(taskKey);\n\t\t\t\t\tif (isActiveStatus(event.status)) {\n\t\t\t\t\t\tif (terminalRuns.has(runKey)) return;\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tcurrent &&\n\t\t\t\t\t\t\t(current.agentId !== event.agentId || current.runId !== event.runId) &&\n\t\t\t\t\t\t\tcurrent.timestamp > event.timestamp\n\t\t\t\t\t\t)\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\tawait runtime.confirmOwnerLive(metadata[\"pi.todo/task-id\"], event.agentId);\n\t\t\t\t\t\tcurrentRuns.set(taskKey, { agentId: event.agentId, runId: event.runId, timestamp: event.timestamp });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (current && (current.agentId !== event.agentId || current.runId !== event.runId)) return;\n\t\t\t\t\t\tif (event.status === \"completed\") {\n\t\t\t\t\t\t\tconst task = await runtime.getTask(metadata[\"pi.todo/task-id\"]);\n\t\t\t\t\t\t\tif (task?.status !== \"completed\") {\n\t\t\t\t\t\t\t\t// Surface the mismatch to the model instead of silently swallowing it:\n\t\t\t\t\t\t\t\t// the pending entry is rendered in the next digest so the coordinator\n\t\t\t\t\t\t\t\t// can check the task off with todo_update after verifying.\n\t\t\t\t\t\t\t\truntime.recordPendingReconcile(metadata[\"pi.todo/task-id\"], event.agentId);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tawait runtime.syncCurrent();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tawait runtime.releaseIfOwned(metadata[\"pi.todo/task-id\"], event.agentId);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tremember(terminalRuns, runKey);\n\t\t\t\t\t}\n\t\t\t\t\tremember(seen, event.eventId);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (!(error instanceof TodoValidationError)) throw error;\n\t\t\t\t}\n\t\t\t});\n\t\toperations.set(taskKey, operation);\n\t\ttry {\n\t\t\tawait operation;\n\t\t} finally {\n\t\t\tif (operations.get(taskKey) === operation) operations.delete(taskKey);\n\t\t}\n\t});\n}\n\nfunction remember(values: Set<string>, value: string): void {\n\tvalues.add(value);\n\tif (values.size > PROTOCOL_STATE_LIMIT) values.delete(values.values().next().value ?? \"\");\n}\n\nfunction isActiveStatus(status: AgentLifecycleEvent[\"status\"]): boolean {\n\treturn status === \"queued\" || status === \"started\" || status === \"running\";\n}\n\nfunction parseLifecycleEvent(value: unknown): AgentLifecycleEvent | undefined {\n\tif (typeof value !== \"object\" || value === null) return undefined;\n\tconst record = value as Record<string, unknown>;\n\tif (\n\t\trecord.version !== 2 ||\n\t\ttypeof record.eventId !== \"string\" ||\n\t\ttypeof record.runId !== \"string\" ||\n\t\ttypeof record.agentId !== \"string\" ||\n\t\ttypeof record.parentSessionId !== \"string\" ||\n\t\ttypeof record.timestamp !== \"string\" ||\n\t\ttypeof record.metadata !== \"object\" ||\n\t\trecord.metadata === null ||\n\t\t!isLifecycleStatus(record.status)\n\t) {\n\t\treturn undefined;\n\t}\n\treturn record as unknown as AgentLifecycleEvent;\n}\n\nfunction parseTodoMetadata(metadata: Record<string, unknown>): TodoAgentMetadata | undefined {\n\tconst listId = metadata[\"pi.todo/list-id\"];\n\tconst taskId = metadata[\"pi.todo/task-id\"];\n\tif (typeof listId !== \"string\" || typeof taskId !== \"string\") return undefined;\n\treturn { \"pi.todo/list-id\": listId, \"pi.todo/task-id\": taskId };\n}\n\nfunction isLifecycleStatus(value: unknown): value is AgentLifecycleEvent[\"status\"] {\n\treturn [\"queued\", \"started\", \"running\", \"completed\", \"failed\", \"stopped\", \"interrupted\"].includes(String(value));\n}\n"]}