blun-king-cli 9.1.563 → 9.1.565

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.
Files changed (85) hide show
  1. package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
  2. package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
  3. package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
  4. package/agent-spine-plugin/.mcp.json +8 -8
  5. package/agent-spine-plugin/CONTRIBUTING.md +52 -0
  6. package/agent-spine-plugin/LICENSE +186 -186
  7. package/agent-spine-plugin/README.md +394 -0
  8. package/agent-spine-plugin/SECURITY.md +47 -0
  9. package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
  10. package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
  11. package/agent-spine-plugin/bin/agentspine.js +7 -7
  12. package/agent-spine-plugin/blun.plugin.json +33 -33
  13. package/agent-spine-plugin/hooks/codex.json +47 -47
  14. package/agent-spine-plugin/hooks/hooks.json +106 -106
  15. package/agent-spine-plugin/hooks/version.json +5 -5
  16. package/agent-spine-plugin/package.json +69 -69
  17. package/agent-spine-plugin/scripts/check-hosts.js +199 -199
  18. package/agent-spine-plugin/skill/SKILL.md +76 -76
  19. package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
  20. package/agent-spine-plugin/src/cli.js +1488 -1442
  21. package/agent-spine-plugin/src/hook.js +886 -812
  22. package/agent-spine-plugin/src/index.js +93 -93
  23. package/agent-spine-plugin/src/lib/acceptance.js +333 -333
  24. package/agent-spine-plugin/src/lib/attention.js +755 -755
  25. package/agent-spine-plugin/src/lib/audit.js +351 -342
  26. package/agent-spine-plugin/src/lib/authentication.js +515 -515
  27. package/agent-spine-plugin/src/lib/briefing.js +317 -317
  28. package/agent-spine-plugin/src/lib/catalog.js +167 -167
  29. package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
  30. package/agent-spine-plugin/src/lib/context.js +154 -154
  31. package/agent-spine-plugin/src/lib/continuity.js +338 -338
  32. package/agent-spine-plugin/src/lib/coordination.js +577 -577
  33. package/agent-spine-plugin/src/lib/documents.js +217 -217
  34. package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
  35. package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
  36. package/agent-spine-plugin/src/lib/gateway-runtime.js +1119 -760
  37. package/agent-spine-plugin/src/lib/graph.js +337 -337
  38. package/agent-spine-plugin/src/lib/hook-audit.js +29 -0
  39. package/agent-spine-plugin/src/lib/https-transport.js +392 -392
  40. package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
  41. package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
  42. package/agent-spine-plugin/src/lib/learning.js +6923 -6466
  43. package/agent-spine-plugin/src/lib/object-transport.js +206 -206
  44. package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
  45. package/agent-spine-plugin/src/lib/paths.js +109 -109
  46. package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
  47. package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
  48. package/agent-spine-plugin/src/lib/preflight.js +702 -702
  49. package/agent-spine-plugin/src/lib/runtime.js +13 -13
  50. package/agent-spine-plugin/src/lib/selfstarter.js +892 -819
  51. package/agent-spine-plugin/src/lib/sharing.js +969 -969
  52. package/agent-spine-plugin/src/lib/source-roots.js +529 -482
  53. package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
  54. package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
  55. package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
  56. package/agent-spine-plugin/src/mcp.js +597 -572
  57. package/agent-spine-plugin/src/version.js +1 -1
  58. package/agent-spine-plugin/src/worker.js +202 -195
  59. package/bin/active-steer-priority-policy.cjs +24 -0
  60. package/bin/launcher-runtime.js +8 -1
  61. package/bin/mnemo-tool-agent-policy.cjs +22 -0
  62. package/bin/thinking-activity-status-policy.cjs +132 -0
  63. package/bin/thinking-only-guard.cjs +75 -0
  64. package/bin/tool-call-loop-policy.cjs +53 -0
  65. package/bin/turn-thinking-policy.cjs +25 -1
  66. package/blun.mjs +554 -64
  67. package/package.json +4 -1
  68. package/standard-skills/translate-native/LICENSE +21 -21
  69. package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
  70. package/standard-skills/translate-native/references/native-orthography.md +79 -79
  71. package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
  72. package/standard-skills/translate-native/references/structured-content.md +72 -72
  73. package/standard-skills/translate-native/references/translationese-review.md +77 -77
  74. package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
  75. package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
  76. package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
  77. package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
  78. package/standard-skills/translate-native/scripts/language_quality.py +377 -377
  79. package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
  80. package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
  81. package/standard-tools/language-guard/blun_language_guard.py +697 -697
  82. package/standard-tools/language-guard/check_diacritics.py +353 -353
  83. package/standard-tools/language-guard/guard_service_client.py +264 -264
  84. package/standard-tools/language-guard/language_quality.py +377 -377
  85. package/standard-tools/language-guard/translation_guard.py +916 -916
