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.
- package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
- package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
- package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
- package/agent-spine-plugin/.mcp.json +8 -8
- package/agent-spine-plugin/CONTRIBUTING.md +52 -0
- package/agent-spine-plugin/LICENSE +186 -186
- package/agent-spine-plugin/README.md +394 -0
- package/agent-spine-plugin/SECURITY.md +47 -0
- package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
- package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
- package/agent-spine-plugin/bin/agentspine.js +7 -7
- package/agent-spine-plugin/blun.plugin.json +33 -33
- package/agent-spine-plugin/hooks/codex.json +47 -47
- package/agent-spine-plugin/hooks/hooks.json +106 -106
- package/agent-spine-plugin/hooks/version.json +5 -5
- package/agent-spine-plugin/package.json +69 -69
- package/agent-spine-plugin/scripts/check-hosts.js +199 -199
- package/agent-spine-plugin/skill/SKILL.md +76 -76
- package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
- package/agent-spine-plugin/src/cli.js +1488 -1442
- package/agent-spine-plugin/src/hook.js +886 -812
- package/agent-spine-plugin/src/index.js +93 -93
- package/agent-spine-plugin/src/lib/acceptance.js +333 -333
- package/agent-spine-plugin/src/lib/attention.js +755 -755
- package/agent-spine-plugin/src/lib/audit.js +351 -342
- package/agent-spine-plugin/src/lib/authentication.js +515 -515
- package/agent-spine-plugin/src/lib/briefing.js +317 -317
- package/agent-spine-plugin/src/lib/catalog.js +167 -167
- package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
- package/agent-spine-plugin/src/lib/context.js +154 -154
- package/agent-spine-plugin/src/lib/continuity.js +338 -338
- package/agent-spine-plugin/src/lib/coordination.js +577 -577
- package/agent-spine-plugin/src/lib/documents.js +217 -217
- package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
- package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
- package/agent-spine-plugin/src/lib/gateway-runtime.js +1119 -760
- package/agent-spine-plugin/src/lib/graph.js +337 -337
- package/agent-spine-plugin/src/lib/hook-audit.js +29 -0
- package/agent-spine-plugin/src/lib/https-transport.js +392 -392
- package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
- package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
- package/agent-spine-plugin/src/lib/learning.js +6923 -6466
- package/agent-spine-plugin/src/lib/object-transport.js +206 -206
- package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
- package/agent-spine-plugin/src/lib/paths.js +109 -109
- package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
- package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
- package/agent-spine-plugin/src/lib/preflight.js +702 -702
- package/agent-spine-plugin/src/lib/runtime.js +13 -13
- package/agent-spine-plugin/src/lib/selfstarter.js +892 -819
- package/agent-spine-plugin/src/lib/sharing.js +969 -969
- package/agent-spine-plugin/src/lib/source-roots.js +529 -482
- package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
- package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
- package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
- package/agent-spine-plugin/src/mcp.js +597 -572
- package/agent-spine-plugin/src/version.js +1 -1
- package/agent-spine-plugin/src/worker.js +202 -195
- package/bin/active-steer-priority-policy.cjs +24 -0
- package/bin/launcher-runtime.js +8 -1
- package/bin/mnemo-tool-agent-policy.cjs +22 -0
- package/bin/thinking-activity-status-policy.cjs +132 -0
- package/bin/thinking-only-guard.cjs +75 -0
- package/bin/tool-call-loop-policy.cjs +53 -0
- package/bin/turn-thinking-policy.cjs +25 -1
- package/blun.mjs +554 -64
- package/package.json +4 -1
- package/standard-skills/translate-native/LICENSE +21 -21
- package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
- package/standard-skills/translate-native/references/native-orthography.md +79 -79
- package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
- package/standard-skills/translate-native/references/structured-content.md +72 -72
- package/standard-skills/translate-native/references/translationese-review.md +77 -77
- package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
- package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
- package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
- package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
- package/standard-skills/translate-native/scripts/language_quality.py +377 -377
- package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
- package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
- package/standard-tools/language-guard/blun_language_guard.py +697 -697
- package/standard-tools/language-guard/check_diacritics.py +353 -353
- package/standard-tools/language-guard/guard_service_client.py +264 -264
- package/standard-tools/language-guard/language_quality.py +377 -377
- 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
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
function
|
|
38
|
-
return { schema:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
&& ID_RE.test(
|
|
115
|
-
&&
|
|
116
|
-
&&
|
|
117
|
-
&&
|
|
118
|
-
&&
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
&& new
|
|
132
|
-
&&
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
&& ID_RE.test(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (!
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (!
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if (
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
if (
|
|
552
|
-
throw new Error("
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const current = timestamp(now);
|
|
627
|
-
|
|
628
|
-
if (
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
runtime.
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (!
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
+
}
|