blun-king-cli 9.1.486 → 9.1.489

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 (99) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LIESMICH.txt +28 -7
  3. package/README.md +16 -3
  4. package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -0
  5. package/agent-spine-plugin/.claude-plugin/plugin.json +14 -0
  6. package/agent-spine-plugin/.codex-plugin/plugin.json +34 -0
  7. package/agent-spine-plugin/.mcp.json +8 -0
  8. package/agent-spine-plugin/CHANGELOG.md +277 -0
  9. package/agent-spine-plugin/LICENSE +186 -0
  10. package/agent-spine-plugin/README.md +342 -0
  11. package/agent-spine-plugin/assets/agentspine-banner.svg +32 -0
  12. package/agent-spine-plugin/bin/agentspine-mcp.js +4 -0
  13. package/agent-spine-plugin/bin/agentspine.js +7 -0
  14. package/agent-spine-plugin/blun.plugin.json +33 -0
  15. package/agent-spine-plugin/docs/acceptance.md +61 -0
  16. package/agent-spine-plugin/docs/architecture.md +183 -0
  17. package/agent-spine-plugin/docs/attention.md +121 -0
  18. package/agent-spine-plugin/docs/automatic-continuity.md +79 -0
  19. package/agent-spine-plugin/docs/channel-runtime.md +92 -0
  20. package/agent-spine-plugin/docs/coordination.md +138 -0
  21. package/agent-spine-plugin/docs/feed-transport.md +99 -0
  22. package/agent-spine-plugin/docs/gateway-runtime.md +109 -0
  23. package/agent-spine-plugin/docs/harness-reference.md +45 -0
  24. package/agent-spine-plugin/docs/host-integration.md +129 -0
  25. package/agent-spine-plugin/docs/https-transport.md +116 -0
  26. package/agent-spine-plugin/docs/learning.md +98 -0
  27. package/agent-spine-plugin/docs/object-transport.md +93 -0
  28. package/agent-spine-plugin/docs/peer-transport.md +88 -0
  29. package/agent-spine-plugin/docs/preservation-contract.md +53 -0
  30. package/agent-spine-plugin/docs/quality-gates.md +50 -0
  31. package/agent-spine-plugin/docs/relationships.md +67 -0
  32. package/agent-spine-plugin/docs/releasing.md +83 -0
  33. package/agent-spine-plugin/docs/roadmap.md +307 -0
  34. package/agent-spine-plugin/docs/selfstarter.md +88 -0
  35. package/agent-spine-plugin/docs/session-briefing.md +74 -0
  36. package/agent-spine-plugin/docs/shared-memory.md +259 -0
  37. package/agent-spine-plugin/docs/source-roots.md +86 -0
  38. package/agent-spine-plugin/docs/sqlite-transport.md +76 -0
  39. package/agent-spine-plugin/hooks/hooks.json +95 -0
  40. package/agent-spine-plugin/hooks/version.json +5 -0
  41. package/agent-spine-plugin/package.json +69 -0
  42. package/agent-spine-plugin/scripts/check-hosts.js +193 -0
  43. package/agent-spine-plugin/scripts/check-install.js +551 -0
  44. package/agent-spine-plugin/scripts/check-syntax.js +29 -0
  45. package/agent-spine-plugin/scripts/github-actions.js +11 -0
  46. package/agent-spine-plugin/scripts/release-check.js +127 -0
  47. package/agent-spine-plugin/scripts/run-acceptance.js +19 -0
  48. package/agent-spine-plugin/scripts/run-checks.js +46 -0
  49. package/agent-spine-plugin/scripts/run-tests-hermetic.js +73 -0
  50. package/agent-spine-plugin/skill/SKILL.md +76 -0
  51. package/agent-spine-plugin/skills/agent-spine/SKILL.md +84 -0
  52. package/agent-spine-plugin/spine-example/1-identity.md +12 -0
  53. package/agent-spine-plugin/spine-example/2-voice.md +6 -0
  54. package/agent-spine-plugin/spine-example/3-conduct.md +8 -0
  55. package/agent-spine-plugin/spine-example/4-history.md +4 -0
  56. package/agent-spine-plugin/src/cli.js +950 -0
  57. package/agent-spine-plugin/src/hook.js +646 -0
  58. package/agent-spine-plugin/src/index.js +84 -0
  59. package/agent-spine-plugin/src/lib/acceptance.js +294 -0
  60. package/agent-spine-plugin/src/lib/attention.js +755 -0
  61. package/agent-spine-plugin/src/lib/audit.js +308 -0
  62. package/agent-spine-plugin/src/lib/authentication.js +513 -0
  63. package/agent-spine-plugin/src/lib/briefing.js +305 -0
  64. package/agent-spine-plugin/src/lib/catalog.js +142 -0
  65. package/agent-spine-plugin/src/lib/channel-runtime.js +665 -0
  66. package/agent-spine-plugin/src/lib/context.js +154 -0
  67. package/agent-spine-plugin/src/lib/continuity.js +333 -0
  68. package/agent-spine-plugin/src/lib/coordination.js +577 -0
  69. package/agent-spine-plugin/src/lib/documents.js +199 -0
  70. package/agent-spine-plugin/src/lib/feed-transport.js +497 -0
  71. package/agent-spine-plugin/src/lib/filesystem-retry.js +27 -0
  72. package/agent-spine-plugin/src/lib/gateway-runtime.js +760 -0
  73. package/agent-spine-plugin/src/lib/graph.js +284 -0
  74. package/agent-spine-plugin/src/lib/https-transport.js +392 -0
  75. package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -0
  76. package/agent-spine-plugin/src/lib/indexed-memory.js +281 -0
  77. package/agent-spine-plugin/src/lib/learning.js +645 -0
  78. package/agent-spine-plugin/src/lib/object-transport.js +206 -0
  79. package/agent-spine-plugin/src/lib/paths.js +60 -0
  80. package/agent-spine-plugin/src/lib/peer-transport.js +283 -0
  81. package/agent-spine-plugin/src/lib/persona-runtime.js +503 -0
  82. package/agent-spine-plugin/src/lib/runtime.js +13 -0
  83. package/agent-spine-plugin/src/lib/selfstarter.js +819 -0
  84. package/agent-spine-plugin/src/lib/sharing.js +969 -0
  85. package/agent-spine-plugin/src/lib/source-roots.js +451 -0
  86. package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -0
  87. package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -0
  88. package/agent-spine-plugin/src/lib/voice-runtime.js +39 -0
  89. package/agent-spine-plugin/src/mcp.js +526 -0
  90. package/agent-spine-plugin/src/version.js +1 -0
  91. package/agent-spine-plugin/src/worker.js +178 -0
  92. package/bin/identity-context-policy.cjs +51 -1
  93. package/bin/launcher-runtime.js +42 -1
  94. package/bin/plugin-bootstrap.js +68 -27
  95. package/bin/repeated-user-message-projection.cjs +6 -0
  96. package/bin/turn-thinking-policy.cjs +5 -4
  97. package/bin/turn-tool-performance-policy.cjs +16 -2
  98. package/blun.mjs +187 -35
  99. package/package.json +3 -1
@@ -0,0 +1,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
+
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
+ }