@@ -1,760 +1,1119 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { open, readFile, stat, unlink, writeFile } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { buildCatalog } from "./catalog.js";
5
- import { isFileLockContention, replaceFileWithRetry } from "./filesystem-retry.js";
6
- import { attentionFindings, loadAttention } from "./attention.js";
7
- import {
8
- channelRuntimeFindings, loadChannelPolicy, loadChannelRuntime
9
- } from "./channel-runtime.js";
10
- import { loadGraph } from "./graph.js";
11
- import { projectStateDir } from "./paths.js";
12
- import { loadPersonaRuntime, personaRuntimeFindings } from "./persona-runtime.js";
13
- import { evaluateVoiceOutput } from "./voice-runtime.js";
14
-
15
- export const GATEWAY_POLICY_SCHEMA = "agentspine.gateway-policy/v1";
16
- export const GATEWAY_RUNTIME_SCHEMA = "agentspine.gateway-runtime/v1";
17
- export const GATEWAY_EVENT_SCHEMA = "agentspine.gateway-event/v1";
18
-
19
- const CONFIRMATION = "local-owner-confirmed";
20
- const MAX_BYTES = 8 * 1024 * 1024;
21
- const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/+~-]{0,255}$/;
22
- const ROUTE_RE = /^-?[A-Za-z0-9][A-Za-z0-9:_.@/+~-]{0,255}$/;
23
- const GOAL_STATUSES = new Set(["active", "blocked", "completed", "cancelled"]);
24
- const QUEUE_STATUSES = new Set(["pending", "leased", "awaiting-delivery", "completed", "blocked", "dead-letter", "cancelled"]);
25
- const OUTBOX_STATUSES = new Set(["prepared", "sending", "delivered", "failed", "dead-letter", "delivery-unknown", "acknowledged"]);
26
- const WAKE_KINDS = new Set(["direct-message", "deadline", "promise", "resolved-blocker", "assignment", "follow-up", "relationship"]);
27
- const PRIORITY = { "direct-message": 100, deadline: 90, promise: 90, "resolved-blocker": 80, assignment: 70, "follow-up": 60, relationship: 50 };
28
- const SECRET_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|gh[opusu])_[A-Za-z0-9_-]{20,}\b|\bBearer\s+[A-Za-z0-9._~+/-]{20,}|\b(?:api[-_ ]?key|token|password|secret)\s*[:=]\s*\S{8,}/i;
29
- const SECRET_KEY_RE = /"(?:api[-_ ]?key|token|password|secret|credential)"\s*:/i;
30
- const AUTHORITY_RE = /\b(?:permission|rights?|roles?|owner|trusted|delegat|authorized|approval|production|payment|spending|tool capability|send capability)\b/i;
31
- const HEALTH_VALUES = new Set(["stopped", "running", "unknown", "healthy", "degraded", "failed"]);
32
-
33
- function emptyPolicy(root) {
34
- return { schema: GATEWAY_POLICY_SCHEMA, root, revision: 0, enabled: false, killSwitch: false, goals: [], history: [] };
35
- }
36
-
37
- function emptyRuntime(root) {
38
- return { schema: GATEWAY_RUNTIME_SCHEMA, root, revision: 0, queue: [], lanes: [], outbox: [], receipts: [], history: [],
39
- health: { gateway: "stopped", adapter: "unknown", scheduler: "unknown", queue: "healthy", worker: "unknown", host: "unknown", lastTickAt: null, lastReconciledAt: null } };
40
- }
41
-
42
- function exactId(value, field, nullable = false) {
43
- if ((value === null || value === undefined || value === "") && nullable) return null;
44
- if (typeof value !== "string" || !ID_RE.test(value) || value.includes("*")) throw new Error(field + " must be an exact stable ID without wildcards");
45
- return value;
46
- }
47
-
48
- function safeText(value, field, maximum = 1000) {
49
- if (typeof value !== "string" || !value.trim()) throw new Error(field + " is required");
50
- const text = value.trim().replace(/\s+/g, " ").slice(0, maximum);
51
- if (SECRET_RE.test(text)) throw new Error(field + " appears to contain a secret");
52
- return text;
53
- }
54
-
55
- function timestamp(value = new Date()) {
56
- const date = value instanceof Date ? value : new Date(value);
57
- if (!Number.isFinite(date.getTime())) throw new Error("timestamp is invalid");
58
- return date.toISOString();
59
- }
60
-
61
- function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
62
-
63
- function safeCheckpoint(value) {
64
- if (value === null || value === undefined) return null;
65
- let content;
66
- try { content = JSON.stringify(value); } catch { throw new Error("checkpoint must be JSON serializable"); }
67
- if (!content || Buffer.byteLength(content) > 16384) throw new Error("checkpoint exceeds 16 KiB");
68
- if (SECRET_RE.test(content) || SECRET_KEY_RE.test(content) || AUTHORITY_RE.test(content)) {
69
- throw new Error("checkpoint contains secret- or authority-shaped content");
70
- }
71
- return JSON.parse(content);
72
- }
73
-
74
- function validGoal(goal) {
75
- return goal && ID_RE.test(goal.goalId || "") && ID_RE.test(goal.agentId || "")
76
- && ID_RE.test(goal.ownerSubjectId || "") && ID_RE.test(goal.projectId || "")
77
- && (goal.groupId === null || ID_RE.test(goal.groupId || "")) && GOAL_STATUSES.has(goal.status)
78
- && typeof goal.successCriterion === "string" && goal.successCriterion.length > 0
79
- && typeof goal.nextSafeStep === "string" && goal.nextSafeStep.length > 0
80
- && (goal.deadline === null || Number.isFinite(new Date(goal.deadline).getTime()))
81
- && Number.isInteger(goal.priority) && goal.priority >= 0 && goal.priority <= 100
82
- && (goal.checkpoint === null || (() => { try { safeCheckpoint(goal.checkpoint); return true; } catch { return false; } })())
83
- && (goal.blocker === null || (typeof goal.blocker === "string" && goal.blocker.length <= 500 && !SECRET_RE.test(goal.blocker)))
84
- && (goal.heartbeatAt === null || Number.isFinite(new Date(goal.heartbeatAt).getTime()))
85
- && goal.authority === "authenticated-goal-policy" && Number.isFinite(new Date(goal.createdAt).getTime())
86
- && Number.isFinite(new Date(goal.updatedAt).getTime());
87
- }
88
-
89
- function validPolicyHistory(item) {
90
- if (!item || item.authority !== "authenticated-goal-policy" || !Number.isFinite(new Date(item.at).getTime())) return false;
91
- if (item.kind === "goal") return validGoal(item.value);
92
- return item.kind === "control" && item.value && typeof item.value.enabled === "boolean" && typeof item.value.killSwitch === "boolean";
93
- }
94
-
95
- function validQueue(item) {
96
- return item && ID_RE.test(item.queueId || "") && WAKE_KINDS.has(item.kind)
97
- && ID_RE.test(item.dedupeKey || "") && item.queueId === "gateway-queue:" + sha256(item.dedupeKey).slice(0, 32)
98
- && ID_RE.test(item.agentId || "") && ID_RE.test(item.projectId || "")
99
- && (item.groupId === null || ID_RE.test(item.groupId || ""))
100
- && (item.goalId === null || ID_RE.test(item.goalId || ""))
101
- && (item.channelEventId === null || ID_RE.test(item.channelEventId || ""))
102
- && QUEUE_STATUSES.has(item.status) && Number.isInteger(item.priority) && item.priority >= 0 && item.priority <= 100
103
- && Number.isInteger(item.attempts) && item.attempts >= 0 && item.attempts <= 20
104
- && item.authority === "execution-state-only" && Number.isFinite(new Date(item.createdAt).getTime())
105
- && Number.isFinite(new Date(item.availableAt).getTime()) && Number.isFinite(new Date(item.updatedAt).getTime())
106
- && (item.completedAt === null || Number.isFinite(new Date(item.completedAt).getTime()))
107
- && (item.lastError === null || (typeof item.lastError === "string" && item.lastError.length <= 500 && !SECRET_RE.test(item.lastError)))
108
- && (item.lease === null || (item.status === "leased" && ID_RE.test(item.lease.workerId || "")
109
- && Number.isFinite(new Date(item.lease.expiresAt).getTime())));
110
- }
111
-
112
- function validOutbox(item) {
113
- return item && ID_RE.test(item.outboxId || "") && ID_RE.test(item.queueId || "")
114
- && ID_RE.test(item.idempotencyKey || "") && ID_RE.test(item.bindingId || "")
115
- && item.outboxId === "gateway-outbox:" + sha256(item.idempotencyKey).slice(0, 32)
116
- && ID_RE.test(item.eventId || "") && ROUTE_RE.test(item.provider || "") && ROUTE_RE.test(item.tenantId || "")
117
- && ROUTE_RE.test(item.accountId || "") && ROUTE_RE.test(item.chatId || "")
118
- && (item.threadId === null || ROUTE_RE.test(item.threadId || "")) && (item.replyTo === null || ROUTE_RE.test(item.replyTo || ""))
119
- && OUTBOX_STATUSES.has(item.status)
120
- && typeof item.text === "string" && item.text.length > 0 && item.text.length <= 16000 && !SECRET_RE.test(item.text)
121
- && Number.isInteger(item.attempts) && item.attempts >= 0 && item.attempts <= 20
122
- && item.authority === "delivery-state-only" && Number.isFinite(new Date(item.createdAt).getTime())
123
- && Number.isFinite(new Date(item.updatedAt).getTime()) && Number.isFinite(new Date(item.nextAttemptAt).getTime())
124
- && (item.deliveredAt === null || Number.isFinite(new Date(item.deliveredAt).getTime()))
125
- && (item.adapterReceipt === null || (typeof item.adapterReceipt === "string" && item.adapterReceipt.length <= 500))
126
- && (item.lastError === null || (typeof item.lastError === "string" && item.lastError.length <= 500 && !SECRET_RE.test(item.lastError)));
127
- }
128
-
129
- function validLane(item) {
130
- return item && ID_RE.test(item.agentId || "") && ID_RE.test(item.queueId || "") && ID_RE.test(item.workerId || "")
131
- && new Set(["leased", "completed", "expired"]).has(item.status)
132
- && Number.isFinite(new Date(item.claimedAt).getTime()) && Number.isFinite(new Date(item.expiresAt).getTime())
133
- && Number.isFinite(new Date(item.updatedAt).getTime()) && item.authority === "execution-state-only";
134
- }
135
-
136
- function validReceipt(item) {
137
- if (!(item && ID_RE.test(item.id || "") && ID_RE.test(item.kind || "") && ID_RE.test(item.objectId || "")
138
- && Number.isFinite(new Date(item.at).getTime()) && item.details && typeof item.details === "object"
139
- && !Array.isArray(item.details) && !SECRET_RE.test(JSON.stringify(item.details))
140
- && item.authority === "execution-state-only" && /^[a-f0-9]{64}$/.test(item.digest || ""))) return false;
141
- const material = { kind: item.kind, objectId: item.objectId, at: item.at, details: item.details, authority: "execution-state-only" };
142
- return item.digest === sha256(JSON.stringify(material)) && item.id === "gateway-receipt:" + item.digest.slice(0, 24);
143
- }
144
-
145
- function validHistory(item) {
146
- if (!(item && new Set(["queue", "outbox"]).has(item.kind) && ID_RE.test(item.objectId || "")
147
- && ID_RE.test(item.transition || "") && Number.isFinite(new Date(item.at).getTime())
148
- && item.value && item.authority === "execution-state-only")) return false;
149
- return item.kind === "queue" ? validQueue(item.value) && item.objectId === item.value.queueId
150
- : validOutbox(item.value) && item.objectId === item.value.outboxId;
151
- }
152
-
153
- function validHealth(health) {
154
- return health && ["gateway", "adapter", "scheduler", "queue", "worker", "host"].every((key) => HEALTH_VALUES.has(health[key]))
155
- && (health.lastTickAt === null || Number.isFinite(new Date(health.lastTickAt).getTime()))
156
- && (health.lastReconciledAt === null || Number.isFinite(new Date(health.lastReconciledAt).getTime()));
157
- }
158
-
159
- function normalizePolicy(value, root) {
160
- if (!value || value.schema !== GATEWAY_POLICY_SCHEMA || value.root !== root
161
- || !Number.isInteger(value.revision) || typeof value.enabled !== "boolean" || typeof value.killSwitch !== "boolean"
162
- || !Array.isArray(value.goals) || !Array.isArray(value.history) || value.goals.some((item) => !validGoal(item))
163
- || value.history.some((item) => !validPolicyHistory(item))) {
164
- throw new Error("gateway policy is invalid; autonomous runtime is disabled");
165
- }
166
- return value;
167
- }
168
-
169
- function normalizeRuntime(value, root) {
170
- const baseValid = value && value.schema === GATEWAY_RUNTIME_SCHEMA && value.root === root && Number.isInteger(value.revision)
171
- && Array.isArray(value.queue) && Array.isArray(value.lanes) && Array.isArray(value.outbox)
172
- && Array.isArray(value.receipts) && Array.isArray(value.history);
173
- if (!baseValid) throw new Error("gateway runtime structure is invalid; worker is disabled");
174
- const invalid = [];
175
- if (!validHealth(value.health)) invalid.push("health");
176
- const queueIndex = value.queue.findIndex((item) => !validQueue(item));
177
- const laneIndex = value.lanes.findIndex((item) => !validLane(item));
178
- const outboxIndex = value.outbox.findIndex((item) => !validOutbox(item));
179
- const receiptIndex = value.receipts.findIndex((item) => !validReceipt(item));
180
- const historyIndex = value.history.findIndex((item) => !validHistory(item));
181
- if (queueIndex >= 0) invalid.push("queue:" + queueIndex);
182
- if (laneIndex >= 0) invalid.push("lanes:" + laneIndex);
183
- if (outboxIndex >= 0) invalid.push("outbox:" + outboxIndex);
184
- if (receiptIndex >= 0) invalid.push("receipts:" + receiptIndex);
185
- if (historyIndex >= 0) invalid.push("history:" + historyIndex);
186
- if (invalid.length) throw new Error("gateway runtime is invalid (" + invalid.join(",") + "); worker is disabled");
187
- return value;
188
- }
189
-
190
- async function pathsFor(root, catalog = null) {
191
- catalog ||= await buildCatalog(root);
192
- const directory = await projectStateDir(catalog.root);
193
- return { catalog, directory, gatewayPolicyPath: join(directory, "gateway-policy.json"), gatewayRuntimePath: join(directory, "gateway-runtime.json") };
194
- }
195
-
196
- async function readJson(path, root, normalize, empty) {
197
- try {
198
- const metadata = await stat(path);
199
- if (metadata.size > MAX_BYTES) throw new Error("gateway state exceeds 8 MiB");
200
- return normalize(JSON.parse(await readFile(path, "utf8")), root);
201
- } catch (error) {
202
- if (error.code !== "ENOENT") throw error;
203
- return empty(root);
204
- }
205
- }
206
-
207
- async function writeJson(path, value) {
208
- const content = JSON.stringify(value, null, 2) + "\n";
209
- if (Buffer.byteLength(content) > MAX_BYTES) throw new Error("gateway state exceeds 8 MiB");
210
- const temporary = path + "." + process.pid + "." + randomUUID() + ".tmp";
211
- try { await writeFile(temporary, content, { mode: 0o600 }); await replaceFileWithRetry(temporary, path); }
212
- finally { await unlink(temporary).catch((error) => { if (error.code !== "ENOENT") throw error; }); }
213
- }
214
-
215
- async function withLock(paths, task) {
216
- const lockPath = join(paths.directory, "gateway-runtime.lock");
217
- let handle;
218
- for (let attempt = 0; attempt < 160; attempt += 1) {
219
- try { handle = await open(lockPath, "wx", 0o600); break; } catch (error) {
220
- if (!isFileLockContention(error)) throw error;
221
- try { const metadata = await stat(lockPath); if (Date.now() - metadata.mtimeMs > 120000) await unlink(lockPath); }
222
- catch (lockError) { if (lockError.code !== "ENOENT") throw lockError; }
223
- await new Promise((resolve) => setTimeout(resolve, 25));
224
- }
225
- }
226
- if (!handle) throw new Error("gateway state is busy; retry later");
227
- try { return await task(); } finally {
228
- await handle.close();
229
- await unlink(lockPath).catch((error) => { if (error.code !== "ENOENT") throw error; });
230
- }
231
- }
232
-
233
- function appendReceipt(runtime, kind, objectId, now, details = {}) {
234
- const material = { kind, objectId, at: now, details, authority: "execution-state-only" };
235
- const digest = sha256(JSON.stringify(material));
236
- const id = "gateway-receipt:" + digest.slice(0, 24);
237
- const previous = runtime.receipts.find((item) => item.id === id);
238
- if (previous) return previous;
239
- const receipt = { id, ...material, digest };
240
- runtime.receipts.push(receipt);
241
- return receipt;
242
- }
243
-
244
- function preserve(runtime, kind, value, transition, now) {
245
- runtime.history.push({ kind, objectId: kind === "outbox" ? value.outboxId : value.queueId, transition, at: now,
246
- value: structuredClone(value), authority: "execution-state-only" });
247
- }
248
-
249
- function currentLane(runtime, agentId) {
250
- return runtime.lanes.find((item) => item.agentId === agentId && item.status === "leased") || null;
251
- }
252
-
253
- function assertActivePersona(personaPolicy, personaRuntime, agentId, projectId, groupId) {
254
- const findings = personaRuntimeFindings(personaPolicy, personaRuntime);
255
- if (findings.length) throw new Error("persona runtime is unhealthy: " + findings.join(", "));
256
- const persona = personaRuntime.personas.find((item) => item.personaId === agentId && item.status === "active");
257
- if (!persona || !["agent", "bot"].includes(persona.kind)) throw new Error("gateway work requires an active authenticated agent or bot");
258
- if (groupId !== null && persona.groupId !== groupId) throw new Error("gateway work group does not match authenticated persona membership");
259
- const binding = personaPolicy.bindings.find((item) => item.id === persona.bindingId && item.active);
260
- if (!binding || !binding.tenantId || !binding.profileId || !projectId) throw new Error("gateway work lacks an active authenticated identity binding");
261
- return { persona, binding };
262
- }
263
-
264
- export async function setGatewayControl({ root = process.cwd(), enabled, killSwitch, confirmation, now = new Date() }) {
265
- if (confirmation !== CONFIRMATION) throw new Error("gateway control changes require explicit local owner confirmation");
266
- const paths = await pathsFor(root);
267
- return withLock(paths, async () => {
268
- const policy = await readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy);
269
- const previous = { enabled: policy.enabled, killSwitch: policy.killSwitch };
270
- if (enabled !== undefined) policy.enabled = Boolean(enabled);
271
- if (killSwitch !== undefined) policy.killSwitch = Boolean(killSwitch);
272
- policy.revision += 1;
273
- policy.history.push({ kind: "control", at: timestamp(now), value: previous, authority: "authenticated-goal-policy" });
274
- await writeJson(paths.gatewayPolicyPath, policy);
275
- return { enabled: policy.enabled, killSwitch: policy.killSwitch, revision: policy.revision };
276
- });
277
- }
278
-
279
- export async function assignGoal({ root = process.cwd(), goalId, agentId, ownerSubjectId, projectId, groupId = null,
280
- priority = 70, successCriterion, nextSafeStep, deadline = null, confirmation, now = new Date() }) {
281
- if (confirmation !== CONFIRMATION) throw new Error("goal assignment requires explicit local owner confirmation");
282
- const paths = await pathsFor(root);
283
- return withLock(paths, async () => {
284
- const [policy, runtime, personas] = await Promise.all([
285
- readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
286
- readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
287
- loadPersonaRuntime(paths.catalog.root, paths.catalog)
288
- ]);
289
- agentId = exactId(agentId, "agentId"); projectId = exactId(projectId, "projectId"); groupId = exactId(groupId, "groupId", true);
290
- assertActivePersona(personas.policy, personas.runtime, agentId, projectId, groupId);
291
- const createdAt = timestamp(now);
292
- const active = policy.goals.find((item) => item.agentId === agentId && item.status === "active" && item.goalId !== goalId);
293
- if (active) throw new Error("an agent may have only one active focused goal");
294
- const goal = { goalId: exactId(goalId, "goalId"), agentId, ownerSubjectId: exactId(ownerSubjectId, "ownerSubjectId"),
295
- projectId, groupId, priority: Number(priority), successCriterion: safeText(successCriterion, "successCriterion"),
296
- nextSafeStep: safeText(nextSafeStep, "nextSafeStep"), deadline: deadline === null ? null : timestamp(deadline),
297
- status: "active", checkpoint: null, heartbeatAt: null, blocker: null, createdAt, updatedAt: createdAt,
298
- authority: "authenticated-goal-policy" };
299
- if (!validGoal(goal)) throw new Error("goal assignment is invalid");
300
- const previous = policy.goals.find((item) => item.goalId === goal.goalId);
301
- if (previous && [previous.agentId, previous.ownerSubjectId, previous.projectId, previous.groupId].join("\0")
302
- !== [goal.agentId, goal.ownerSubjectId, goal.projectId, goal.groupId].join("\0")) throw new Error("goal scope is immutable");
303
- if (previous) {
304
- policy.history.push({ kind: "goal", at: createdAt, value: structuredClone(previous), authority: "authenticated-goal-policy" });
305
- goal.createdAt = previous.createdAt;
306
- }
307
- policy.goals = policy.goals.filter((item) => item.goalId !== goal.goalId);
308
- policy.goals.push(goal);
309
- policy.revision += 1;
310
- const key = "goal:" + goal.goalId + ":assignment";
311
- if (!runtime.queue.some((item) => item.dedupeKey === key)) {
312
- runtime.queue.push({ queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind: "assignment",
313
- agentId, projectId, groupId, goalId: goal.goalId, channelEventId: null, priority: PRIORITY.assignment,
314
- status: "pending", attempts: 0, lease: null, availableAt: createdAt, createdAt, updatedAt: createdAt,
315
- completedAt: null, lastError: null, authority: "execution-state-only" });
316
- runtime.revision += 1;
317
- appendReceipt(runtime, "queued", "gateway-queue:" + sha256(key).slice(0, 32), createdAt, { kind: "assignment", dedupeKey: key });
318
- }
319
- await Promise.all([writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)]);
320
- return { goal, gatewayPolicyPath: paths.gatewayPolicyPath };
321
- });
322
- }
323
-
324
- export async function enqueueGatewayWake({ root = process.cwd(), kind, agentId, projectId, groupId = null, goalId = null,
325
- channelEventId = null, dedupeKey, availableAt = null, now = new Date() }) {
326
- if (!WAKE_KINDS.has(kind)) throw new Error("unsupported gateway wake kind");
327
- const paths = await pathsFor(root);
328
- return withLock(paths, async () => {
329
- const [policy, runtime, personas] = await Promise.all([
330
- readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
331
- readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
332
- loadPersonaRuntime(paths.catalog.root, paths.catalog)
333
- ]);
334
- if (!policy.enabled || policy.killSwitch) throw new Error("gateway is disabled by local policy");
335
- agentId = exactId(agentId, "agentId"); projectId = exactId(projectId, "projectId"); groupId = exactId(groupId, "groupId", true);
336
- assertActivePersona(personas.policy, personas.runtime, agentId, projectId, groupId);
337
- goalId = exactId(goalId, "goalId", true); channelEventId = exactId(channelEventId, "channelEventId", true);
338
- const key = exactId(dedupeKey, "dedupeKey");
339
- const existing = runtime.queue.find((item) => item.dedupeKey === key);
340
- if (existing) return { item: existing, duplicate: true };
341
- const createdAt = timestamp(now);
342
- const item = { queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind, agentId, projectId, groupId,
343
- goalId, channelEventId, priority: PRIORITY[kind], status: "pending", attempts: 0, lease: null,
344
- availableAt: availableAt === null ? createdAt : timestamp(availableAt), createdAt, updatedAt: createdAt,
345
- completedAt: null, lastError: null, authority: "execution-state-only" };
346
- runtime.queue.push(item); runtime.revision += 1;
347
- appendReceipt(runtime, "queued", item.queueId, createdAt, { kind, dedupeKey: key });
348
- await writeJson(paths.gatewayRuntimePath, runtime);
349
- return { item, duplicate: false };
350
- });
351
- }
352
-
353
- export async function reconcileGateway({ root = process.cwd(), now = new Date() }) {
354
- const paths = await pathsFor(root);
355
- return withLock(paths, async () => {
356
- const [policy, runtime, channelPolicy, channel, attention, personas, { graph }] = await Promise.all([
357
- readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
358
- readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
359
- loadChannelPolicy(paths.catalog.root, paths.catalog), loadChannelRuntime(paths.catalog.root, paths.catalog),
360
- loadAttention(paths.catalog.root, paths.catalog), loadPersonaRuntime(paths.catalog.root, paths.catalog),
361
- loadGraph(paths.catalog.root, paths.catalog)
362
- ]);
363
- const current = timestamp(now);
364
- const findings = personaRuntimeFindings(personas.policy, personas.runtime, graph);
365
- if (findings.length) throw new Error("persona runtime is unhealthy: " + findings.join(", "));
366
- const channelFindings = channelRuntimeFindings(channel.runtime, channelPolicy.policy, graph);
367
- if (channelFindings.length) throw new Error("channel runtime is unhealthy: " + channelFindings.join(", "));
368
- const attentionIssues = attentionFindings(attention.attention);
369
- if (attentionIssues.length) throw new Error("attention runtime is unhealthy: " + attentionIssues.join(", "));
370
- for (const lane of runtime.lanes.filter((item) => item.status === "leased" && new Date(item.expiresAt) <= new Date(current))) {
371
- const item = runtime.queue.find((entry) => entry.queueId === lane.queueId && entry.status === "leased");
372
- if (item) { preserve(runtime, "queue", item, "lease-expired", current); item.status = item.attempts >= 3 ? "dead-letter" : "pending"; item.lease = null; item.updatedAt = current; }
373
- lane.status = "expired"; lane.updatedAt = current;
374
- }
375
- for (const outbox of runtime.outbox.filter((item) => item.status === "sending")) {
376
- preserve(runtime, "outbox", outbox, "ambiguous-send-recovery", current);
377
- outbox.status = "delivery-unknown"; outbox.updatedAt = current;
378
- appendReceipt(runtime, "delivery-unknown", outbox.outboxId, current, { reason: "crash-during-send" });
379
- }
380
- if (policy.enabled && !policy.killSwitch) {
381
- for (const goal of policy.goals.filter((item) => item.status === "active" && item.deadline
382
- && new Date(item.deadline) <= new Date(current))) {
383
- try { assertActivePersona(personas.policy, personas.runtime, goal.agentId, goal.projectId, goal.groupId); }
384
- catch { continue; }
385
- const key = "goal:" + goal.goalId + ":deadline:" + goal.deadline;
386
- if (!runtime.queue.some((item) => item.dedupeKey === key)) runtime.queue.push({
387
- queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind: "deadline",
388
- agentId: goal.agentId, projectId: goal.projectId, groupId: goal.groupId, goalId: goal.goalId,
389
- channelEventId: null, priority: PRIORITY.deadline, status: "pending", attempts: 0, lease: null,
390
- availableAt: current, createdAt: current, updatedAt: current, completedAt: null, lastError: null,
391
- authority: "execution-state-only"
392
- });
393
- }
394
- for (const event of channel.runtime.events.filter((item) => item.status === "pending")) {
395
- if (!personas.runtime.personas.some((persona) => persona.personaId === event.agentId && persona.status === "active")) continue;
396
- const key = "channel:" + event.eventId;
397
- if (!runtime.queue.some((item) => item.dedupeKey === key)) runtime.queue.push({ queueId: "gateway-queue:" + sha256(key).slice(0, 32),
398
- dedupeKey: key, kind: "direct-message", agentId: event.agentId, projectId: event.projectId, groupId: event.groupId,
399
- goalId: null, channelEventId: event.eventId, priority: PRIORITY["direct-message"], status: "pending", attempts: 0,
400
- lease: null, availableAt: current, createdAt: current, updatedAt: current, completedAt: null, lastError: null, authority: "execution-state-only" });
401
- }
402
- for (const event of attention.attention.events.filter((item) => item.entityId
403
- && ((item.kind === "promise" && item.status === "open") || (item.kind === "blocker" && item.status === "resolved")))) {
404
- try { assertActivePersona(personas.policy, personas.runtime, event.entityId, event.projectId, event.groupId); }
405
- catch { continue; }
406
- const kind = event.kind === "promise" ? "promise" : "resolved-blocker";
407
- const key = "attention:" + event.id + ":" + event.status;
408
- if (!runtime.queue.some((entry) => entry.dedupeKey === key)) runtime.queue.push({ queueId: "gateway-queue:" + sha256(key).slice(0, 32),
409
- dedupeKey: key, kind, agentId: event.entityId, projectId: event.projectId, groupId: event.groupId,
410
- goalId: null, channelEventId: null, priority: PRIORITY[kind], status: "pending", attempts: 0, lease: null,
411
- availableAt: event.dueAt || current, createdAt: current, updatedAt: current, completedAt: null, lastError: null, authority: "execution-state-only" });
412
- }
413
- }
414
- runtime.health.gateway = policy.enabled && !policy.killSwitch ? "running" : "stopped";
415
- runtime.health.scheduler = "healthy"; runtime.health.queue = "healthy"; runtime.health.lastReconciledAt = current;
416
- runtime.revision += 1;
417
- await writeJson(paths.gatewayRuntimePath, runtime);
418
- return { policy, runtime, recovered: true };
419
- });
420
- }
421
-
422
- export async function claimGatewayWork({ root = process.cwd(), workerId, leaseSeconds = 120, now = new Date() }) {
423
- const paths = await pathsFor(root);
424
- return withLock(paths, async () => {
425
- const [policy, runtime, personas] = await Promise.all([
426
- readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
427
- readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
428
- loadPersonaRuntime(paths.catalog.root, paths.catalog)
429
- ]);
430
- if (!policy.enabled || policy.killSwitch) return { item: null, reason: "disabled" };
431
- const current = timestamp(now); workerId = exactId(workerId, "workerId");
432
- const seconds = Number(leaseSeconds);
433
- if (!Number.isInteger(seconds) || seconds < 15 || seconds > 900) throw new Error("leaseSeconds must be 15-900");
434
- const items = runtime.queue.filter((item) => item.status === "pending" && new Date(item.availableAt) <= new Date(current)
435
- && !currentLane(runtime, item.agentId));
436
- items.sort((a, b) => b.priority - a.priority || a.createdAt.localeCompare(b.createdAt) || a.queueId.localeCompare(b.queueId));
437
- let item = null; let revoked = false;
438
- for (const candidate of items) {
439
- try {
440
- assertActivePersona(personas.policy, personas.runtime, candidate.agentId, candidate.projectId, candidate.groupId);
441
- item = candidate;
442
- break;
443
- } catch {
444
- preserve(runtime, "queue", candidate, "identity-revoked", current);
445
- candidate.status = "cancelled"; candidate.completedAt = current; candidate.updatedAt = current;
446
- appendReceipt(runtime, "identity-revoked", candidate.queueId, current, {});
447
- runtime.revision += 1; revoked = true;
448
- }
449
- }
450
- if (!item && revoked) await writeJson(paths.gatewayRuntimePath, runtime);
451
- if (!item) return { item: null, reason: runtime.queue.some((entry) => entry.status === "pending") ? "waiting" : "idle/needs-goal" };
452
- preserve(runtime, "queue", item, "leased", current);
453
- item.status = "leased"; item.attempts += 1; item.updatedAt = current;
454
- item.lease = { workerId, claimedAt: current, expiresAt: new Date(new Date(current).getTime() + seconds * 1000).toISOString() };
455
- runtime.lanes = runtime.lanes.filter((lane) => lane.agentId !== item.agentId || lane.status !== "leased");
456
- runtime.lanes.push({ agentId: item.agentId, queueId: item.queueId, workerId, status: "leased", claimedAt: current,
457
- expiresAt: item.lease.expiresAt, updatedAt: current, authority: "execution-state-only" });
458
- runtime.health.worker = "healthy"; runtime.health.lastTickAt = current; runtime.revision += 1;
459
- const receipt = appendReceipt(runtime, "leased", item.queueId, current, { workerId, attempt: item.attempts });
460
- await writeJson(paths.gatewayRuntimePath, runtime);
461
- return { item: structuredClone(item), receipt };
462
- });
463
- }
464
-
465
- function exactReplyBinding(channelPolicy, event) {
466
- const binding = channelPolicy.bindings.find((item) => item.id === event.bindingId && item.status === "active"
467
- && item.agentId === event.agentId && item.projectId === event.projectId && item.groupId === event.groupId
468
- && item.provider === event.provider && item.tenantId === event.tenantId && item.accountId === event.accountId
469
- && item.chatId === event.chatId && item.threadId === event.threadId && item.capabilities.includes("reply"));
470
- if (!binding) throw new Error("current exact channel reply capability is unavailable");
471
- return binding;
472
- }
473
-
474
- export async function completeGatewayRun({ root = process.cwd(), queueId, workerId, result, now = new Date() }) {
475
- const paths = await pathsFor(root);
476
- return withLock(paths, async () => {
477
- const [policy, runtime, channelPolicy, channelRuntime, personas] = await Promise.all([
478
- readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
479
- readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
480
- loadChannelPolicy(paths.catalog.root, paths.catalog), loadChannelRuntime(paths.catalog.root, paths.catalog),
481
- loadPersonaRuntime(paths.catalog.root, paths.catalog)
482
- ]);
483
- if (!policy.enabled || policy.killSwitch) throw new Error("gateway was disabled before run completion");
484
- const item = runtime.queue.find((entry) => entry.queueId === exactId(queueId, "queueId"));
485
- workerId = exactId(workerId, "workerId");
486
- if (!item || item.status !== "leased" || item.lease?.workerId !== workerId) throw new Error("run completion requires the exact active queue lease");
487
- assertActivePersona(personas.policy, personas.runtime, item.agentId, item.projectId, item.groupId);
488
- const current = timestamp(now);
489
- const lane = runtime.lanes.find((entry) => entry.queueId === item.queueId && entry.workerId === workerId && entry.status === "leased");
490
- if (!lane) throw new Error("agent lane lease is missing");
491
- const text = result?.text ? safeText(result.text, "result.text", 16000) : null;
492
- preserve(runtime, "queue", item, "run-completed", current);
493
- if (item.channelEventId) {
494
- if (!text) throw new Error("a channel obligation requires a non-empty response");
495
- const voice = evaluateVoiceOutput(text);
496
- if (!voice.ok) throw new Error("channel response contains a prohibited attachment or consciousness claim");
497
- const event = channelRuntime.runtime.events.find((entry) => entry.eventId === item.channelEventId);
498
- if (!event) throw new Error("channel event disappeared before delivery preparation");
499
- const binding = exactReplyBinding(channelPolicy.policy, event);
500
- const idempotencyKey = "delivery:" + sha256([event.eventId, binding.id, event.chatId, event.threadId || "", event.replyTo || ""].join("\0")).slice(0, 32);
501
- let outbox = runtime.outbox.find((entry) => entry.idempotencyKey === idempotencyKey);
502
- if (!outbox) {
503
- outbox = { outboxId: "gateway-outbox:" + sha256(idempotencyKey).slice(0, 32), queueId: item.queueId,
504
- idempotencyKey, bindingId: binding.id, eventId: event.eventId, provider: event.provider, tenantId: event.tenantId,
505
- accountId: event.accountId, chatId: event.chatId, threadId: event.threadId, replyTo: event.replyTo,
506
- text, status: "prepared", attempts: 0, nextAttemptAt: current, createdAt: current, updatedAt: current,
507
- deliveredAt: null, adapterReceipt: null, lastError: null, authority: "delivery-state-only" };
508
- runtime.outbox.push(outbox);
509
- }
510
- item.status = "awaiting-delivery";
511
- } else {
512
- item.status = result?.blocked ? "blocked" : "completed"; item.completedAt = current;
513
- const goal = item.goalId ? policy.goals.find((entry) => entry.goalId === item.goalId) : null;
514
- if (goal) {
515
- policy.history.push({ kind: "goal", at: current, value: structuredClone(goal), authority: "authenticated-goal-policy" });
516
- goal.checkpoint = result?.checkpoint === undefined ? goal.checkpoint : safeCheckpoint(result.checkpoint); goal.heartbeatAt = current;
517
- goal.blocker = result?.blocked ? safeText(result.blocker || "Run blocked.", "blocker", 500) : null;
518
- goal.status = result?.completed ? "completed" : result?.blocked ? "blocked" : "active"; goal.updatedAt = current;
519
- policy.revision += 1;
520
- if (goal.status === "active") {
521
- const checkpointDigest = sha256(JSON.stringify(goal.checkpoint || { heartbeatAt: current })).slice(0, 20);
522
- const key = "goal:" + goal.goalId + ":follow-up:" + checkpointDigest;
523
- if (!runtime.queue.some((entry) => entry.dedupeKey === key)) runtime.queue.push({
524
- queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind: "follow-up",
525
- agentId: goal.agentId, projectId: goal.projectId, groupId: goal.groupId, goalId: goal.goalId,
526
- channelEventId: null, priority: PRIORITY["follow-up"], status: "pending", attempts: 0, lease: null,
527
- availableAt: new Date(new Date(current).getTime() + 60000).toISOString(), createdAt: current, updatedAt: current,
528
- completedAt: null, lastError: null, authority: "execution-state-only"
529
- });
530
- }
531
- }
532
- }
533
- item.lease = null; item.updatedAt = current; lane.status = "completed"; lane.updatedAt = current;
534
- runtime.health.host = "healthy"; runtime.health.worker = "healthy"; runtime.health.lastTickAt = current;
535
- appendReceipt(runtime, "run-terminal", item.queueId, current, { status: item.status }); runtime.revision += 1;
536
- await Promise.all([writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)]);
537
- return { item, outbox: runtime.outbox.find((entry) => entry.queueId === item.queueId) || null };
538
- });
539
- }
540
-
541
- export async function failGatewayRun({ root = process.cwd(), queueId, workerId, error, retryAfterMs = 5000,
542
- now = new Date() }) {
543
- const paths = await pathsFor(root);
544
- return withLock(paths, async () => {
545
- const [policy, runtime] = await Promise.all([
546
- readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
547
- readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime)
548
- ]);
549
- const item = runtime.queue.find((entry) => entry.queueId === exactId(queueId, "queueId"));
550
- workerId = exactId(workerId, "workerId");
551
- if (!item || item.status !== "leased" || item.lease?.workerId !== workerId) {
552
- throw new Error("run failure requires the exact active queue lease");
553
- }
554
- const lane = runtime.lanes.find((entry) => entry.queueId === item.queueId && entry.workerId === workerId
555
- && entry.status === "leased");
556
- if (!lane) throw new Error("agent lane lease is missing");
557
- const current = timestamp(now);
558
- const message = safeText(String(error || "host runtime unavailable"), "runError", 500);
559
- const delay = Number(retryAfterMs);
560
- if (!Number.isFinite(delay) || delay < 250 || delay > 300000) throw new Error("retryAfterMs must be 250-300000");
561
- preserve(runtime, "queue", item, "run-failed", current);
562
- item.status = item.attempts >= 3 ? "dead-letter" : "pending";
563
- item.lease = null; item.lastError = message; item.updatedAt = current;
564
- item.availableAt = new Date(new Date(current).getTime() + delay).toISOString();
565
- if (item.status === "dead-letter") item.completedAt = current;
566
- lane.status = "completed"; lane.updatedAt = current;
567
- runtime.health.host = "failed"; runtime.health.worker = "degraded"; runtime.health.lastTickAt = current;
568
- runtime.revision += 1;
569
- const receipt = appendReceipt(runtime, item.status === "dead-letter" ? "run-dead-letter" : "run-retry",
570
- item.queueId, current, { attempt: item.attempts });
571
- await writeJson(paths.gatewayRuntimePath, runtime);
572
- return { item, receipt, policyEnabled: policy.enabled && !policy.killSwitch };
573
- });
574
- }
575
-
576
- export async function deliverPrepared({ root = process.cwd(), outboxId, adapter, now = new Date() }) {
577
- if (!adapter || typeof adapter.send !== "function") throw new Error("delivery adapter is unavailable");
578
- const paths = await pathsFor(root);
579
- let prepared;
580
- await withLock(paths, async () => {
581
- const [policy, runtime, channelPolicy, channelRuntime, personas] = await Promise.all([
582
- readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
583
- readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
584
- loadChannelPolicy(paths.catalog.root, paths.catalog), loadChannelRuntime(paths.catalog.root, paths.catalog),
585
- loadPersonaRuntime(paths.catalog.root, paths.catalog)
586
- ]);
587
- if (!policy.enabled || policy.killSwitch) throw new Error("gateway was disabled before delivery");
588
- const outbox = runtime.outbox.find((item) => item.outboxId === exactId(outboxId, "outboxId"));
589
- if (!outbox) throw new Error("unknown outbox item");
590
- if (["delivered", "acknowledged"].includes(outbox.status)) { prepared = { duplicate: true, outbox: structuredClone(outbox) }; return; }
591
- if (outbox.status !== "prepared" && outbox.status !== "failed") throw new Error("outbox item is not safely retryable");
592
- if (new Date(outbox.nextAttemptAt) > new Date(timestamp(now))) throw new Error("outbox retry is not due");
593
- const event = channelRuntime.runtime.events.find((item) => item.eventId === outbox.eventId);
594
- if (!event) throw new Error("outbox channel event is missing");
595
- try {
596
- assertActivePersona(personas.policy, personas.runtime, event.agentId, event.projectId, event.groupId);
597
- exactReplyBinding(channelPolicy.policy, event);
598
- }
599
- catch (error) {
600
- const current = timestamp(now);
601
- preserve(runtime, "outbox", outbox, "capability-revoked", current);
602
- outbox.status = "dead-letter"; outbox.lastError = safeText(error.message, "adapterError", 500);
603
- outbox.updatedAt = current;
604
- const queue = runtime.queue.find((item) => item.queueId === outbox.queueId);
605
- if (queue) { queue.status = "dead-letter"; queue.completedAt = current; queue.updatedAt = current; }
606
- runtime.health.adapter = "failed"; runtime.revision += 1;
607
- const receipt = appendReceipt(runtime, "dead-letter", outbox.outboxId, current, { reason: "identity-or-capability-revoked" });
608
- await writeJson(paths.gatewayRuntimePath, runtime);
609
- prepared = { duplicate: false, terminal: true, outbox: structuredClone(outbox), receipt };
610
- return;
611
- }
612
- preserve(runtime, "outbox", outbox, "sending", timestamp(now));
613
- outbox.status = "sending"; outbox.attempts += 1; outbox.updatedAt = timestamp(now); runtime.revision += 1;
614
- appendReceipt(runtime, "sending", outbox.outboxId, outbox.updatedAt, { attempt: outbox.attempts });
615
- await writeJson(paths.gatewayRuntimePath, runtime);
616
- prepared = { duplicate: false, outbox: structuredClone(outbox) };
617
- });
618
- if (prepared.duplicate || prepared.terminal) return prepared;
619
- let outcome;
620
- try { outcome = await adapter.send(structuredClone(prepared.outbox)); }
621
- catch (error) { outcome = { ok: false, effect: "unknown", error: error.message }; }
622
- return withLock(paths, async () => {
623
- const runtime = await readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime);
624
- const outbox = runtime.outbox.find((item) => item.outboxId === prepared.outbox.outboxId);
625
- if (!outbox || outbox.status !== "sending") throw new Error("outbox sending state changed unexpectedly");
626
- const current = timestamp(now);
627
- preserve(runtime, "outbox", outbox, outcome?.ok ? "delivered" : "send-failed", current);
628
- if (outcome?.ok) {
629
- outbox.status = "delivered"; outbox.deliveredAt = current; outbox.adapterReceipt = safeText(String(outcome.receipt || "delivered"), "adapterReceipt", 500);
630
- runtime.health.adapter = "healthy";
631
- const queue = runtime.queue.find((item) => item.queueId === outbox.queueId);
632
- if (queue) { queue.status = "completed"; queue.completedAt = current; queue.updatedAt = current; }
633
- } else if (outcome?.effect === "none") {
634
- outbox.status = outbox.attempts < 3 ? "failed" : "dead-letter";
635
- outbox.lastError = safeText(String(outcome.error || "adapter failure"), "adapterError", 500);
636
- runtime.health.adapter = outbox.status === "failed" ? "degraded" : "failed";
637
- if (outbox.status === "failed") {
638
- const delay = Math.min(300000, Number(outcome.retryAfterMs) || 1000 * (2 ** outbox.attempts));
639
- outbox.nextAttemptAt = new Date(new Date(current).getTime() + delay).toISOString();
640
- } else {
641
- const queue = runtime.queue.find((item) => item.queueId === outbox.queueId);
642
- if (queue) { queue.status = "dead-letter"; queue.completedAt = current; queue.updatedAt = current; }
643
- }
644
- } else {
645
- outbox.status = "delivery-unknown"; outbox.lastError = safeText(String(outcome?.error || "delivery outcome is ambiguous"), "adapterError", 500);
646
- runtime.health.adapter = "failed";
647
- }
648
- outbox.updatedAt = current; runtime.revision += 1;
649
- const receipt = appendReceipt(runtime, outbox.status, outbox.outboxId, current, { adapterReceipt: outbox.adapterReceipt });
650
- await writeJson(paths.gatewayRuntimePath, runtime);
651
- return { outbox, receipt, duplicate: false };
652
- });
653
- }
654
-
655
- export async function updateGatewayHealth({ root = process.cwd(), worker = null, adapter = null, host = null,
656
- now = new Date() } = {}) {
657
- const paths = await pathsFor(root);
658
- return withLock(paths, async () => {
659
- const runtime = await readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime);
660
- for (const [key, value] of Object.entries({ worker, adapter, host })) {
661
- if (value !== null) {
662
- if (!HEALTH_VALUES.has(value)) throw new Error("unsupported gateway health value");
663
- runtime.health[key] = value;
664
- }
665
- }
666
- runtime.health.lastTickAt = timestamp(now); runtime.revision += 1;
667
- await writeJson(paths.gatewayRuntimePath, runtime);
668
- return structuredClone(runtime.health);
669
- });
670
- }
671
-
672
- export async function loadGatewayRuntime(root = process.cwd(), catalog = null) {
673
- const paths = await pathsFor(root, catalog);
674
- const [policy, runtime] = await Promise.all([
675
- readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
676
- readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime)
677
- ]);
678
- return { policy, runtime, ...paths };
679
- }
680
-
681
- export async function inspectGatewayRuntime(root = process.cwd(), catalog = null) {
682
- const paths = await pathsFor(root, catalog); const errors = [];
683
- let policy = emptyPolicy(paths.catalog.root); let runtime = emptyRuntime(paths.catalog.root);
684
- try { policy = await readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy); } catch (error) { errors.push("policy:" + error.message); }
685
- try { runtime = await readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime); } catch (error) { errors.push("runtime:" + error.message); }
686
- return { policy, runtime, errors, ...paths };
687
- }
688
-
689
- export function gatewayRuntimeFindings(policy, runtime) {
690
- const findings = [];
691
- const active = new Set();
692
- const goalIds = new Set();
693
- for (const goal of policy.goals) {
694
- if (!validGoal(goal)) findings.push("invalid-goal:" + (goal?.goalId || "unknown"));
695
- if (goalIds.has(goal.goalId)) findings.push("duplicate-goal:" + goal.goalId);
696
- goalIds.add(goal.goalId);
697
- if (goal.status === "active" && active.has(goal.agentId)) findings.push("multiple-active-goals:" + goal.agentId);
698
- if (goal.status === "active") active.add(goal.agentId);
699
- }
700
- for (const item of policy.history) if (!validPolicyHistory(item)) findings.push("invalid-gateway-policy-history");
701
- const queueIds = new Set(); const dedupeKeys = new Set();
702
- for (const item of runtime.queue) {
703
- if (!validQueue(item)) findings.push("invalid-queue-item:" + (item?.queueId || "unknown"));
704
- if (queueIds.has(item.queueId)) findings.push("duplicate-queue-item:" + item.queueId);
705
- if (dedupeKeys.has(item.dedupeKey)) findings.push("duplicate-queue-dedupe:" + item.dedupeKey);
706
- queueIds.add(item.queueId); dedupeKeys.add(item.dedupeKey);
707
- }
708
- const outboxIds = new Set(); const idempotencyKeys = new Set();
709
- for (const item of runtime.outbox) {
710
- if (!validOutbox(item)) findings.push("invalid-outbox-item:" + (item?.outboxId || "unknown"));
711
- if (!queueIds.has(item.queueId)) findings.push("orphan-outbox-item:" + item.outboxId);
712
- if (outboxIds.has(item.outboxId)) findings.push("duplicate-outbox-item:" + item.outboxId);
713
- if (idempotencyKeys.has(item.idempotencyKey)) findings.push("duplicate-outbox-idempotency:" + item.idempotencyKey);
714
- outboxIds.add(item.outboxId); idempotencyKeys.add(item.idempotencyKey);
715
- }
716
- for (const item of runtime.receipts) if (!validReceipt(item)
717
- || (!queueIds.has(item.objectId) && !outboxIds.has(item.objectId))) findings.push("invalid-gateway-receipt:" + (item?.id || "unknown"));
718
- for (const item of runtime.history) if (!validHistory(item)) findings.push("invalid-gateway-history:" + (item?.objectId || "unknown"));
719
- const leased = runtime.lanes.filter((item) => item.status === "leased");
720
- for (const lane of runtime.lanes) {
721
- if (!validLane(lane)) findings.push("invalid-agent-lane:" + (lane?.queueId || "unknown"));
722
- const queue = runtime.queue.find((item) => item.queueId === lane.queueId);
723
- if (!queue || (lane.status === "leased" && (queue.status !== "leased" || queue.lease?.workerId !== lane.workerId))) {
724
- findings.push("agent-lane-queue-mismatch:" + (lane.queueId || "unknown"));
725
- }
726
- }
727
- if (new Set(leased.map((item) => item.agentId)).size !== leased.length) findings.push("duplicate-agent-lane");
728
- if (!validHealth(runtime.health)) findings.push("invalid-gateway-health");
729
- return findings;
730
- }
731
-
732
- export function gatewayHealthFindings(policy, runtime, { now = new Date(), staleAfterMs = 180000 } = {}) {
733
- if (!policy.enabled || policy.killSwitch) return [];
734
- const findings = [];
735
- if (runtime.health.gateway !== "running") findings.push("gateway-not-running");
736
- if (runtime.health.scheduler !== "healthy") findings.push("scheduler-not-healthy");
737
- if (runtime.health.queue !== "healthy") findings.push("queue-not-healthy");
738
- if (!new Set(["healthy", "degraded"]).has(runtime.health.worker)) findings.push("worker-not-healthy");
739
- if (!new Set(["healthy", "degraded"]).has(runtime.health.adapter)) findings.push("adapter-not-healthy");
740
- const current = now instanceof Date ? now : new Date(now);
741
- const tick = runtime.health.lastTickAt === null ? null : new Date(runtime.health.lastTickAt);
742
- const reconciliation = runtime.health.lastReconciledAt === null ? null : new Date(runtime.health.lastReconciledAt);
743
- if (!tick || current.getTime() - tick.getTime() > staleAfterMs) findings.push("worker-heartbeat-stale");
744
- if (!reconciliation || current.getTime() - reconciliation.getTime() > staleAfterMs) findings.push("scheduler-heartbeat-stale");
745
- return findings;
746
- }
747
-
748
- export async function gatewayContext({ root = process.cwd(), agentId = null } = {}) {
749
- const { policy, runtime } = await loadGatewayRuntime(root);
750
- const findings = gatewayRuntimeFindings(policy, runtime);
751
- if (findings.length) throw new Error("gateway runtime failed closed: " + findings.join(", "));
752
- const goals = policy.goals.filter((item) => agentId === null || item.agentId === exactId(agentId, "agentId"));
753
- const queue = runtime.queue.filter((item) => agentId === null || item.agentId === exactId(agentId, "agentId"));
754
- const queueIds = new Set(queue.map((item) => item.queueId));
755
- return { schema: "agentspine.gateway-context/v1", enabled: policy.enabled, killSwitch: policy.killSwitch,
756
- goals: structuredClone(goals), queue: structuredClone(queue),
757
- outbox: structuredClone(runtime.outbox.filter((item) => queueIds.has(item.queueId))),
758
- health: structuredClone(runtime.health), healthFindings: gatewayHealthFindings(policy, runtime),
759
- authority: "execution-state-only" };
760
- }
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { open, readFile, stat, unlink, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { buildCatalog } from "./catalog.js";
5
+ import { isFileLockContention, replaceFileWithRetry } from "./filesystem-retry.js";
6
+ import { attentionFindings, loadAttention } from "./attention.js";
7
+ import {
8
+ channelRuntimeFindings, loadChannelPolicy, loadChannelRuntime
9
+ } from "./channel-runtime.js";
10
+ import { loadGraph } from "./graph.js";
11
+ import { projectStateDir } from "./paths.js";
12
+ import { loadPersonaRuntime, personaRuntimeFindings } from "./persona-runtime.js";
13
+ import { evaluateVoiceOutput } from "./voice-runtime.js";
14
+
15
+ export const GATEWAY_POLICY_SCHEMA = "agentspine.gateway-policy/v1";
16
+ export const GATEWAY_RUNTIME_SCHEMA = "agentspine.gateway-runtime/v1";
17
+ export const GATEWAY_EVENT_SCHEMA = "agentspine.gateway-event/v1";
18
+ export const GOAL_PLAN_SCHEMA = "agentspine.goal-plan/v1";
19
+ export const KNOWLEDGE_GAP_SCHEMA = "agentspine.knowledge-gap/v1";
20
+
21
+ const CONFIRMATION = "local-owner-confirmed";
22
+ const MAX_BYTES = 8 * 1024 * 1024;
23
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/+~-]{0,255}$/;
24
+ const ROUTE_RE = /^-?[A-Za-z0-9][A-Za-z0-9:_.@/+~-]{0,255}$/;
25
+ const GOAL_STATUSES = new Set(["active", "blocked", "completed", "cancelled"]);
26
+ const PLAN_STEP_STATUSES = new Set(["pending", "active", "blocked", "completed", "cancelled"]);
27
+ const QUEUE_STATUSES = new Set(["pending", "leased", "awaiting-delivery", "completed", "blocked", "dead-letter", "cancelled"]);
28
+ const OUTBOX_STATUSES = new Set(["prepared", "sending", "delivered", "failed", "dead-letter", "delivery-unknown", "acknowledged"]);
29
+ const WAKE_KINDS = new Set(["direct-message", "deadline", "promise", "resolved-blocker", "assignment", "follow-up", "relationship"]);
30
+ const PRIORITY = { "direct-message": 100, deadline: 90, promise: 90, "resolved-blocker": 80, assignment: 70, "follow-up": 60, relationship: 50 };
31
+ const SECRET_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|gh[opusu])_[A-Za-z0-9_-]{20,}\b|\bBearer\s+[A-Za-z0-9._~+/-]{20,}|\b(?:api[-_ ]?key|token|password|secret)\s*[:=]\s*\S{8,}/i;
32
+ const SECRET_KEY_RE = /"(?:api[-_ ]?key|token|password|secret|credential)"\s*:/i;
33
+ const AUTHORITY_RE = /\b(?:permission|rights?|roles?|owner|trusted|delegat|authorized|approval|production|payment|spending|tool capability|send capability)\b/i;
34
+ const HEALTH_VALUES = new Set(["stopped", "running", "unknown", "healthy", "degraded", "failed"]);
35
+ const KNOWLEDGE_EVIDENCE = new Set(["owner-input", "objective-observation"]);
36
+
37
+ function emptyPolicy(root) {
38
+ return { schema: GATEWAY_POLICY_SCHEMA, root, revision: 0, enabled: false, killSwitch: false, goals: [], history: [] };
39
+ }
40
+
41
+ function emptyRuntime(root) {
42
+ return { schema: GATEWAY_RUNTIME_SCHEMA, root, revision: 0, queue: [], lanes: [], outbox: [], receipts: [], history: [],
43
+ health: { gateway: "stopped", adapter: "unknown", scheduler: "unknown", queue: "healthy", worker: "unknown", host: "unknown", lastTickAt: null, lastReconciledAt: null } };
44
+ }
45
+
46
+ function exactId(value, field, nullable = false) {
47
+ if ((value === null || value === undefined || value === "") && nullable) return null;
48
+ if (typeof value !== "string" || !ID_RE.test(value) || value.includes("*")) throw new Error(field + " must be an exact stable ID without wildcards");
49
+ return value;
50
+ }
51
+
52
+ function safeText(value, field, maximum = 1000) {
53
+ if (typeof value !== "string" || !value.trim()) throw new Error(field + " is required");
54
+ const text = value.trim().replace(/\s+/g, " ").slice(0, maximum);
55
+ if (SECRET_RE.test(text)) throw new Error(field + " appears to contain a secret");
56
+ return text;
57
+ }
58
+
59
+ function timestamp(value = new Date()) {
60
+ const date = value instanceof Date ? value : new Date(value);
61
+ if (!Number.isFinite(date.getTime())) throw new Error("timestamp is invalid");
62
+ return date.toISOString();
63
+ }
64
+
65
+ function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
66
+
67
+ function safeCheckpoint(value) {
68
+ if (value === null || value === undefined) return null;
69
+ let content;
70
+ try { content = JSON.stringify(value); } catch { throw new Error("checkpoint must be JSON serializable"); }
71
+ if (!content || Buffer.byteLength(content) > 16384) throw new Error("checkpoint exceeds 16 KiB");
72
+ if (SECRET_RE.test(content) || SECRET_KEY_RE.test(content) || AUTHORITY_RE.test(content)) {
73
+ throw new Error("checkpoint contains secret- or authority-shaped content");
74
+ }
75
+ return JSON.parse(content);
76
+ }
77
+
78
+ function safeKnowledgeText(value, field, maximum) {
79
+ const text = safeText(value, field, maximum);
80
+ if (AUTHORITY_RE.test(text)) throw new Error(field + " cannot grant or describe authority");
81
+ return text;
82
+ }
83
+
84
+ function knowledgeGapSubjectMaterial(gap) {
85
+ return {
86
+ goalId: gap.goalId, goalStepId: gap.goalStepId, planDefinitionsDigest: gap.planDefinitionsDigest,
87
+ question: gap.question, reason: gap.reason, requiredEvidence: gap.requiredEvidence,
88
+ authority: "context-only"
89
+ };
90
+ }
91
+
92
+ function knowledgeGapRequestMaterial(gap) {
93
+ return {
94
+ subjectDigest: gap.subjectDigest, requestedAt: gap.requestedAt,
95
+ requestedByQueueId: gap.requestedByQueueId, authority: "context-only"
96
+ };
97
+ }
98
+
99
+ function knowledgeGapResolutionMaterial(gap) {
100
+ return {
101
+ requestDigest: gap.requestDigest, answer: gap.answer, answerSource: gap.answerSource,
102
+ sourceDigest: gap.sourceDigest, resolvedAt: gap.resolvedAt,
103
+ resolvedBySubjectId: gap.resolvedBySubjectId, authority: "context-only"
104
+ };
105
+ }
106
+
107
+ function validKnowledgeGap(gap) {
108
+ if (!(gap && gap.schema === KNOWLEDGE_GAP_SCHEMA && ID_RE.test(gap.gapId || "")
109
+ && ID_RE.test(gap.goalId || "") && ID_RE.test(gap.goalStepId || "")
110
+ && /^[a-f0-9]{64}$/.test(gap.planDefinitionsDigest || "")
111
+ && typeof gap.question === "string" && gap.question.length <= 500
112
+ && typeof gap.reason === "string" && gap.reason.length <= 500
113
+ && KNOWLEDGE_EVIDENCE.has(gap.requiredEvidence)
114
+ && ID_RE.test(gap.requestedByQueueId || "")
115
+ && Number.isFinite(new Date(gap.requestedAt).getTime())
116
+ && /^[a-f0-9]{64}$/.test(gap.subjectDigest || "")
117
+ && /^[a-f0-9]{64}$/.test(gap.requestDigest || "")
118
+ && ["open", "resolved"].includes(gap.status) && gap.authority === "context-only")) return false;
119
+ try {
120
+ if (safeKnowledgeText(gap.question, "knowledgeGap.question", 500) !== gap.question
121
+ || safeKnowledgeText(gap.reason, "knowledgeGap.reason", 500) !== gap.reason) return false;
122
+ } catch { return false; }
123
+ if (gap.subjectDigest !== sha256(JSON.stringify(knowledgeGapSubjectMaterial(gap)))
124
+ || gap.gapId !== "knowledge-gap:" + gap.subjectDigest.slice(0, 32)
125
+ || gap.requestDigest !== sha256(JSON.stringify(knowledgeGapRequestMaterial(gap)))) return false;
126
+ if (gap.status === "open") return gap.answer === null && gap.answerSource === null && gap.sourceDigest === null
127
+ && gap.resolvedAt === null && gap.resolvedBySubjectId === null && gap.resolutionDigest === null;
128
+ if (!(typeof gap.answer === "string" && gap.answer.length <= 2000
129
+ && gap.answerSource === gap.requiredEvidence
130
+ && (gap.sourceDigest === null || /^[a-f0-9]{64}$/.test(gap.sourceDigest))
131
+ && Number.isFinite(new Date(gap.resolvedAt).getTime()) && ID_RE.test(gap.resolvedBySubjectId || "")
132
+ && /^[a-f0-9]{64}$/.test(gap.resolutionDigest || ""))) return false;
133
+ if (new Date(gap.resolvedAt) < new Date(gap.requestedAt)) return false;
134
+ if ((gap.answerSource === "objective-observation") !== (gap.sourceDigest !== null)) return false;
135
+ try { if (safeKnowledgeText(gap.answer, "knowledgeGap.answer", 2000) !== gap.answer) return false; } catch { return false; }
136
+ return gap.resolutionDigest === sha256(JSON.stringify(knowledgeGapResolutionMaterial(gap)));
137
+ }
138
+
139
+ function planDefinitionMaterial(steps) {
140
+ return steps.map(({ stepId, title, successCriterion, dependsOn }) => ({ stepId, title, successCriterion, dependsOn }));
141
+ }
142
+
143
+ function validGoalPlan(plan) {
144
+ if (!(plan && plan.schema === GOAL_PLAN_SCHEMA && plan.authority === "context-only-plan"
145
+ && Number.isInteger(plan.revision) && plan.revision >= 0 && Array.isArray(plan.steps)
146
+ && plan.steps.length > 0 && plan.steps.length <= 32 && /^[a-f0-9]{64}$/.test(plan.definitionsDigest || "")
147
+ && (plan.currentStepId === null || ID_RE.test(plan.currentStepId || "")))) return false;
148
+ const ids = new Set(plan.steps.map((step) => step?.stepId));
149
+ if (ids.size !== plan.steps.length || ids.has(undefined)) return false;
150
+ for (const step of plan.steps) {
151
+ if (!(ID_RE.test(step.stepId || "") && typeof step.title === "string" && step.title.length > 0 && step.title.length <= 500
152
+ && typeof step.successCriterion === "string" && step.successCriterion.length > 0 && step.successCriterion.length <= 1000
153
+ && Array.isArray(step.dependsOn) && new Set(step.dependsOn).size === step.dependsOn.length
154
+ && step.dependsOn.every((dependency) => ids.has(dependency) && dependency !== step.stepId)
155
+ && PLAN_STEP_STATUSES.has(step.status)
156
+ && (step.checkpoint === null || (() => { try { safeCheckpoint(step.checkpoint); return true; } catch { return false; } })())
157
+ && (step.blocker === null || (typeof step.blocker === "string" && step.blocker.length <= 500 && !SECRET_RE.test(step.blocker)))
158
+ && (step.completedAt === null || Number.isFinite(new Date(step.completedAt).getTime()))
159
+ && (step.completedByQueueId === null || ID_RE.test(step.completedByQueueId || ""))
160
+ && (step.knowledgeGaps === undefined || (Array.isArray(step.knowledgeGaps) && step.knowledgeGaps.length <= 16
161
+ && new Set(step.knowledgeGaps.map((gap) => gap.gapId)).size === step.knowledgeGaps.length
162
+ && step.knowledgeGaps.every((gap) => validKnowledgeGap(gap)
163
+ && gap.goalStepId === step.stepId && gap.planDefinitionsDigest === plan.definitionsDigest)))
164
+ && Number.isFinite(new Date(step.updatedAt).getTime()))) return false;
165
+ }
166
+ const visiting = new Set(); const visited = new Set();
167
+ const visit = (stepId) => {
168
+ if (visiting.has(stepId)) return false;
169
+ if (visited.has(stepId)) return true;
170
+ visiting.add(stepId);
171
+ const step = plan.steps.find((entry) => entry.stepId === stepId);
172
+ if (!step.dependsOn.every(visit)) return false;
173
+ visiting.delete(stepId); visited.add(stepId); return true;
174
+ };
175
+ if (!plan.steps.every((step) => visit(step.stepId))) return false;
176
+ const completed = new Set(plan.steps.filter((step) => step.status === "completed").map((step) => step.stepId));
177
+ if (plan.steps.some((step) => ["completed", "active", "blocked"].includes(step.status)
178
+ && !step.dependsOn.every((dependency) => completed.has(dependency)))) return false;
179
+ if (plan.steps.some((step) => (step.status === "completed") !== (step.completedAt !== null && step.completedByQueueId !== null))) return false;
180
+ if (plan.steps.some((step) => (step.status === "blocked") !== (step.blocker !== null))) return false;
181
+ if (plan.steps.some((step) => (step.knowledgeGaps || []).filter((gap) => gap.status === "open").length > 1)) return false;
182
+ if (plan.steps.some((step) => (step.knowledgeGaps || []).some((gap) => gap.status === "open")
183
+ && step.status !== "blocked")) return false;
184
+ if (plan.definitionsDigest !== sha256(JSON.stringify(planDefinitionMaterial(plan.steps)))) return false;
185
+ const current = plan.steps.filter((step) => ["active", "blocked"].includes(step.status));
186
+ return current.length <= 1 && (plan.currentStepId === null
187
+ ? current.length === 0 : current.length === 1 && current[0].stepId === plan.currentStepId);
188
+ }
189
+
190
+ function activateNextPlanStep(plan, now) {
191
+ const completed = new Set(plan.steps.filter((step) => step.status === "completed").map((step) => step.stepId));
192
+ const next = plan.steps.find((step) => step.status === "pending" && step.dependsOn.every((dependency) => completed.has(dependency)));
193
+ if (!next) { plan.currentStepId = null; return null; }
194
+ next.status = "active"; next.updatedAt = now; plan.currentStepId = next.stepId; plan.revision += 1;
195
+ return next;
196
+ }
197
+
198
+ function createGoalPlan(steps, now) {
199
+ if (!Array.isArray(steps) || steps.length === 0 || steps.length > 32) throw new Error("goal plan requires 1-32 steps");
200
+ const normalized = steps.map((step, index) => ({
201
+ stepId: exactId(step?.stepId ?? step?.id, `steps[${index}].stepId`),
202
+ title: safeText(step?.title, `steps[${index}].title`, 500),
203
+ successCriterion: safeText(step?.successCriterion, `steps[${index}].successCriterion`),
204
+ dependsOn: Array.isArray(step?.dependsOn) ? step.dependsOn.map((dependency) => exactId(dependency, `steps[${index}].dependsOn`)) : [],
205
+ status: "pending", checkpoint: null, blocker: null, completedAt: null, completedByQueueId: null,
206
+ knowledgeGaps: [], updatedAt: now
207
+ }));
208
+ const plan = { schema: GOAL_PLAN_SCHEMA, revision: 0, currentStepId: null, steps: normalized,
209
+ definitionsDigest: sha256(JSON.stringify(planDefinitionMaterial(normalized))), authority: "context-only-plan" };
210
+ if (!validGoalPlan(plan)) throw new Error("goal plan must be an acyclic dependency graph with exact unique step IDs");
211
+ activateNextPlanStep(plan, now);
212
+ return plan;
213
+ }
214
+
215
+ function currentPlanStep(goal) {
216
+ return goal.plan?.steps.find((step) => step.stepId === goal.plan.currentStepId) || null;
217
+ }
218
+
219
+ function createKnowledgeGap(goal, step, queueId, request, now) {
220
+ const gap = {
221
+ schema: KNOWLEDGE_GAP_SCHEMA, gapId: null, goalId: goal.goalId, goalStepId: step.stepId,
222
+ planDefinitionsDigest: goal.plan.definitionsDigest,
223
+ question: safeKnowledgeText(request?.question, "knowledgeGap.question", 500),
224
+ reason: safeKnowledgeText(request?.reason, "knowledgeGap.reason", 500),
225
+ requiredEvidence: request?.requiredEvidence,
226
+ requestedAt: now, requestedByQueueId: exactId(queueId, "requestedByQueueId"),
227
+ subjectDigest: null, requestDigest: null, status: "open", answer: null, answerSource: null,
228
+ sourceDigest: null, resolvedAt: null, resolvedBySubjectId: null, resolutionDigest: null,
229
+ authority: "context-only"
230
+ };
231
+ if (!KNOWLEDGE_EVIDENCE.has(gap.requiredEvidence)) {
232
+ throw new Error("knowledgeGap.requiredEvidence must be owner-input or objective-observation");
233
+ }
234
+ gap.subjectDigest = sha256(JSON.stringify(knowledgeGapSubjectMaterial(gap)));
235
+ gap.gapId = "knowledge-gap:" + gap.subjectDigest.slice(0, 32);
236
+ gap.requestDigest = sha256(JSON.stringify(knowledgeGapRequestMaterial(gap)));
237
+ if (!validKnowledgeGap(gap)) throw new Error("knowledge gap request is invalid");
238
+ return gap;
239
+ }
240
+
241
+ function planQueueKey(goalId, stepId, phase, suffix = "") {
242
+ return ["goal", goalId, "step", stepId, phase, suffix].filter(Boolean).join(":");
243
+ }
244
+
245
+ function newGoalQueue(goal, step, kind, key, current, availableAt = current) {
246
+ return { queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind,
247
+ agentId: goal.agentId, projectId: goal.projectId, groupId: goal.groupId, goalId: goal.goalId,
248
+ goalStepId: step?.stepId || null, channelEventId: null, priority: PRIORITY[kind], status: "pending",
249
+ attempts: 0, lease: null, availableAt, createdAt: current, updatedAt: current,
250
+ completedAt: null, lastError: null, authority: "execution-state-only" };
251
+ }
252
+
253
+ function validGoal(goal) {
254
+ return goal && ID_RE.test(goal.goalId || "") && ID_RE.test(goal.agentId || "")
255
+ && ID_RE.test(goal.ownerSubjectId || "") && ID_RE.test(goal.projectId || "")
256
+ && (goal.groupId === null || ID_RE.test(goal.groupId || "")) && GOAL_STATUSES.has(goal.status)
257
+ && typeof goal.successCriterion === "string" && goal.successCriterion.length > 0
258
+ && typeof goal.nextSafeStep === "string" && goal.nextSafeStep.length > 0
259
+ && (goal.deadline === null || Number.isFinite(new Date(goal.deadline).getTime()))
260
+ && Number.isInteger(goal.priority) && goal.priority >= 0 && goal.priority <= 100
261
+ && (goal.checkpoint === null || (() => { try { safeCheckpoint(goal.checkpoint); return true; } catch { return false; } })())
262
+ && (goal.blocker === null || (typeof goal.blocker === "string" && goal.blocker.length <= 500 && !SECRET_RE.test(goal.blocker)))
263
+ && (goal.heartbeatAt === null || Number.isFinite(new Date(goal.heartbeatAt).getTime()))
264
+ && goal.authority === "authenticated-goal-policy" && Number.isFinite(new Date(goal.createdAt).getTime())
265
+ && Number.isFinite(new Date(goal.updatedAt).getTime())
266
+ && (goal.plan === undefined || goal.plan === null || (validGoalPlan(goal.plan)
267
+ && goal.plan.steps.every((step) => (step.knowledgeGaps || []).every((gap) => gap.goalId === goal.goalId))
268
+ && (goal.status !== "active" || (currentPlanStep(goal)?.status === "active" && goal.nextSafeStep === currentPlanStep(goal).title))
269
+ && (goal.status !== "blocked" || currentPlanStep(goal)?.status === "blocked")
270
+ && (goal.status !== "completed" || goal.plan.steps.every((step) => step.status === "completed"))));
271
+ }
272
+
273
+ function validPolicyHistory(item) {
274
+ if (!item || item.authority !== "authenticated-goal-policy" || !Number.isFinite(new Date(item.at).getTime())) return false;
275
+ if (item.kind === "goal") return validGoal(item.value);
276
+ return item.kind === "control" && item.value && typeof item.value.enabled === "boolean" && typeof item.value.killSwitch === "boolean";
277
+ }
278
+
279
+ function validQueue(item) {
280
+ return item && ID_RE.test(item.queueId || "") && WAKE_KINDS.has(item.kind)
281
+ && ID_RE.test(item.dedupeKey || "") && item.queueId === "gateway-queue:" + sha256(item.dedupeKey).slice(0, 32)
282
+ && ID_RE.test(item.agentId || "") && ID_RE.test(item.projectId || "")
283
+ && (item.groupId === null || ID_RE.test(item.groupId || ""))
284
+ && (item.goalId === null || ID_RE.test(item.goalId || ""))
285
+ && (item.goalStepId === undefined || item.goalStepId === null || (item.goalId !== null && ID_RE.test(item.goalStepId || "")))
286
+ && (item.channelEventId === null || ID_RE.test(item.channelEventId || ""))
287
+ && QUEUE_STATUSES.has(item.status) && Number.isInteger(item.priority) && item.priority >= 0 && item.priority <= 100
288
+ && Number.isInteger(item.attempts) && item.attempts >= 0 && item.attempts <= 20
289
+ && item.authority === "execution-state-only" && Number.isFinite(new Date(item.createdAt).getTime())
290
+ && Number.isFinite(new Date(item.availableAt).getTime()) && Number.isFinite(new Date(item.updatedAt).getTime())
291
+ && (item.completedAt === null || Number.isFinite(new Date(item.completedAt).getTime()))
292
+ && (item.lastError === null || (typeof item.lastError === "string" && item.lastError.length <= 500 && !SECRET_RE.test(item.lastError)))
293
+ && (item.lease === null || (item.status === "leased" && ID_RE.test(item.lease.workerId || "")
294
+ && Number.isFinite(new Date(item.lease.expiresAt).getTime())));
295
+ }
296
+
297
+ function validOutbox(item) {
298
+ return item && ID_RE.test(item.outboxId || "") && ID_RE.test(item.queueId || "")
299
+ && ID_RE.test(item.idempotencyKey || "") && ID_RE.test(item.bindingId || "")
300
+ && item.outboxId === "gateway-outbox:" + sha256(item.idempotencyKey).slice(0, 32)
301
+ && ID_RE.test(item.eventId || "") && ROUTE_RE.test(item.provider || "") && ROUTE_RE.test(item.tenantId || "")
302
+ && ROUTE_RE.test(item.accountId || "") && ROUTE_RE.test(item.chatId || "")
303
+ && (item.threadId === null || ROUTE_RE.test(item.threadId || "")) && (item.replyTo === null || ROUTE_RE.test(item.replyTo || ""))
304
+ && OUTBOX_STATUSES.has(item.status)
305
+ && typeof item.text === "string" && item.text.length > 0 && item.text.length <= 16000 && !SECRET_RE.test(item.text)
306
+ && Number.isInteger(item.attempts) && item.attempts >= 0 && item.attempts <= 20
307
+ && item.authority === "delivery-state-only" && Number.isFinite(new Date(item.createdAt).getTime())
308
+ && Number.isFinite(new Date(item.updatedAt).getTime()) && Number.isFinite(new Date(item.nextAttemptAt).getTime())
309
+ && (item.deliveredAt === null || Number.isFinite(new Date(item.deliveredAt).getTime()))
310
+ && (item.adapterReceipt === null || (typeof item.adapterReceipt === "string" && item.adapterReceipt.length <= 500))
311
+ && (item.lastError === null || (typeof item.lastError === "string" && item.lastError.length <= 500 && !SECRET_RE.test(item.lastError)));
312
+ }
313
+
314
+ function validLane(item) {
315
+ return item && ID_RE.test(item.agentId || "") && ID_RE.test(item.queueId || "") && ID_RE.test(item.workerId || "")
316
+ && new Set(["leased", "completed", "expired"]).has(item.status)
317
+ && Number.isFinite(new Date(item.claimedAt).getTime()) && Number.isFinite(new Date(item.expiresAt).getTime())
318
+ && Number.isFinite(new Date(item.updatedAt).getTime()) && item.authority === "execution-state-only";
319
+ }
320
+
321
+ function validReceipt(item) {
322
+ if (!(item && ID_RE.test(item.id || "") && ID_RE.test(item.kind || "") && ID_RE.test(item.objectId || "")
323
+ && Number.isFinite(new Date(item.at).getTime()) && item.details && typeof item.details === "object"
324
+ && !Array.isArray(item.details) && !SECRET_RE.test(JSON.stringify(item.details))
325
+ && item.authority === "execution-state-only" && /^[a-f0-9]{64}$/.test(item.digest || ""))) return false;
326
+ const material = { kind: item.kind, objectId: item.objectId, at: item.at, details: item.details, authority: "execution-state-only" };
327
+ return item.digest === sha256(JSON.stringify(material)) && item.id === "gateway-receipt:" + item.digest.slice(0, 24);
328
+ }
329
+
330
+ function validHistory(item) {
331
+ if (!(item && new Set(["queue", "outbox"]).has(item.kind) && ID_RE.test(item.objectId || "")
332
+ && ID_RE.test(item.transition || "") && Number.isFinite(new Date(item.at).getTime())
333
+ && item.value && item.authority === "execution-state-only")) return false;
334
+ return item.kind === "queue" ? validQueue(item.value) && item.objectId === item.value.queueId
335
+ : validOutbox(item.value) && item.objectId === item.value.outboxId;
336
+ }
337
+
338
+ function validHealth(health) {
339
+ return health && ["gateway", "adapter", "scheduler", "queue", "worker", "host"].every((key) => HEALTH_VALUES.has(health[key]))
340
+ && (health.lastTickAt === null || Number.isFinite(new Date(health.lastTickAt).getTime()))
341
+ && (health.lastReconciledAt === null || Number.isFinite(new Date(health.lastReconciledAt).getTime()));
342
+ }
343
+
344
+ function normalizePolicy(value, root) {
345
+ if (!value || value.schema !== GATEWAY_POLICY_SCHEMA || value.root !== root
346
+ || !Number.isInteger(value.revision) || typeof value.enabled !== "boolean" || typeof value.killSwitch !== "boolean"
347
+ || !Array.isArray(value.goals) || !Array.isArray(value.history) || value.goals.some((item) => !validGoal(item))
348
+ || value.history.some((item) => !validPolicyHistory(item))) {
349
+ throw new Error("gateway policy is invalid; autonomous runtime is disabled");
350
+ }
351
+ return value;
352
+ }
353
+
354
+ function normalizeRuntime(value, root) {
355
+ const baseValid = value && value.schema === GATEWAY_RUNTIME_SCHEMA && value.root === root && Number.isInteger(value.revision)
356
+ && Array.isArray(value.queue) && Array.isArray(value.lanes) && Array.isArray(value.outbox)
357
+ && Array.isArray(value.receipts) && Array.isArray(value.history);
358
+ if (!baseValid) throw new Error("gateway runtime structure is invalid; worker is disabled");
359
+ const invalid = [];
360
+ if (!validHealth(value.health)) invalid.push("health");
361
+ const queueIndex = value.queue.findIndex((item) => !validQueue(item));
362
+ const laneIndex = value.lanes.findIndex((item) => !validLane(item));
363
+ const outboxIndex = value.outbox.findIndex((item) => !validOutbox(item));
364
+ const receiptIndex = value.receipts.findIndex((item) => !validReceipt(item));
365
+ const historyIndex = value.history.findIndex((item) => !validHistory(item));
366
+ if (queueIndex >= 0) invalid.push("queue:" + queueIndex);
367
+ if (laneIndex >= 0) invalid.push("lanes:" + laneIndex);
368
+ if (outboxIndex >= 0) invalid.push("outbox:" + outboxIndex);
369
+ if (receiptIndex >= 0) invalid.push("receipts:" + receiptIndex);
370
+ if (historyIndex >= 0) invalid.push("history:" + historyIndex);
371
+ if (invalid.length) throw new Error("gateway runtime is invalid (" + invalid.join(",") + "); worker is disabled");
372
+ return value;
373
+ }
374
+
375
+ async function pathsFor(root, catalog = null) {
376
+ catalog ||= await buildCatalog(root);
377
+ const directory = await projectStateDir(catalog.root);
378
+ return { catalog, directory, gatewayPolicyPath: join(directory, "gateway-policy.json"), gatewayRuntimePath: join(directory, "gateway-runtime.json") };
379
+ }
380
+
381
+ async function readJson(path, root, normalize, empty) {
382
+ try {
383
+ const metadata = await stat(path);
384
+ if (metadata.size > MAX_BYTES) throw new Error("gateway state exceeds 8 MiB");
385
+ return normalize(JSON.parse(await readFile(path, "utf8")), root);
386
+ } catch (error) {
387
+ if (error.code !== "ENOENT") throw error;
388
+ return empty(root);
389
+ }
390
+ }
391
+
392
+ async function writeJson(path, value) {
393
+ const content = JSON.stringify(value, null, 2) + "\n";
394
+ if (Buffer.byteLength(content) > MAX_BYTES) throw new Error("gateway state exceeds 8 MiB");
395
+ const temporary = path + "." + process.pid + "." + randomUUID() + ".tmp";
396
+ try { await writeFile(temporary, content, { mode: 0o600 }); await replaceFileWithRetry(temporary, path); }
397
+ finally { await unlink(temporary).catch((error) => { if (error.code !== "ENOENT") throw error; }); }
398
+ }
399
+
400
+ async function withLock(paths, task) {
401
+ const lockPath = join(paths.directory, "gateway-runtime.lock");
402
+ let handle;
403
+ for (let attempt = 0; attempt < 160; attempt += 1) {
404
+ try { handle = await open(lockPath, "wx", 0o600); break; } catch (error) {
405
+ if (!isFileLockContention(error)) throw error;
406
+ try { const metadata = await stat(lockPath); if (Date.now() - metadata.mtimeMs > 120000) await unlink(lockPath); }
407
+ catch (lockError) { if (lockError.code !== "ENOENT") throw lockError; }
408
+ await new Promise((resolve) => setTimeout(resolve, 25));
409
+ }
410
+ }
411
+ if (!handle) throw new Error("gateway state is busy; retry later");
412
+ try { return await task(); } finally {
413
+ await handle.close();
414
+ await unlink(lockPath).catch((error) => { if (error.code !== "ENOENT") throw error; });
415
+ }
416
+ }
417
+
418
+ function appendReceipt(runtime, kind, objectId, now, details = {}) {
419
+ const material = { kind, objectId, at: now, details, authority: "execution-state-only" };
420
+ const digest = sha256(JSON.stringify(material));
421
+ const id = "gateway-receipt:" + digest.slice(0, 24);
422
+ const previous = runtime.receipts.find((item) => item.id === id);
423
+ if (previous) return previous;
424
+ const receipt = { id, ...material, digest };
425
+ runtime.receipts.push(receipt);
426
+ return receipt;
427
+ }
428
+
429
+ function preserve(runtime, kind, value, transition, now) {
430
+ runtime.history.push({ kind, objectId: kind === "outbox" ? value.outboxId : value.queueId, transition, at: now,
431
+ value: structuredClone(value), authority: "execution-state-only" });
432
+ }
433
+
434
+ function currentLane(runtime, agentId) {
435
+ return runtime.lanes.find((item) => item.agentId === agentId && item.status === "leased") || null;
436
+ }
437
+
438
+ function assertActivePersona(personaPolicy, personaRuntime, agentId, projectId, groupId) {
439
+ const findings = personaRuntimeFindings(personaPolicy, personaRuntime);
440
+ if (findings.length) throw new Error("persona runtime is unhealthy: " + findings.join(", "));
441
+ const persona = personaRuntime.personas.find((item) => item.personaId === agentId && item.status === "active");
442
+ if (!persona || !["agent", "bot"].includes(persona.kind)) throw new Error("gateway work requires an active authenticated agent or bot");
443
+ if (groupId !== null && persona.groupId !== groupId) throw new Error("gateway work group does not match authenticated persona membership");
444
+ const binding = personaPolicy.bindings.find((item) => item.id === persona.bindingId && item.active);
445
+ if (!binding || !binding.tenantId || !binding.profileId || !projectId) throw new Error("gateway work lacks an active authenticated identity binding");
446
+ return { persona, binding };
447
+ }
448
+
449
+ export async function setGatewayControl({ root = process.cwd(), enabled, killSwitch, confirmation, now = new Date() }) {
450
+ if (confirmation !== CONFIRMATION) throw new Error("gateway control changes require explicit local owner confirmation");
451
+ const paths = await pathsFor(root);
452
+ return withLock(paths, async () => {
453
+ const policy = await readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy);
454
+ const previous = { enabled: policy.enabled, killSwitch: policy.killSwitch };
455
+ if (enabled !== undefined) policy.enabled = Boolean(enabled);
456
+ if (killSwitch !== undefined) policy.killSwitch = Boolean(killSwitch);
457
+ policy.revision += 1;
458
+ policy.history.push({ kind: "control", at: timestamp(now), value: previous, authority: "authenticated-goal-policy" });
459
+ await writeJson(paths.gatewayPolicyPath, policy);
460
+ return { enabled: policy.enabled, killSwitch: policy.killSwitch, revision: policy.revision };
461
+ });
462
+ }
463
+
464
+ export async function assignGoal({ root = process.cwd(), goalId, agentId, ownerSubjectId, projectId, groupId = null,
465
+ priority = 70, successCriterion, nextSafeStep = null, steps = null, deadline = null, confirmation, now = new Date() }) {
466
+ if (confirmation !== CONFIRMATION) throw new Error("goal assignment requires explicit local owner confirmation");
467
+ const paths = await pathsFor(root);
468
+ return withLock(paths, async () => {
469
+ const [policy, runtime, personas] = await Promise.all([
470
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
471
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
472
+ loadPersonaRuntime(paths.catalog.root, paths.catalog)
473
+ ]);
474
+ agentId = exactId(agentId, "agentId"); projectId = exactId(projectId, "projectId"); groupId = exactId(groupId, "groupId", true);
475
+ assertActivePersona(personas.policy, personas.runtime, agentId, projectId, groupId);
476
+ const createdAt = timestamp(now);
477
+ const active = policy.goals.find((item) => item.agentId === agentId && item.status === "active" && item.goalId !== goalId);
478
+ if (active) throw new Error("an agent may have only one active focused goal");
479
+ const plan = steps === null ? null : createGoalPlan(steps, createdAt);
480
+ const firstStep = plan ? plan.steps.find((step) => step.stepId === plan.currentStepId) : null;
481
+ const goal = { goalId: exactId(goalId, "goalId"), agentId, ownerSubjectId: exactId(ownerSubjectId, "ownerSubjectId"),
482
+ projectId, groupId, priority: Number(priority), successCriterion: safeText(successCriterion, "successCriterion"),
483
+ nextSafeStep: safeText(firstStep?.title || nextSafeStep, "nextSafeStep"), deadline: deadline === null ? null : timestamp(deadline),
484
+ status: "active", checkpoint: null, heartbeatAt: null, blocker: null, createdAt, updatedAt: createdAt,
485
+ plan, authority: "authenticated-goal-policy" };
486
+ if (!validGoal(goal)) throw new Error("goal assignment is invalid");
487
+ const previous = policy.goals.find((item) => item.goalId === goal.goalId);
488
+ if (previous && [previous.agentId, previous.ownerSubjectId, previous.projectId, previous.groupId].join("\0")
489
+ !== [goal.agentId, goal.ownerSubjectId, goal.projectId, goal.groupId].join("\0")) throw new Error("goal scope is immutable");
490
+ if (previous?.plan && previous.plan.definitionsDigest !== goal.plan?.definitionsDigest) {
491
+ throw new Error("goal plan definitions are immutable; assign a new goal ID");
492
+ }
493
+ if (previous?.plan && previous.status === "blocked") {
494
+ const blockedStep = currentPlanStep(previous);
495
+ if (!blockedStep || blockedStep.status !== "blocked") throw new Error("blocked goal plan has no bound blocked step");
496
+ if ((blockedStep.knowledgeGaps || []).some((gap) => gap.status === "open")) {
497
+ throw new Error("blocked goal plan has an open knowledge gap; resolve it with goal-clarify");
498
+ }
499
+ policy.history.push({ kind: "goal", at: createdAt, value: structuredClone(previous), authority: "authenticated-goal-policy" });
500
+ blockedStep.status = "active"; blockedStep.blocker = null; blockedStep.updatedAt = createdAt;
501
+ previous.status = "active"; previous.blocker = null; previous.updatedAt = createdAt; previous.plan.revision += 1;
502
+ policy.revision += 1;
503
+ const key = planQueueKey(previous.goalId, blockedStep.stepId, "owner-resume", String(previous.plan.revision));
504
+ const queued = newGoalQueue(previous, blockedStep, "follow-up", key, createdAt);
505
+ runtime.queue.push(queued); runtime.revision += 1;
506
+ appendReceipt(runtime, "goal-step-resumed", queued.queueId, createdAt, { goalStepId: blockedStep.stepId });
507
+ await Promise.all([writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)]);
508
+ return { goal: structuredClone(previous), gatewayPolicyPath: paths.gatewayPolicyPath, resumed: true };
509
+ }
510
+ if (previous?.plan) return { goal: structuredClone(previous), gatewayPolicyPath: paths.gatewayPolicyPath, duplicate: true };
511
+ if (previous) {
512
+ policy.history.push({ kind: "goal", at: createdAt, value: structuredClone(previous), authority: "authenticated-goal-policy" });
513
+ goal.createdAt = previous.createdAt;
514
+ }
515
+ policy.goals = policy.goals.filter((item) => item.goalId !== goal.goalId);
516
+ policy.goals.push(goal);
517
+ policy.revision += 1;
518
+ const key = firstStep ? planQueueKey(goal.goalId, firstStep.stepId, "assignment") : "goal:" + goal.goalId + ":assignment";
519
+ if (!runtime.queue.some((item) => item.dedupeKey === key)) {
520
+ runtime.queue.push(newGoalQueue(goal, firstStep, "assignment", key, createdAt));
521
+ runtime.revision += 1;
522
+ appendReceipt(runtime, "queued", "gateway-queue:" + sha256(key).slice(0, 32), createdAt, { kind: "assignment", dedupeKey: key });
523
+ }
524
+ await Promise.all([writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)]);
525
+ return { goal, gatewayPolicyPath: paths.gatewayPolicyPath };
526
+ });
527
+ }
528
+
529
+ export async function resolveGoalKnowledgeGap({ root = process.cwd(), goalId, gapId, answer,
530
+ answerSource = "owner-input", sourceDigest = null, confirmation, now = new Date() }) {
531
+ if (confirmation !== CONFIRMATION) throw new Error("knowledge gap resolution requires explicit local owner confirmation");
532
+ const paths = await pathsFor(root);
533
+ return withLock(paths, async () => {
534
+ const [policy, runtime, personas] = await Promise.all([
535
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
536
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
537
+ loadPersonaRuntime(paths.catalog.root, paths.catalog)
538
+ ]);
539
+ goalId = exactId(goalId, "goalId"); gapId = exactId(gapId, "gapId");
540
+ const goal = policy.goals.find((item) => item.goalId === goalId);
541
+ const step = goal && currentPlanStep(goal);
542
+ if (!goal?.plan || !step) throw new Error("knowledge gap resolution requires the exact current goal-plan step");
543
+ assertActivePersona(personas.policy, personas.runtime, goal.agentId, goal.projectId, goal.groupId);
544
+ const gap = (step.knowledgeGaps || []).find((item) => item.gapId === gapId);
545
+ if (!gap) throw new Error("knowledge gap is not bound to the current goal-plan step");
546
+ answer = safeKnowledgeText(answer, "answer", 2000);
547
+ if (!KNOWLEDGE_EVIDENCE.has(answerSource) || answerSource !== gap.requiredEvidence) {
548
+ throw new Error("answer source does not satisfy the requested evidence class");
549
+ }
550
+ sourceDigest = sourceDigest === null || sourceDigest === undefined || sourceDigest === "" ? null : String(sourceDigest);
551
+ if ((answerSource === "objective-observation") !== (/^[a-f0-9]{64}$/.test(sourceDigest || ""))) {
552
+ throw new Error("objective observation answers require one exact SHA-256 source digest");
553
+ }
554
+ if (gap.status === "resolved") {
555
+ if (gap.answer !== answer || gap.answerSource !== answerSource || gap.sourceDigest !== sourceDigest) {
556
+ throw new Error("knowledge gap already has a different bound resolution");
557
+ }
558
+ return { goal: structuredClone(goal), gap: structuredClone(gap), duplicate: true };
559
+ }
560
+ if (goal.status !== "blocked" || step.status !== "blocked") {
561
+ throw new Error("open knowledge gap resolution requires the exact blocked goal-plan step");
562
+ }
563
+ const resolvedAt = timestamp(now);
564
+ const candidate = { ...gap, status: "resolved", answer, answerSource, sourceDigest,
565
+ resolvedAt, resolvedBySubjectId: goal.ownerSubjectId, resolutionDigest: null };
566
+ candidate.resolutionDigest = sha256(JSON.stringify(knowledgeGapResolutionMaterial(candidate)));
567
+ if (!validKnowledgeGap(candidate)) throw new Error("knowledge gap resolution is invalid");
568
+ policy.history.push({ kind: "goal", at: resolvedAt, value: structuredClone(goal), authority: "authenticated-goal-policy" });
569
+ Object.assign(gap, candidate);
570
+ step.status = "active"; step.blocker = null; step.updatedAt = resolvedAt;
571
+ goal.status = "active"; goal.blocker = null; goal.updatedAt = resolvedAt; goal.nextSafeStep = step.title;
572
+ goal.plan.revision += 1; policy.revision += 1;
573
+ const key = planQueueKey(goal.goalId, step.stepId, "clarified", gap.resolutionDigest.slice(0, 20));
574
+ let queued = runtime.queue.find((item) => item.dedupeKey === key);
575
+ if (!queued) {
576
+ queued = newGoalQueue(goal, step, "follow-up", key, resolvedAt);
577
+ runtime.queue.push(queued); runtime.revision += 1;
578
+ appendReceipt(runtime, "knowledge-gap-resolved", queued.queueId, resolvedAt, {
579
+ goalStepId: step.stepId, gapId: gap.gapId, answerSource
580
+ });
581
+ }
582
+ await Promise.all([writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)]);
583
+ return { goal: structuredClone(goal), gap: structuredClone(gap), queueId: queued.queueId, duplicate: false };
584
+ });
585
+ }
586
+
587
+ export async function enqueueGatewayWake({ root = process.cwd(), kind, agentId, projectId, groupId = null, goalId = null,
588
+ channelEventId = null, dedupeKey, availableAt = null, now = new Date() }) {
589
+ if (!WAKE_KINDS.has(kind)) throw new Error("unsupported gateway wake kind");
590
+ const paths = await pathsFor(root);
591
+ return withLock(paths, async () => {
592
+ const [policy, runtime, personas] = await Promise.all([
593
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
594
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
595
+ loadPersonaRuntime(paths.catalog.root, paths.catalog)
596
+ ]);
597
+ if (!policy.enabled || policy.killSwitch) throw new Error("gateway is disabled by local policy");
598
+ agentId = exactId(agentId, "agentId"); projectId = exactId(projectId, "projectId"); groupId = exactId(groupId, "groupId", true);
599
+ assertActivePersona(personas.policy, personas.runtime, agentId, projectId, groupId);
600
+ goalId = exactId(goalId, "goalId", true); channelEventId = exactId(channelEventId, "channelEventId", true);
601
+ const key = exactId(dedupeKey, "dedupeKey");
602
+ const existing = runtime.queue.find((item) => item.dedupeKey === key);
603
+ if (existing) return { item: existing, duplicate: true };
604
+ const createdAt = timestamp(now);
605
+ const item = { queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind, agentId, projectId, groupId,
606
+ goalId, goalStepId: null, channelEventId, priority: PRIORITY[kind], status: "pending", attempts: 0, lease: null,
607
+ availableAt: availableAt === null ? createdAt : timestamp(availableAt), createdAt, updatedAt: createdAt,
608
+ completedAt: null, lastError: null, authority: "execution-state-only" };
609
+ runtime.queue.push(item); runtime.revision += 1;
610
+ appendReceipt(runtime, "queued", item.queueId, createdAt, { kind, dedupeKey: key });
611
+ await writeJson(paths.gatewayRuntimePath, runtime);
612
+ return { item, duplicate: false };
613
+ });
614
+ }
615
+
616
+ export async function reconcileGateway({ root = process.cwd(), now = new Date() }) {
617
+ const paths = await pathsFor(root);
618
+ return withLock(paths, async () => {
619
+ const [policy, runtime, channelPolicy, channel, attention, personas, { graph }] = await Promise.all([
620
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
621
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
622
+ loadChannelPolicy(paths.catalog.root, paths.catalog), loadChannelRuntime(paths.catalog.root, paths.catalog),
623
+ loadAttention(paths.catalog.root, paths.catalog), loadPersonaRuntime(paths.catalog.root, paths.catalog),
624
+ loadGraph(paths.catalog.root, paths.catalog)
625
+ ]);
626
+ const current = timestamp(now);
627
+ const findings = personaRuntimeFindings(personas.policy, personas.runtime, graph);
628
+ if (findings.length) throw new Error("persona runtime is unhealthy: " + findings.join(", "));
629
+ const channelFindings = channelRuntimeFindings(channel.runtime, channelPolicy.policy, graph);
630
+ if (channelFindings.length) throw new Error("channel runtime is unhealthy: " + channelFindings.join(", "));
631
+ const attentionIssues = attentionFindings(attention.attention);
632
+ if (attentionIssues.length) throw new Error("attention runtime is unhealthy: " + attentionIssues.join(", "));
633
+ for (const lane of runtime.lanes.filter((item) => item.status === "leased" && new Date(item.expiresAt) <= new Date(current))) {
634
+ const item = runtime.queue.find((entry) => entry.queueId === lane.queueId && entry.status === "leased");
635
+ if (item) { preserve(runtime, "queue", item, "lease-expired", current); item.status = item.attempts >= 3 ? "dead-letter" : "pending"; item.lease = null; item.updatedAt = current; }
636
+ lane.status = "expired"; lane.updatedAt = current;
637
+ }
638
+ for (const outbox of runtime.outbox.filter((item) => item.status === "sending")) {
639
+ preserve(runtime, "outbox", outbox, "ambiguous-send-recovery", current);
640
+ outbox.status = "delivery-unknown"; outbox.updatedAt = current;
641
+ appendReceipt(runtime, "delivery-unknown", outbox.outboxId, current, { reason: "crash-during-send" });
642
+ }
643
+ if (policy.enabled && !policy.killSwitch) {
644
+ for (const goal of policy.goals.filter((item) => item.status === "active" && item.plan)) {
645
+ const step = currentPlanStep(goal);
646
+ if (!step) throw new Error("active goal plan has no current step");
647
+ const runnable = runtime.queue.some((item) => item.goalId === goal.goalId && item.goalStepId === step.stepId
648
+ && ["pending", "leased", "awaiting-delivery"].includes(item.status));
649
+ if (!runnable) {
650
+ const key = planQueueKey(goal.goalId, step.stepId, "recovery", String(goal.plan.revision));
651
+ if (!runtime.queue.some((item) => item.dedupeKey === key)) {
652
+ const queued = newGoalQueue(goal, step, "follow-up", key, current);
653
+ runtime.queue.push(queued);
654
+ appendReceipt(runtime, "queued", queued.queueId, current, { kind: "follow-up", goalStepId: step.stepId });
655
+ }
656
+ }
657
+ }
658
+ for (const goal of policy.goals.filter((item) => item.status === "active" && item.deadline
659
+ && new Date(item.deadline) <= new Date(current))) {
660
+ try { assertActivePersona(personas.policy, personas.runtime, goal.agentId, goal.projectId, goal.groupId); }
661
+ catch { continue; }
662
+ const key = "goal:" + goal.goalId + ":deadline:" + goal.deadline;
663
+ if (!runtime.queue.some((item) => item.dedupeKey === key)) runtime.queue.push({
664
+ queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind: "deadline",
665
+ agentId: goal.agentId, projectId: goal.projectId, groupId: goal.groupId, goalId: goal.goalId,
666
+ goalStepId: currentPlanStep(goal)?.stepId || null, channelEventId: null, priority: PRIORITY.deadline, status: "pending", attempts: 0, lease: null,
667
+ availableAt: current, createdAt: current, updatedAt: current, completedAt: null, lastError: null,
668
+ authority: "execution-state-only"
669
+ });
670
+ }
671
+ for (const event of channel.runtime.events.filter((item) => item.status === "pending")) {
672
+ if (!personas.runtime.personas.some((persona) => persona.personaId === event.agentId && persona.status === "active")) continue;
673
+ const key = "channel:" + event.eventId;
674
+ if (!runtime.queue.some((item) => item.dedupeKey === key)) runtime.queue.push({ queueId: "gateway-queue:" + sha256(key).slice(0, 32),
675
+ dedupeKey: key, kind: "direct-message", agentId: event.agentId, projectId: event.projectId, groupId: event.groupId,
676
+ goalId: null, goalStepId: null, channelEventId: event.eventId, priority: PRIORITY["direct-message"], status: "pending", attempts: 0,
677
+ lease: null, availableAt: current, createdAt: current, updatedAt: current, completedAt: null, lastError: null, authority: "execution-state-only" });
678
+ }
679
+ for (const event of attention.attention.events.filter((item) => item.entityId
680
+ && ((item.kind === "promise" && item.status === "open") || (item.kind === "blocker" && item.status === "resolved")))) {
681
+ try { assertActivePersona(personas.policy, personas.runtime, event.entityId, event.projectId, event.groupId); }
682
+ catch { continue; }
683
+ const kind = event.kind === "promise" ? "promise" : "resolved-blocker";
684
+ const key = "attention:" + event.id + ":" + event.status;
685
+ if (!runtime.queue.some((entry) => entry.dedupeKey === key)) runtime.queue.push({ queueId: "gateway-queue:" + sha256(key).slice(0, 32),
686
+ dedupeKey: key, kind, agentId: event.entityId, projectId: event.projectId, groupId: event.groupId,
687
+ goalId: null, goalStepId: null, channelEventId: null, priority: PRIORITY[kind], status: "pending", attempts: 0, lease: null,
688
+ availableAt: event.dueAt || current, createdAt: current, updatedAt: current, completedAt: null, lastError: null, authority: "execution-state-only" });
689
+ }
690
+ }
691
+ runtime.health.gateway = policy.enabled && !policy.killSwitch ? "running" : "stopped";
692
+ runtime.health.scheduler = "healthy"; runtime.health.queue = "healthy"; runtime.health.lastReconciledAt = current;
693
+ runtime.revision += 1;
694
+ await writeJson(paths.gatewayRuntimePath, runtime);
695
+ return { policy, runtime, recovered: true };
696
+ });
697
+ }
698
+
699
+ export async function claimGatewayWork({ root = process.cwd(), workerId, leaseSeconds = 120, now = new Date() }) {
700
+ const paths = await pathsFor(root);
701
+ return withLock(paths, async () => {
702
+ const [policy, runtime, personas] = await Promise.all([
703
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
704
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
705
+ loadPersonaRuntime(paths.catalog.root, paths.catalog)
706
+ ]);
707
+ if (!policy.enabled || policy.killSwitch) return { item: null, reason: "disabled" };
708
+ const current = timestamp(now); workerId = exactId(workerId, "workerId");
709
+ const seconds = Number(leaseSeconds);
710
+ if (!Number.isInteger(seconds) || seconds < 15 || seconds > 900) throw new Error("leaseSeconds must be 15-900");
711
+ const items = runtime.queue.filter((item) => item.status === "pending" && new Date(item.availableAt) <= new Date(current)
712
+ && !currentLane(runtime, item.agentId));
713
+ items.sort((a, b) => b.priority - a.priority || a.createdAt.localeCompare(b.createdAt) || a.queueId.localeCompare(b.queueId));
714
+ let item = null; let revoked = false;
715
+ for (const candidate of items) {
716
+ try {
717
+ assertActivePersona(personas.policy, personas.runtime, candidate.agentId, candidate.projectId, candidate.groupId);
718
+ } catch {
719
+ preserve(runtime, "queue", candidate, "identity-revoked", current);
720
+ candidate.status = "cancelled"; candidate.completedAt = current; candidate.updatedAt = current;
721
+ appendReceipt(runtime, "identity-revoked", candidate.queueId, current, {});
722
+ runtime.revision += 1; revoked = true;
723
+ continue;
724
+ }
725
+ if (candidate.goalStepId) {
726
+ const goal = policy.goals.find((entry) => entry.goalId === candidate.goalId);
727
+ const step = goal && currentPlanStep(goal);
728
+ if (!goal?.plan || goal.status !== "active" || step?.stepId !== candidate.goalStepId || step.status !== "active") {
729
+ preserve(runtime, "queue", candidate, "plan-step-stale", current);
730
+ candidate.status = "cancelled"; candidate.completedAt = current; candidate.updatedAt = current;
731
+ appendReceipt(runtime, "plan-step-stale", candidate.queueId, current, { goalStepId: candidate.goalStepId });
732
+ runtime.revision += 1; revoked = true;
733
+ continue;
734
+ }
735
+ }
736
+ item = candidate;
737
+ break;
738
+ }
739
+ if (!item && revoked) await writeJson(paths.gatewayRuntimePath, runtime);
740
+ if (!item) return { item: null, reason: runtime.queue.some((entry) => entry.status === "pending") ? "waiting" : "idle/needs-goal" };
741
+ preserve(runtime, "queue", item, "leased", current);
742
+ item.status = "leased"; item.attempts += 1; item.updatedAt = current;
743
+ item.lease = { workerId, claimedAt: current, expiresAt: new Date(new Date(current).getTime() + seconds * 1000).toISOString() };
744
+ runtime.lanes = runtime.lanes.filter((lane) => lane.agentId !== item.agentId || lane.status !== "leased");
745
+ runtime.lanes.push({ agentId: item.agentId, queueId: item.queueId, workerId, status: "leased", claimedAt: current,
746
+ expiresAt: item.lease.expiresAt, updatedAt: current, authority: "execution-state-only" });
747
+ runtime.health.worker = "healthy"; runtime.health.lastTickAt = current; runtime.revision += 1;
748
+ const receipt = appendReceipt(runtime, "leased", item.queueId, current, { workerId, attempt: item.attempts });
749
+ await writeJson(paths.gatewayRuntimePath, runtime);
750
+ return { item: structuredClone(item), receipt };
751
+ });
752
+ }
753
+
754
+ function exactReplyBinding(channelPolicy, event) {
755
+ const binding = channelPolicy.bindings.find((item) => item.id === event.bindingId && item.status === "active"
756
+ && item.agentId === event.agentId && item.projectId === event.projectId && item.groupId === event.groupId
757
+ && item.provider === event.provider && item.tenantId === event.tenantId && item.accountId === event.accountId
758
+ && item.chatId === event.chatId && item.threadId === event.threadId && item.capabilities.includes("reply"));
759
+ if (!binding) throw new Error("current exact channel reply capability is unavailable");
760
+ return binding;
761
+ }
762
+
763
+ export async function completeGatewayRun({ root = process.cwd(), queueId, workerId, result, now = new Date() }) {
764
+ const paths = await pathsFor(root);
765
+ return withLock(paths, async () => {
766
+ const [policy, runtime, channelPolicy, channelRuntime, personas] = await Promise.all([
767
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
768
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
769
+ loadChannelPolicy(paths.catalog.root, paths.catalog), loadChannelRuntime(paths.catalog.root, paths.catalog),
770
+ loadPersonaRuntime(paths.catalog.root, paths.catalog)
771
+ ]);
772
+ if (!policy.enabled || policy.killSwitch) throw new Error("gateway was disabled before run completion");
773
+ const item = runtime.queue.find((entry) => entry.queueId === exactId(queueId, "queueId"));
774
+ workerId = exactId(workerId, "workerId");
775
+ if (!item || item.status !== "leased" || item.lease?.workerId !== workerId) throw new Error("run completion requires the exact active queue lease");
776
+ assertActivePersona(personas.policy, personas.runtime, item.agentId, item.projectId, item.groupId);
777
+ const current = timestamp(now);
778
+ const lane = runtime.lanes.find((entry) => entry.queueId === item.queueId && entry.workerId === workerId && entry.status === "leased");
779
+ if (!lane) throw new Error("agent lane lease is missing");
780
+ const boundGoal = item.goalId ? policy.goals.find((entry) => entry.goalId === item.goalId) : null;
781
+ if (item.goalStepId) {
782
+ const step = boundGoal && currentPlanStep(boundGoal);
783
+ if (!boundGoal?.plan || boundGoal.status !== "active" || step?.stepId !== item.goalStepId || step.status !== "active") {
784
+ throw new Error("run completion is not bound to the current active goal step");
785
+ }
786
+ }
787
+ const knowledgeGapRequest = result?.knowledgeGap === undefined || result?.knowledgeGap === null
788
+ ? null : result.knowledgeGap;
789
+ if (knowledgeGapRequest && (item.channelEventId || !item.goalStepId || !boundGoal?.plan)) {
790
+ throw new Error("knowledge gaps require an exact goal-plan step without a channel obligation");
791
+ }
792
+ if (knowledgeGapRequest && (result?.blocked || result?.completed)) {
793
+ throw new Error("knowledge gap result cannot also complete or generically block a step");
794
+ }
795
+ const text = result?.text ? safeText(result.text, "result.text", 16000) : null;
796
+ let clarification = null;
797
+ preserve(runtime, "queue", item, "run-completed", current);
798
+ if (item.channelEventId) {
799
+ if (!text) throw new Error("a channel obligation requires a non-empty response");
800
+ const voice = evaluateVoiceOutput(text);
801
+ if (!voice.ok) throw new Error("channel response contains a prohibited attachment or consciousness claim");
802
+ const event = channelRuntime.runtime.events.find((entry) => entry.eventId === item.channelEventId);
803
+ if (!event) throw new Error("channel event disappeared before delivery preparation");
804
+ const binding = exactReplyBinding(channelPolicy.policy, event);
805
+ const idempotencyKey = "delivery:" + sha256([event.eventId, binding.id, event.chatId, event.threadId || "", event.replyTo || ""].join("\0")).slice(0, 32);
806
+ let outbox = runtime.outbox.find((entry) => entry.idempotencyKey === idempotencyKey);
807
+ if (!outbox) {
808
+ outbox = { outboxId: "gateway-outbox:" + sha256(idempotencyKey).slice(0, 32), queueId: item.queueId,
809
+ idempotencyKey, bindingId: binding.id, eventId: event.eventId, provider: event.provider, tenantId: event.tenantId,
810
+ accountId: event.accountId, chatId: event.chatId, threadId: event.threadId, replyTo: event.replyTo,
811
+ text, status: "prepared", attempts: 0, nextAttemptAt: current, createdAt: current, updatedAt: current,
812
+ deliveredAt: null, adapterReceipt: null, lastError: null, authority: "delivery-state-only" };
813
+ runtime.outbox.push(outbox);
814
+ }
815
+ item.status = "awaiting-delivery";
816
+ } else {
817
+ item.status = result?.blocked || knowledgeGapRequest ? "blocked" : "completed"; item.completedAt = current;
818
+ const goal = boundGoal;
819
+ if (goal) {
820
+ policy.history.push({ kind: "goal", at: current, value: structuredClone(goal), authority: "authenticated-goal-policy" });
821
+ const checkpoint = result?.checkpoint === undefined ? goal.checkpoint : safeCheckpoint(result.checkpoint);
822
+ goal.checkpoint = checkpoint; goal.heartbeatAt = current;
823
+ goal.blocker = result?.blocked ? safeText(result.blocker || "Run blocked.", "blocker", 500) : null;
824
+ if (goal.plan && item.goalStepId) {
825
+ const step = currentPlanStep(goal);
826
+ step.checkpoint = checkpoint; step.updatedAt = current;
827
+ if (knowledgeGapRequest) {
828
+ if (!Array.isArray(step.knowledgeGaps)) step.knowledgeGaps = [];
829
+ const proposed = createKnowledgeGap(goal, step, item.queueId, knowledgeGapRequest, current);
830
+ const existing = step.knowledgeGaps.find((gap) => gap.gapId === proposed.gapId);
831
+ if (!existing && step.knowledgeGaps.length >= 16) throw new Error("goal step exceeds 16 knowledge gaps");
832
+ if (existing?.status === "resolved") {
833
+ goal.blocker = "The host repeated an already resolved knowledge gap.";
834
+ step.status = "blocked"; step.blocker = goal.blocker; goal.status = "blocked";
835
+ appendReceipt(runtime, "knowledge-gap-regression", item.queueId, current, {
836
+ goalStepId: step.stepId, gapId: existing.gapId
837
+ });
838
+ } else {
839
+ const gap = existing || proposed;
840
+ if (!existing) step.knowledgeGaps.push(gap);
841
+ goal.blocker = gap.question; step.status = "blocked"; step.blocker = gap.question; goal.status = "blocked";
842
+ clarification = structuredClone(gap);
843
+ appendReceipt(runtime, "knowledge-gap-opened", item.queueId, current, {
844
+ goalStepId: step.stepId, gapId: gap.gapId, requiredEvidence: gap.requiredEvidence
845
+ });
846
+ }
847
+ goal.plan.revision += 1;
848
+ } else if (result?.blocked) {
849
+ step.status = "blocked"; step.blocker = goal.blocker; goal.status = "blocked";
850
+ } else if (result?.completed) {
851
+ step.status = "completed"; step.completedAt = current; step.completedByQueueId = item.queueId; step.blocker = null;
852
+ goal.plan.currentStepId = null; goal.plan.revision += 1;
853
+ const next = activateNextPlanStep(goal.plan, current);
854
+ if (next) {
855
+ goal.status = "active"; goal.nextSafeStep = next.title; goal.blocker = null;
856
+ const key = planQueueKey(goal.goalId, next.stepId, "ready", String(goal.plan.revision));
857
+ if (!runtime.queue.some((entry) => entry.dedupeKey === key)) {
858
+ const queued = newGoalQueue(goal, next, "follow-up", key, current);
859
+ runtime.queue.push(queued);
860
+ appendReceipt(runtime, "goal-step-ready", queued.queueId, current, { goalStepId: next.stepId });
861
+ }
862
+ } else {
863
+ goal.status = "completed";
864
+ }
865
+ } else {
866
+ goal.status = "active"; step.blocker = null;
867
+ }
868
+ } else if (!goal.plan) {
869
+ goal.status = result?.completed ? "completed" : result?.blocked ? "blocked" : "active";
870
+ }
871
+ goal.updatedAt = current;
872
+ policy.revision += 1;
873
+ if (goal.status === "active" && (!goal.plan || !result?.completed)) {
874
+ const checkpointDigest = sha256(JSON.stringify(goal.checkpoint || { heartbeatAt: current })).slice(0, 20);
875
+ const step = currentPlanStep(goal);
876
+ const key = step ? planQueueKey(goal.goalId, step.stepId, "follow-up", checkpointDigest)
877
+ : "goal:" + goal.goalId + ":follow-up:" + checkpointDigest;
878
+ if (!runtime.queue.some((entry) => entry.dedupeKey === key)) runtime.queue.push({
879
+ queueId: "gateway-queue:" + sha256(key).slice(0, 32), dedupeKey: key, kind: "follow-up",
880
+ agentId: goal.agentId, projectId: goal.projectId, groupId: goal.groupId, goalId: goal.goalId,
881
+ goalStepId: step?.stepId || null, channelEventId: null, priority: PRIORITY["follow-up"], status: "pending", attempts: 0, lease: null,
882
+ availableAt: new Date(new Date(current).getTime() + 60000).toISOString(), createdAt: current, updatedAt: current,
883
+ completedAt: null, lastError: null, authority: "execution-state-only"
884
+ });
885
+ }
886
+ }
887
+ }
888
+ item.lease = null; item.updatedAt = current; lane.status = "completed"; lane.updatedAt = current;
889
+ runtime.health.host = "healthy"; runtime.health.worker = "healthy"; runtime.health.lastTickAt = current;
890
+ appendReceipt(runtime, "run-terminal", item.queueId, current, { status: item.status, goalStepId: item.goalStepId || null }); runtime.revision += 1;
891
+ await Promise.all([writeJson(paths.gatewayPolicyPath, policy), writeJson(paths.gatewayRuntimePath, runtime)]);
892
+ return { item, outbox: runtime.outbox.find((entry) => entry.queueId === item.queueId) || null, clarification };
893
+ });
894
+ }
895
+
896
+ export async function failGatewayRun({ root = process.cwd(), queueId, workerId, error, retryAfterMs = 5000,
897
+ now = new Date() }) {
898
+ const paths = await pathsFor(root);
899
+ return withLock(paths, async () => {
900
+ const [policy, runtime] = await Promise.all([
901
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
902
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime)
903
+ ]);
904
+ const item = runtime.queue.find((entry) => entry.queueId === exactId(queueId, "queueId"));
905
+ workerId = exactId(workerId, "workerId");
906
+ if (!item || item.status !== "leased" || item.lease?.workerId !== workerId) {
907
+ throw new Error("run failure requires the exact active queue lease");
908
+ }
909
+ const lane = runtime.lanes.find((entry) => entry.queueId === item.queueId && entry.workerId === workerId
910
+ && entry.status === "leased");
911
+ if (!lane) throw new Error("agent lane lease is missing");
912
+ const current = timestamp(now);
913
+ const message = safeText(String(error || "host runtime unavailable"), "runError", 500);
914
+ const delay = Number(retryAfterMs);
915
+ if (!Number.isFinite(delay) || delay < 250 || delay > 300000) throw new Error("retryAfterMs must be 250-300000");
916
+ preserve(runtime, "queue", item, "run-failed", current);
917
+ item.status = item.attempts >= 3 ? "dead-letter" : "pending";
918
+ item.lease = null; item.lastError = message; item.updatedAt = current;
919
+ item.availableAt = new Date(new Date(current).getTime() + delay).toISOString();
920
+ if (item.status === "dead-letter") item.completedAt = current;
921
+ lane.status = "completed"; lane.updatedAt = current;
922
+ runtime.health.host = "failed"; runtime.health.worker = "degraded"; runtime.health.lastTickAt = current;
923
+ runtime.revision += 1;
924
+ const receipt = appendReceipt(runtime, item.status === "dead-letter" ? "run-dead-letter" : "run-retry",
925
+ item.queueId, current, { attempt: item.attempts });
926
+ await writeJson(paths.gatewayRuntimePath, runtime);
927
+ return { item, receipt, policyEnabled: policy.enabled && !policy.killSwitch };
928
+ });
929
+ }
930
+
931
+ export async function deliverPrepared({ root = process.cwd(), outboxId, adapter, now = new Date() }) {
932
+ if (!adapter || typeof adapter.send !== "function") throw new Error("delivery adapter is unavailable");
933
+ const paths = await pathsFor(root);
934
+ let prepared;
935
+ await withLock(paths, async () => {
936
+ const [policy, runtime, channelPolicy, channelRuntime, personas] = await Promise.all([
937
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
938
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime),
939
+ loadChannelPolicy(paths.catalog.root, paths.catalog), loadChannelRuntime(paths.catalog.root, paths.catalog),
940
+ loadPersonaRuntime(paths.catalog.root, paths.catalog)
941
+ ]);
942
+ if (!policy.enabled || policy.killSwitch) throw new Error("gateway was disabled before delivery");
943
+ const outbox = runtime.outbox.find((item) => item.outboxId === exactId(outboxId, "outboxId"));
944
+ if (!outbox) throw new Error("unknown outbox item");
945
+ if (["delivered", "acknowledged"].includes(outbox.status)) { prepared = { duplicate: true, outbox: structuredClone(outbox) }; return; }
946
+ if (outbox.status !== "prepared" && outbox.status !== "failed") throw new Error("outbox item is not safely retryable");
947
+ if (new Date(outbox.nextAttemptAt) > new Date(timestamp(now))) throw new Error("outbox retry is not due");
948
+ const event = channelRuntime.runtime.events.find((item) => item.eventId === outbox.eventId);
949
+ if (!event) throw new Error("outbox channel event is missing");
950
+ try {
951
+ assertActivePersona(personas.policy, personas.runtime, event.agentId, event.projectId, event.groupId);
952
+ exactReplyBinding(channelPolicy.policy, event);
953
+ }
954
+ catch (error) {
955
+ const current = timestamp(now);
956
+ preserve(runtime, "outbox", outbox, "capability-revoked", current);
957
+ outbox.status = "dead-letter"; outbox.lastError = safeText(error.message, "adapterError", 500);
958
+ outbox.updatedAt = current;
959
+ const queue = runtime.queue.find((item) => item.queueId === outbox.queueId);
960
+ if (queue) { queue.status = "dead-letter"; queue.completedAt = current; queue.updatedAt = current; }
961
+ runtime.health.adapter = "failed"; runtime.revision += 1;
962
+ const receipt = appendReceipt(runtime, "dead-letter", outbox.outboxId, current, { reason: "identity-or-capability-revoked" });
963
+ await writeJson(paths.gatewayRuntimePath, runtime);
964
+ prepared = { duplicate: false, terminal: true, outbox: structuredClone(outbox), receipt };
965
+ return;
966
+ }
967
+ preserve(runtime, "outbox", outbox, "sending", timestamp(now));
968
+ outbox.status = "sending"; outbox.attempts += 1; outbox.updatedAt = timestamp(now); runtime.revision += 1;
969
+ appendReceipt(runtime, "sending", outbox.outboxId, outbox.updatedAt, { attempt: outbox.attempts });
970
+ await writeJson(paths.gatewayRuntimePath, runtime);
971
+ prepared = { duplicate: false, outbox: structuredClone(outbox) };
972
+ });
973
+ if (prepared.duplicate || prepared.terminal) return prepared;
974
+ let outcome;
975
+ try { outcome = await adapter.send(structuredClone(prepared.outbox)); }
976
+ catch (error) { outcome = { ok: false, effect: "unknown", error: error.message }; }
977
+ return withLock(paths, async () => {
978
+ const runtime = await readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime);
979
+ const outbox = runtime.outbox.find((item) => item.outboxId === prepared.outbox.outboxId);
980
+ if (!outbox || outbox.status !== "sending") throw new Error("outbox sending state changed unexpectedly");
981
+ const current = timestamp(now);
982
+ preserve(runtime, "outbox", outbox, outcome?.ok ? "delivered" : "send-failed", current);
983
+ if (outcome?.ok) {
984
+ outbox.status = "delivered"; outbox.deliveredAt = current; outbox.adapterReceipt = safeText(String(outcome.receipt || "delivered"), "adapterReceipt", 500);
985
+ runtime.health.adapter = "healthy";
986
+ const queue = runtime.queue.find((item) => item.queueId === outbox.queueId);
987
+ if (queue) { queue.status = "completed"; queue.completedAt = current; queue.updatedAt = current; }
988
+ } else if (outcome?.effect === "none") {
989
+ outbox.status = outbox.attempts < 3 ? "failed" : "dead-letter";
990
+ outbox.lastError = safeText(String(outcome.error || "adapter failure"), "adapterError", 500);
991
+ runtime.health.adapter = outbox.status === "failed" ? "degraded" : "failed";
992
+ if (outbox.status === "failed") {
993
+ const delay = Math.min(300000, Number(outcome.retryAfterMs) || 1000 * (2 ** outbox.attempts));
994
+ outbox.nextAttemptAt = new Date(new Date(current).getTime() + delay).toISOString();
995
+ } else {
996
+ const queue = runtime.queue.find((item) => item.queueId === outbox.queueId);
997
+ if (queue) { queue.status = "dead-letter"; queue.completedAt = current; queue.updatedAt = current; }
998
+ }
999
+ } else {
1000
+ outbox.status = "delivery-unknown"; outbox.lastError = safeText(String(outcome?.error || "delivery outcome is ambiguous"), "adapterError", 500);
1001
+ runtime.health.adapter = "failed";
1002
+ }
1003
+ outbox.updatedAt = current; runtime.revision += 1;
1004
+ const receipt = appendReceipt(runtime, outbox.status, outbox.outboxId, current, { adapterReceipt: outbox.adapterReceipt });
1005
+ await writeJson(paths.gatewayRuntimePath, runtime);
1006
+ return { outbox, receipt, duplicate: false };
1007
+ });
1008
+ }
1009
+
1010
+ export async function updateGatewayHealth({ root = process.cwd(), worker = null, adapter = null, host = null,
1011
+ now = new Date() } = {}) {
1012
+ const paths = await pathsFor(root);
1013
+ return withLock(paths, async () => {
1014
+ const runtime = await readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime);
1015
+ for (const [key, value] of Object.entries({ worker, adapter, host })) {
1016
+ if (value !== null) {
1017
+ if (!HEALTH_VALUES.has(value)) throw new Error("unsupported gateway health value");
1018
+ runtime.health[key] = value;
1019
+ }
1020
+ }
1021
+ runtime.health.lastTickAt = timestamp(now); runtime.revision += 1;
1022
+ await writeJson(paths.gatewayRuntimePath, runtime);
1023
+ return structuredClone(runtime.health);
1024
+ });
1025
+ }
1026
+
1027
+ export async function loadGatewayRuntime(root = process.cwd(), catalog = null) {
1028
+ const paths = await pathsFor(root, catalog);
1029
+ const [policy, runtime] = await Promise.all([
1030
+ readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy),
1031
+ readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime)
1032
+ ]);
1033
+ return { policy, runtime, ...paths };
1034
+ }
1035
+
1036
+ export async function inspectGatewayRuntime(root = process.cwd(), catalog = null) {
1037
+ const paths = await pathsFor(root, catalog); const errors = [];
1038
+ let policy = emptyPolicy(paths.catalog.root); let runtime = emptyRuntime(paths.catalog.root);
1039
+ try { policy = await readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy); } catch (error) { errors.push("policy:" + error.message); }
1040
+ try { runtime = await readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime); } catch (error) { errors.push("runtime:" + error.message); }
1041
+ return { policy, runtime, errors, ...paths };
1042
+ }
1043
+
1044
+ export function gatewayRuntimeFindings(policy, runtime) {
1045
+ const findings = [];
1046
+ const active = new Set();
1047
+ const goalIds = new Set();
1048
+ for (const goal of policy.goals) {
1049
+ if (!validGoal(goal)) findings.push("invalid-goal:" + (goal?.goalId || "unknown"));
1050
+ if (goalIds.has(goal.goalId)) findings.push("duplicate-goal:" + goal.goalId);
1051
+ goalIds.add(goal.goalId);
1052
+ if (goal.status === "active" && active.has(goal.agentId)) findings.push("multiple-active-goals:" + goal.agentId);
1053
+ if (goal.status === "active") active.add(goal.agentId);
1054
+ }
1055
+ for (const item of policy.history) if (!validPolicyHistory(item)) findings.push("invalid-gateway-policy-history");
1056
+ const queueIds = new Set(); const dedupeKeys = new Set();
1057
+ for (const item of runtime.queue) {
1058
+ if (!validQueue(item)) findings.push("invalid-queue-item:" + (item?.queueId || "unknown"));
1059
+ if (queueIds.has(item.queueId)) findings.push("duplicate-queue-item:" + item.queueId);
1060
+ if (dedupeKeys.has(item.dedupeKey)) findings.push("duplicate-queue-dedupe:" + item.dedupeKey);
1061
+ queueIds.add(item.queueId); dedupeKeys.add(item.dedupeKey);
1062
+ if (item.goalStepId) {
1063
+ const goal = policy.goals.find((entry) => entry.goalId === item.goalId);
1064
+ if (!goal?.plan?.steps.some((step) => step.stepId === item.goalStepId)) findings.push("orphan-goal-step:" + item.queueId);
1065
+ }
1066
+ }
1067
+ const outboxIds = new Set(); const idempotencyKeys = new Set();
1068
+ for (const item of runtime.outbox) {
1069
+ if (!validOutbox(item)) findings.push("invalid-outbox-item:" + (item?.outboxId || "unknown"));
1070
+ if (!queueIds.has(item.queueId)) findings.push("orphan-outbox-item:" + item.outboxId);
1071
+ if (outboxIds.has(item.outboxId)) findings.push("duplicate-outbox-item:" + item.outboxId);
1072
+ if (idempotencyKeys.has(item.idempotencyKey)) findings.push("duplicate-outbox-idempotency:" + item.idempotencyKey);
1073
+ outboxIds.add(item.outboxId); idempotencyKeys.add(item.idempotencyKey);
1074
+ }
1075
+ for (const item of runtime.receipts) if (!validReceipt(item)
1076
+ || (!queueIds.has(item.objectId) && !outboxIds.has(item.objectId))) findings.push("invalid-gateway-receipt:" + (item?.id || "unknown"));
1077
+ for (const item of runtime.history) if (!validHistory(item)) findings.push("invalid-gateway-history:" + (item?.objectId || "unknown"));
1078
+ const leased = runtime.lanes.filter((item) => item.status === "leased");
1079
+ for (const lane of runtime.lanes) {
1080
+ if (!validLane(lane)) findings.push("invalid-agent-lane:" + (lane?.queueId || "unknown"));
1081
+ const queue = runtime.queue.find((item) => item.queueId === lane.queueId);
1082
+ if (!queue || (lane.status === "leased" && (queue.status !== "leased" || queue.lease?.workerId !== lane.workerId))) {
1083
+ findings.push("agent-lane-queue-mismatch:" + (lane.queueId || "unknown"));
1084
+ }
1085
+ }
1086
+ if (new Set(leased.map((item) => item.agentId)).size !== leased.length) findings.push("duplicate-agent-lane");
1087
+ if (!validHealth(runtime.health)) findings.push("invalid-gateway-health");
1088
+ return findings;
1089
+ }
1090
+
1091
+ export function gatewayHealthFindings(policy, runtime, { now = new Date(), staleAfterMs = 180000 } = {}) {
1092
+ if (!policy.enabled || policy.killSwitch) return [];
1093
+ const findings = [];
1094
+ if (runtime.health.gateway !== "running") findings.push("gateway-not-running");
1095
+ if (runtime.health.scheduler !== "healthy") findings.push("scheduler-not-healthy");
1096
+ if (runtime.health.queue !== "healthy") findings.push("queue-not-healthy");
1097
+ if (!new Set(["healthy", "degraded"]).has(runtime.health.worker)) findings.push("worker-not-healthy");
1098
+ if (!new Set(["healthy", "degraded"]).has(runtime.health.adapter)) findings.push("adapter-not-healthy");
1099
+ const current = now instanceof Date ? now : new Date(now);
1100
+ const tick = runtime.health.lastTickAt === null ? null : new Date(runtime.health.lastTickAt);
1101
+ const reconciliation = runtime.health.lastReconciledAt === null ? null : new Date(runtime.health.lastReconciledAt);
1102
+ if (!tick || current.getTime() - tick.getTime() > staleAfterMs) findings.push("worker-heartbeat-stale");
1103
+ if (!reconciliation || current.getTime() - reconciliation.getTime() > staleAfterMs) findings.push("scheduler-heartbeat-stale");
1104
+ return findings;
1105
+ }
1106
+
1107
+ export async function gatewayContext({ root = process.cwd(), agentId = null } = {}) {
1108
+ const { policy, runtime } = await loadGatewayRuntime(root);
1109
+ const findings = gatewayRuntimeFindings(policy, runtime);
1110
+ if (findings.length) throw new Error("gateway runtime failed closed: " + findings.join(", "));
1111
+ const goals = policy.goals.filter((item) => agentId === null || item.agentId === exactId(agentId, "agentId"));
1112
+ const queue = runtime.queue.filter((item) => agentId === null || item.agentId === exactId(agentId, "agentId"));
1113
+ const queueIds = new Set(queue.map((item) => item.queueId));
1114
+ return { schema: "agentspine.gateway-context/v1", enabled: policy.enabled, killSwitch: policy.killSwitch,
1115
+ goals: structuredClone(goals), queue: structuredClone(queue),
1116
+ outbox: structuredClone(runtime.outbox.filter((item) => queueIds.has(item.queueId))),
1117
+ health: structuredClone(runtime.health), healthFindings: gatewayHealthFindings(policy, runtime),
1118
+ authority: "execution-state-only" };
1119
+ }