blun-king-cli 9.1.624 → 9.1.626

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/CHANGELOG.md +62 -213
  2. package/LIESMICH.txt +57 -74
  3. package/README.md +76 -119
  4. package/agent-spine-plugin/scripts/check-install.js +4 -4
  5. package/agent-spine-plugin/src/hook.js +0 -4
  6. package/agent-spine-plugin/src/lib/documents.js +1 -1
  7. package/agent-spine-plugin/src/lib/hook-context.js +0 -1
  8. package/agent-spine-plugin/src/lib/host-instruction-budget.js +2 -1
  9. package/agent-spine-plugin/src/lib/preflight.js +9 -38
  10. package/agent-spine-plugin/src/lib/source-roots.js +1 -1
  11. package/bin/agentspine-king-goal-inbox.mjs +127 -127
  12. package/bin/agentspine-king-goal-intake.mjs +106 -106
  13. package/bin/agentspine-king-host-runner.mjs +109 -109
  14. package/bin/agentspine-king-snapshot-policy.cjs +80 -80
  15. package/bin/agentspine-king-status-policy.mjs +226 -226
  16. package/bin/agentspine-king-worker-host.mjs +160 -160
  17. package/bin/core-bootstrap.js +83 -97
  18. package/bin/launcher-mode.js +93 -93
  19. package/bin/launcher-restart-policy.cjs +131 -131
  20. package/bin/launcher-runtime.js +91 -254
  21. package/bin/managed-node.js +186 -186
  22. package/bin/managed-plugin-selection.cjs +109 -116
  23. package/bin/native-module-repair.js +201 -201
  24. package/bin/node-runtime.js +34 -34
  25. package/bin/node-version.js +49 -49
  26. package/bin/plugin-bootstrap.js +149 -172
  27. package/bin/private-paths.js +1578 -1624
  28. package/bin/runtime-exit-ledger.cjs +144 -144
  29. package/bin/runtime-exit-ledger.d.cts +23 -23
  30. package/bin/standard-tools-bootstrap.js +277 -272
  31. package/bin/turn-thinking-policy.cjs +82 -82
  32. package/bin/update-copy.js +200 -200
  33. package/bin/update-lease.js +597 -599
  34. package/bin/update-notice.js +1568 -1568
  35. package/bin/verify-agent-behavior.cjs +122 -122
  36. package/bin/verify-agent-components.cjs +104 -104
  37. package/bin/verify-bundled-agent-sources.cjs +91 -96
  38. package/bin/windows-node-crash-dump.cjs +289 -289
  39. package/blun.mjs +187640 -180475
  40. package/bundled-agent-sources.json +26 -30
  41. package/codebase-index/codebase_index.py +462 -470
  42. package/dist-web/assets/{CodeBlockNode-TKKkJX8L.js → CodeBlockNode-BMbuG85x.js} +1 -1
  43. package/dist-web/assets/{CodeBlockShell.vue_vue_type_style_index_0_lang-CUj2Ds5x.js → CodeBlockShell.vue_vue_type_style_index_0_lang-C1fv7l_Z.js} +1 -1
  44. package/dist-web/assets/DesignSystemView-6HgAUEmC.css +1 -0
  45. package/dist-web/assets/DesignSystemView-CEEBqXSX.js +12 -0
  46. package/dist-web/assets/{Tooltip-CTWv7WCU.js → Tooltip--YoauJ24.js} +1 -1
  47. package/dist-web/assets/{abnfDiagram-N423BO3Z-DA8V8Zjh.js → abnfDiagram-N423BO3Z-DZvBWkRg.js} +1 -1
  48. package/dist-web/assets/{arc-YtddKbdY.js → arc-B04JATeX.js} +1 -1
  49. package/dist-web/assets/{architectureDiagram-T3A2C74G-CGEiObmo.js → architectureDiagram-T3A2C74G-DmYnMep2.js} +1 -1
  50. package/dist-web/assets/{blockDiagram-VBNYF7ZC-Bd5Vt50e.js → blockDiagram-VBNYF7ZC-DJ1gPS1C.js} +1 -1
  51. package/dist-web/assets/{c4Diagram-5PPSVZJV-D2ApPM54.js → c4Diagram-5PPSVZJV-DmjxvUbd.js} +1 -1
  52. package/dist-web/assets/channel-472fcEvw.js +1 -0
  53. package/dist-web/assets/{chunk-2GRJ4B5K-BDcIEZSl.js → chunk-2GRJ4B5K-AB0raj3I.js} +1 -1
  54. package/dist-web/assets/{chunk-2Q5K7J3B-6aSabCX7.js → chunk-2Q5K7J3B-Drf039vM.js} +1 -1
  55. package/dist-web/assets/{chunk-5RXB4S5H-BFRQvIJS.js → chunk-5RXB4S5H-D5ILttmz.js} +1 -1
  56. package/dist-web/assets/{chunk-5VM5RSS4-ChAa-DNY.js → chunk-5VM5RSS4-DrLSdi8y.js} +1 -1
  57. package/dist-web/assets/{chunk-6Q2QTUOP-BIfrb2Jc.js → chunk-6Q2QTUOP-LiaqdEOQ.js} +1 -1
  58. package/dist-web/assets/{chunk-GF5L2VYU-BR0gvtWI.js → chunk-GF5L2VYU-AIYkb--o.js} +1 -1
  59. package/dist-web/assets/{chunk-JWPE2WC7-BgXKsx5E.js → chunk-JWPE2WC7-CogdNl0i.js} +1 -1
  60. package/dist-web/assets/{chunk-KBJHAD2P-I8mrZhpz.js → chunk-KBJHAD2P-DHjpNtZv.js} +1 -1
  61. package/dist-web/assets/{chunk-RYQCIY6F-BMPaem92.js → chunk-RYQCIY6F-BUSOqH2X.js} +1 -1
  62. package/dist-web/assets/{chunk-XXDRQBXY-COy0J-0j.js → chunk-XXDRQBXY-BpoMV0bh.js} +1 -1
  63. package/dist-web/assets/classDiagram-JCYQIIEL-BTZeLngj.js +1 -0
  64. package/dist-web/assets/classDiagram-v2-OCEON4UE-BTZeLngj.js +1 -0
  65. package/dist-web/assets/{cose-bilkent-JH36ORCC-MosTTRdo.js → cose-bilkent-JH36ORCC-BJr2I61o.js} +1 -1
  66. package/dist-web/assets/{cynefin-VYW2F7L2-5tjPgrkP.js → cynefin-VYW2F7L2-HtysQZ4A.js} +1 -1
  67. package/dist-web/assets/{cynefinDiagram-MW4NZA55-CbDcdInR.js → cynefinDiagram-MW4NZA55-CcpC_2uv.js} +1 -1
  68. package/dist-web/assets/{dagre-VZM6K2ZE-YaBom1ZH.js → dagre-VZM6K2ZE-D8Ao0LUg.js} +1 -1
  69. package/dist-web/assets/{diagram-7IWD3JNH-Da4m-Hgz.js → diagram-7IWD3JNH-BITiLhjX.js} +1 -1
  70. package/dist-web/assets/{diagram-B4RE2ZJO-CAUUegxg.js → diagram-B4RE2ZJO-CJtwfy9f.js} +1 -1
  71. package/dist-web/assets/{diagram-LBJQPF4R-o21mWKG4.js → diagram-LBJQPF4R-BsVTh6w8.js} +1 -1
  72. package/dist-web/assets/{diagram-Q27KOJAE-BA3z3sXf.js → diagram-Q27KOJAE-pA_MUCUf.js} +1 -1
  73. package/dist-web/assets/{diagram-UB23O5K3-CGRdSSDK.js → diagram-UB23O5K3-CIY8z7vu.js} +1 -1
  74. package/dist-web/assets/{ebnfDiagram-BXEA7PRR-Bw-iU9FP.js → ebnfDiagram-BXEA7PRR-C3FMioqp.js} +1 -1
  75. package/dist-web/assets/{erDiagram-JOGREHBK-BwM1yiR0.js → erDiagram-JOGREHBK-CzoyKmKc.js} +1 -1
  76. package/dist-web/assets/{flowDiagram-UKHOOZJN-z05eOR1Q.js → flowDiagram-UKHOOZJN-DOqfushy.js} +1 -1
  77. package/dist-web/assets/{ganttDiagram-PKOTCBZU-BMUubNTl.js → ganttDiagram-PKOTCBZU-Cf9XQSWl.js} +1 -1
  78. package/dist-web/assets/{gitGraphDiagram-DS77QQ5N-Dccbi1wU.js → gitGraphDiagram-DS77QQ5N-DHUDofE5.js} +1 -1
  79. package/dist-web/assets/{index-h0KEOmkU.js → index-B6qEMQMx.js} +14 -14
  80. package/dist-web/assets/{index-C9eDH3u-.css → index-BmKj7CB1.css} +1 -1
  81. package/dist-web/assets/{index-BBbC5LRF.js → index-DokxDX1w.js} +1 -1
  82. package/dist-web/assets/{index-D48p0M2S.js → index-auojtiAA.js} +2 -2
  83. package/dist-web/assets/{index10-Qbuw1N6u.js → index10-Drb2n5e5.js} +1 -1
  84. package/dist-web/assets/{index11-g8v025wN.js → index11-Cx-aV4G2.js} +1 -1
  85. package/dist-web/assets/{index5-BYNcV8BY.js → index5-Dg3qGTyH.js} +1 -1
  86. package/dist-web/assets/{index6-BetANtnV.js → index6-BgvtIICP.js} +1 -1
  87. package/dist-web/assets/{index7-BcPbgCKN.js → index7-uEnniPQG.js} +1 -1
  88. package/dist-web/assets/{index8-B1lp-RvF.js → index8-h4IKc0ek.js} +2 -2
  89. package/dist-web/assets/{infoDiagram-6WML65LV-BF_pbUnN.js → infoDiagram-6WML65LV-D-3tvEiJ.js} +1 -1
  90. package/dist-web/assets/{ishikawaDiagram-WSZJBQD7-Dedv4urS.js → ishikawaDiagram-WSZJBQD7-DIIBNv7a.js} +1 -1
  91. package/dist-web/assets/{journeyDiagram-NVQOT4AX-B1Ex3b6o.js → journeyDiagram-NVQOT4AX-DP4r1Vlx.js} +1 -1
  92. package/dist-web/assets/{kanban-definition-27J2QSJJ-BtCGliBH.js → kanban-definition-27J2QSJJ-C3_Ztgxv.js} +1 -1
  93. package/dist-web/assets/{linear-ByItc8Qr.js → linear-DnqhIw-y.js} +1 -1
  94. package/dist-web/assets/{mermaid.core-B1M2O7nN.js → mermaid.core-xYUgBa3T.js} +4 -4
  95. package/dist-web/assets/{mindmap-definition-FAOFIHXS-D0UHKUyj.js → mindmap-definition-FAOFIHXS-l-dq-zfr.js} +1 -1
  96. package/dist-web/assets/{pegDiagram-VL7TDLO6-D7zYCVmG.js → pegDiagram-VL7TDLO6-C8vXQiaJ.js} +1 -1
  97. package/dist-web/assets/{pieDiagram-7S7Q4E2Y-Bd0i9CHa.js → pieDiagram-7S7Q4E2Y-CyNRSsI6.js} +1 -1
  98. package/dist-web/assets/{quadrantDiagram-CIZ2JOQS-MG-Ug47d.js → quadrantDiagram-CIZ2JOQS-D-w8AD_g.js} +1 -1
  99. package/dist-web/assets/{railroadDiagram-AXF67PYL-AZdl90Gj.js → railroadDiagram-AXF67PYL-BLkyQCWy.js} +1 -1
  100. package/dist-web/assets/{requirementDiagram-LRYGKXZP-DjplF9kt.js → requirementDiagram-LRYGKXZP-CEWO7XfT.js} +1 -1
  101. package/dist-web/assets/{sankeyDiagram-W5VNT64P-Bi9Zgfjs.js → sankeyDiagram-W5VNT64P-ByGoAaGj.js} +1 -1
  102. package/dist-web/assets/{sequenceDiagram-SI44F4Z6-ChEpdCvb.js → sequenceDiagram-SI44F4Z6-Bhn9GQtd.js} +1 -1
  103. package/dist-web/assets/{sizeCapture-X5ZJPWSS-B5RFUdaa.js → sizeCapture-X5ZJPWSS-jVKR9CTu.js} +1 -1
  104. package/dist-web/assets/{stateDiagram-OKZ733FA-BkRblSKp.js → stateDiagram-OKZ733FA-wW3XbegA.js} +1 -1
  105. package/dist-web/assets/stateDiagram-v2-UEYNNEHI-B8aids-Z.js +1 -0
  106. package/dist-web/assets/{swimlanes-SLNWSIFB-DLXH6Zhc.js → swimlanes-SLNWSIFB-B8QyFS7G.js} +2 -2
  107. package/dist-web/assets/swimlanesDiagram-ULZ7WXOC-B8XAkdSm.js +8 -0
  108. package/dist-web/assets/{timeline-definition-Z64GVDOM-C_N88siO.js → timeline-definition-Z64GVDOM-DpvxS-1A.js} +1 -1
  109. package/dist-web/assets/{vennDiagram-T6HMQDX7-DMsnvDt0.js → vennDiagram-T6HMQDX7-BzGM0ihY.js} +1 -1
  110. package/dist-web/assets/{vue.runtime.esm-bundler-B-3uIy85.js → vue.runtime.esm-bundler-BOcpwIX1.js} +1 -1
  111. package/dist-web/assets/{wardleyDiagram-T6FBY63Y-C9SJIPzq.js → wardleyDiagram-T6FBY63Y-2DKAOs5e.js} +1 -1
  112. package/dist-web/assets/{xychartDiagram-ELKLHX3M-DZLWA6Go.js → xychartDiagram-ELKLHX3M-B0SP0TXd.js} +1 -1
  113. package/dist-web/index.html +32 -32
  114. package/dist-web/vis/index.html.gz +0 -0
  115. package/package.json +1 -1
  116. package/scripts/fix-node-pty-perms.js +53 -53
  117. package/standard-skills/blun-app-design-system/SKILL.md +30 -30
  118. package/standard-skills/blun-app-design-system/provenance.json +17 -17
  119. package/standard-skills/blun-app-design-system/references/tokens.md +48 -48
  120. package/standard-skills/blun-app-design-system/scripts/check-blun-design.mjs +49 -49
  121. package/standard-skills/translate-native/README.md +1 -1
  122. package/standard-skills/translate-native/scripts/blun_language_guard.py +2 -0
  123. package/standard-skills/translate-native/scripts/language_gateway.py +2 -0
  124. package/standard-skills/translate-native/scripts/pre_output_guard.py +2 -0
  125. package/standard-tools/language-guard/blun_language_guard.py +2 -0
  126. package/standard-tools/language-guard/language_gateway.py +2 -0
  127. package/standard-tools/language-guard/pre_output_guard.py +2 -0
  128. package/telegram-plugin/blun.plugin.json +20 -20
  129. package/telegram-plugin/commands/access.md +37 -37
  130. package/telegram-plugin/dist/bridge.mjs +310 -187
  131. package/telegram-plugin/dist/mcp-server.mjs +174 -261
  132. package/telegram-plugin/dist/noise.mjs +1 -19
  133. package/worker-host.mjs +13747 -16824
  134. package/bin/profile-handoff.cjs +0 -152
  135. package/bin/profile-launch.cjs +0 -120
  136. package/dist-web/assets/DesignSystemView-DwK1aQMo.js +0 -12
  137. package/dist-web/assets/DesignSystemView-szjTj7ns.css +0 -1
  138. package/dist-web/assets/channel-BnluexuN.js +0 -1
  139. package/dist-web/assets/classDiagram-JCYQIIEL-BliFdSD9.js +0 -1
  140. package/dist-web/assets/classDiagram-v2-OCEON4UE-BliFdSD9.js +0 -1
  141. package/dist-web/assets/stateDiagram-v2-UEYNNEHI-fVkQcACW.js +0 -1
  142. package/dist-web/assets/swimlanesDiagram-ULZ7WXOC-Bg-BiZJQ.js +0 -8
@@ -1,226 +1,226 @@
1
- import { createHash, randomUUID } from 'node:crypto';
2
- import { lstat, mkdir, open, readFile, realpath, rename, unlink } from 'node:fs/promises';
3
- import path from 'node:path';
4
-
5
- const SCHEMA = 'blun.king-goal-status/v2';
6
- const MAX_BYTES = 4 * 1024 * 1024;
7
- const STATES = new Set(['pending', 'dispatching', 'uncertain', 'delivered']);
8
- const RESULT_REJECTIONS = new Set(['GOAL_STEP_RESULT_EXCEEDS_64_KIB', 'GOAL_STEP_RESULT_INVALID_JSON',
9
- 'GOAL_STEP_RESULT_INVALID', 'GOAL_STEP_CHECKPOINT_REQUIRED', 'GOAL_STEP_CHECKPOINT_TOO_LARGE']);
10
- const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
11
- const identifier = value => typeof value === 'string' && /^[A-Za-z0-9:._@/-]{1,200}$/.test(value);
12
- const routeKeys = ['id', 'provider', 'tenantId', 'accountId', 'chatId', 'threadId', 'agentId',
13
- 'projectId', 'groupId', 'createdAt'];
14
-
15
- export function goalResultRejectionCode(error) {
16
- const code = String(error?.message ?? error).replace(/^Host runtime unavailable: /, '');
17
- return RESULT_REJECTIONS.has(code) ? code : null;
18
- }
19
-
20
- function normalizeScope(input) {
21
- const keys = ['logicalPersonaId', 'runtimePersonaId', 'projectId', 'groupId', 'ownerSubjectId',
22
- 'chatId', 'senderId'];
23
- if (!input || keys.some(key => !identifier(input[key]))
24
- || (input.threadId !== null && input.threadId !== undefined && !identifier(input.threadId))) throw new Error('GOAL_STATUS_SCOPE_INVALID');
25
- return { ...Object.fromEntries(keys.map(key => [key, input[key]])), threadId: input.threadId ?? null };
26
- }
27
-
28
- function errorCode(error) {
29
- return /^(GOAL_[A-Z0-9_]{1,100})(?::|\s|$)/.exec(String(error ?? ''))?.[1] ?? 'GOAL_STATUS_OPERATION_REJECTED';
30
- }
31
-
32
- function normalizeEvent(input) {
33
- if (!input || !identifier(input.eventId)) throw new Error('GOAL_STATUS_EVENT_INVALID');
34
- if (input.kind === 'intake-rejected' && identifier(input.messageId)) {
35
- return { kind: input.kind, eventId: input.eventId, messageId: input.messageId, error: errorCode(input.error) };
36
- }
37
- if (!identifier(input.goalId) || (input.goalStepId !== null && input.goalStepId !== undefined && !identifier(input.goalStepId))) {
38
- throw new Error('GOAL_STATUS_EVENT_INVALID');
39
- }
40
- const event = { kind: input.kind, eventId: input.eventId, goalId: input.goalId, goalStepId: input.goalStepId ?? null };
41
- if (input.kind === 'result-rejected' && Number.isSafeInteger(input.attempt) && input.attempt > 0) {
42
- return { ...event, attempt: input.attempt, error: errorCode(input.error) };
43
- }
44
- if (input.kind !== 'goal-status' || !identifier(input.status)
45
- || !['offen', 'blockiert', 'bestanden'].includes(input.gate)) throw new Error('GOAL_STATUS_EVENT_INVALID');
46
- return { ...event, status: input.status, gate: input.gate };
47
- }
48
-
49
- function message(scope, event) {
50
- if (event.kind === 'intake-rejected') {
51
- return `${scope.logicalPersonaId} | Zielannahme Nachricht ${event.messageId} | abgelehnt | ${event.error}`;
52
- }
53
- if (event.kind === 'result-rejected') {
54
- return `${scope.logicalPersonaId} | Zielantwort ${event.goalId} / ${event.goalStepId ?? '-'} | abgelehnt ${event.attempt} | ${event.error}`;
55
- }
56
- return `${scope.logicalPersonaId} | Ziel ${event.goalId} | Schritt ${event.goalStepId ?? '-'} | ${event.status} | Gate ${event.gate}`;
57
- }
58
-
59
- function matches(binding, scope) {
60
- return binding?.status === 'active' && binding.provider === 'telegram'
61
- && binding.authority === 'explicit-local-channel-policy'
62
- && binding.agentId === scope.runtimePersonaId && binding.projectId === scope.projectId
63
- && binding.groupId === scope.groupId && binding.chatId === scope.chatId
64
- && (binding.threadId ?? null) === scope.threadId
65
- && binding.senderIds?.includes(scope.senderId) && binding.capabilities?.includes('reply');
66
- }
67
-
68
- function selectRoute(loaded, scope) {
69
- const candidates = loaded?.policy?.bindings?.filter(binding => matches(binding, scope)) ?? [];
70
- if (candidates.length !== 1) return null;
71
- const binding = candidates[0];
72
- if (routeKeys.some(key => key !== 'threadId' && (typeof binding[key] !== 'string' || !binding[key]))) return null;
73
- return Object.fromEntries(routeKeys.map(key => [key, binding[key] ?? null]));
74
- }
75
-
76
- function sameRoute(left, right) {
77
- return left && right && routeKeys.every(key => left[key] === right[key]);
78
- }
79
-
80
- async function assertRegular(file) {
81
- try {
82
- const meta = await lstat(file);
83
- if (!meta.isFile() || meta.isSymbolicLink()) throw new Error('GOAL_STATUS_NOT_REGULAR');
84
- } catch (error) { if (error.code !== 'ENOENT') throw error; }
85
- }
86
-
87
- async function loadState(file) {
88
- await assertRegular(file);
89
- let bytes;
90
- try { bytes = await readFile(file); } catch (error) {
91
- if (error.code === 'ENOENT') return { schema: SCHEMA, receipts: [] };
92
- throw error;
93
- }
94
- try {
95
- if (bytes.length > MAX_BYTES) throw new Error('oversized');
96
- const state = JSON.parse(bytes);
97
- if (state?.schema !== SCHEMA || !Array.isArray(state.receipts)) throw new Error('schema');
98
- const keys = new Set();
99
- for (const record of state.receipts) {
100
- const scope = normalizeScope(record.scope);
101
- const event = normalizeEvent(record.event);
102
- if (JSON.stringify(scope) !== JSON.stringify(record.scope) || JSON.stringify(event) !== JSON.stringify(record.event)
103
- || record.key !== digest({ scope, event }) || keys.has(record.key) || !STATES.has(record.status)
104
- || !Number.isSafeInteger(record.nextAttemptAt) || record.nextAttemptAt < 0
105
- || !Number.isSafeInteger(record.attempts) || record.attempts < 0
106
- || (record.route !== null && (!record.route || routeKeys.some(key => !(key in record.route))))
107
- || (record.status === 'delivered' && !/^telegram-message:\d+$/.test(record.receipt))) throw new Error('receipt');
108
- keys.add(record.key);
109
- }
110
- return state;
111
- } catch { throw new Error('GOAL_STATUS_STATE_INVALID'); }
112
- }
113
-
114
- async function saveState(file, state, assertOwned) {
115
- const bytes = JSON.stringify(state) + '\n';
116
- if (Buffer.byteLength(bytes) > MAX_BYTES) throw new Error('GOAL_STATUS_CAPACITY');
117
- const temporary = `${file}.${randomUUID()}.tmp`;
118
- const handle = await open(temporary, 'wx', 0o600);
119
- try {
120
- await handle.writeFile(bytes);
121
- await handle.sync();
122
- } finally { await handle.close(); }
123
- try {
124
- await assertOwned();
125
- await assertRegular(file);
126
- await rename(temporary, file);
127
- } finally { await unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error; }); }
128
- }
129
-
130
- export function createGoalStatusReporter({ stateFile, scope: inputScope, withOwnedFileLock,
131
- loadPolicy, adapter, now = Date.now, maxRecords = 512 }) {
132
- const scope = normalizeScope(inputScope);
133
- if (!path.isAbsolute(stateFile) || typeof withOwnedFileLock !== 'function'
134
- || typeof loadPolicy !== 'function' || typeof adapter?.send !== 'function'
135
- || !Number.isSafeInteger(maxRecords) || maxRecords < 1) throw new Error('GOAL_STATUS_CONFIG_INVALID');
136
- const scopeKey = digest(scope);
137
- const transact = async task => {
138
- const directory = path.dirname(stateFile);
139
- await mkdir(directory, { recursive: true });
140
- if (path.relative(directory, await realpath(directory)) !== '') throw new Error('GOAL_STATUS_DIRECTORY_REDIRECTED');
141
- const assertPath = async () => {
142
- if (path.relative(directory, await realpath(directory)) !== '') throw new Error('GOAL_STATUS_DIRECTORY_REDIRECTED');
143
- await assertRegular(`${stateFile}.lock`);
144
- };
145
- return withOwnedFileLock(`${stateFile}.lock`, async ({ assertOwned }) => {
146
- const state = await loadState(stateFile);
147
- return task(state, () => saveState(stateFile, state, assertOwned), assertOwned);
148
- }, { assertPath });
149
- };
150
-
151
- const record = async input => {
152
- const event = normalizeEvent(input);
153
- return transact(async (state, save) => {
154
- const key = digest({ scope, event });
155
- const existing = state.receipts.find(row => row.key === key);
156
- if (existing) return { key, status: existing.status };
157
- // Retain the deduplication history and every unresolved delivery; never silently evict one.
158
- if (state.receipts.length >= maxRecords) throw new Error('GOAL_STATUS_CAPACITY');
159
- state.receipts.push({ key, scope, event, route: selectRoute(await loadPolicy(), scope),
160
- status: 'pending', attempts: 0, nextAttemptAt: 0, receipt: null });
161
- await save();
162
- return { key, status: 'pending' };
163
- });
164
- };
165
-
166
- const flush = () => transact(async (state, save, assertOwned) => {
167
- const results = [];
168
- for (const row of state.receipts) {
169
- if (digest(row.scope) !== scopeKey || row.status === 'delivered') continue;
170
- const result = status => { results.push({ key: row.key, status }); };
171
- // A process may have died after Telegram accepted the request but before the receipt was saved.
172
- if (row.status === 'dispatching') { row.status = 'uncertain'; await save(); }
173
- if (row.status === 'uncertain') { result('uncertain'); continue; }
174
- const current = selectRoute(await loadPolicy(), scope);
175
- if (!current) { result(row.route ? 'route-changed' : 'unrouted'); continue; }
176
- if (row.route && !sameRoute(row.route, current)) { result('route-changed'); continue; }
177
- if (row.nextAttemptAt > now()) { result('waiting'); continue; }
178
- row.route ??= current;
179
- row.status = 'dispatching'; row.attempts++;
180
- await save();
181
- if (!sameRoute(row.route, selectRoute(await loadPolicy(), scope))) {
182
- row.status = 'pending'; await save(); result('route-changed'); continue;
183
- }
184
- await assertOwned();
185
- let outcome;
186
- try {
187
- outcome = await adapter.send({ provider: row.route.provider, bindingId: row.route.id,
188
- tenantId: row.route.tenantId, accountId: row.route.accountId, chatId: row.route.chatId,
189
- threadId: row.route.threadId, replyTo: null,
190
- idempotencyKey: row.key, text: message(scope, row.event) });
191
- } catch { outcome = { ok: false, effect: 'unknown' }; }
192
- if (outcome?.ok === true && /^telegram-message:\d+$/.test(outcome.receipt)) {
193
- row.status = 'delivered'; row.receipt = outcome.receipt;
194
- } else if (outcome?.ok === false && outcome.effect === 'none') {
195
- row.status = 'pending';
196
- const wait = Number.isSafeInteger(outcome.retryAfterMs) && outcome.retryAfterMs > 0 ? outcome.retryAfterMs : 15000;
197
- row.nextAttemptAt = Math.min(Number.MAX_SAFE_INTEGER, now() + wait);
198
- } else { row.status = 'uncertain'; }
199
- await save();
200
- result(row.status);
201
- }
202
- return results;
203
- });
204
-
205
- const reconcile = async context => {
206
- for (const goal of context.goals) {
207
- if (goal.agentId !== scope.runtimePersonaId || goal.projectId !== scope.projectId
208
- || goal.groupId !== scope.groupId || goal.ownerSubjectId !== scope.ownerSubjectId) continue;
209
- const rows = context.queue.filter(row => row.goalId === goal.goalId && row.agentId === scope.runtimePersonaId
210
- && row.projectId === scope.projectId && row.groupId === scope.groupId && row.attempts > 0);
211
- const row = rows.toSorted((a, b) => a.updatedAt.localeCompare(b.updatedAt)).at(-1);
212
- if (!row) continue;
213
- const step = goal.plan?.steps.find(item => item.stepId === row.goalStepId);
214
- if (row.goalStepId && !step) continue;
215
- const identity = { eventId: `${row.queueId}:${row.attempts}`, goalId: goal.goalId, goalStepId: row.goalStepId ?? null };
216
- const rejection = goalResultRejectionCode(row.lastError);
217
- if (rejection) {
218
- await record({ ...identity, kind: 'result-rejected', attempt: row.attempts, error: rejection });
219
- }
220
- const status = row.status === 'blocked' ? 'blocked' : step?.status ?? goal.status;
221
- await record({ ...identity, kind: 'goal-status', status,
222
- gate: goal.status === 'completed' ? 'bestanden' : status === 'blocked' ? 'blockiert' : 'offen' });
223
- }
224
- };
225
- return Object.freeze({ record, flush, reconcile });
226
- }
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { lstat, mkdir, open, readFile, realpath, rename, unlink } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ const SCHEMA = 'blun.king-goal-status/v2';
6
+ const MAX_BYTES = 4 * 1024 * 1024;
7
+ const STATES = new Set(['pending', 'dispatching', 'uncertain', 'delivered']);
8
+ const RESULT_REJECTIONS = new Set(['GOAL_STEP_RESULT_EXCEEDS_64_KIB', 'GOAL_STEP_RESULT_INVALID_JSON',
9
+ 'GOAL_STEP_RESULT_INVALID', 'GOAL_STEP_CHECKPOINT_REQUIRED', 'GOAL_STEP_CHECKPOINT_TOO_LARGE']);
10
+ const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
11
+ const identifier = value => typeof value === 'string' && /^[A-Za-z0-9:._@/-]{1,200}$/.test(value);
12
+ const routeKeys = ['id', 'provider', 'tenantId', 'accountId', 'chatId', 'threadId', 'agentId',
13
+ 'projectId', 'groupId', 'createdAt'];
14
+
15
+ export function goalResultRejectionCode(error) {
16
+ const code = String(error?.message ?? error).replace(/^Host runtime unavailable: /, '');
17
+ return RESULT_REJECTIONS.has(code) ? code : null;
18
+ }
19
+
20
+ function normalizeScope(input) {
21
+ const keys = ['logicalPersonaId', 'runtimePersonaId', 'projectId', 'groupId', 'ownerSubjectId',
22
+ 'chatId', 'senderId'];
23
+ if (!input || keys.some(key => !identifier(input[key]))
24
+ || (input.threadId !== null && input.threadId !== undefined && !identifier(input.threadId))) throw new Error('GOAL_STATUS_SCOPE_INVALID');
25
+ return { ...Object.fromEntries(keys.map(key => [key, input[key]])), threadId: input.threadId ?? null };
26
+ }
27
+
28
+ function errorCode(error) {
29
+ return /^(GOAL_[A-Z0-9_]{1,100})(?::|\s|$)/.exec(String(error ?? ''))?.[1] ?? 'GOAL_STATUS_OPERATION_REJECTED';
30
+ }
31
+
32
+ function normalizeEvent(input) {
33
+ if (!input || !identifier(input.eventId)) throw new Error('GOAL_STATUS_EVENT_INVALID');
34
+ if (input.kind === 'intake-rejected' && identifier(input.messageId)) {
35
+ return { kind: input.kind, eventId: input.eventId, messageId: input.messageId, error: errorCode(input.error) };
36
+ }
37
+ if (!identifier(input.goalId) || (input.goalStepId !== null && input.goalStepId !== undefined && !identifier(input.goalStepId))) {
38
+ throw new Error('GOAL_STATUS_EVENT_INVALID');
39
+ }
40
+ const event = { kind: input.kind, eventId: input.eventId, goalId: input.goalId, goalStepId: input.goalStepId ?? null };
41
+ if (input.kind === 'result-rejected' && Number.isSafeInteger(input.attempt) && input.attempt > 0) {
42
+ return { ...event, attempt: input.attempt, error: errorCode(input.error) };
43
+ }
44
+ if (input.kind !== 'goal-status' || !identifier(input.status)
45
+ || !['offen', 'blockiert', 'bestanden'].includes(input.gate)) throw new Error('GOAL_STATUS_EVENT_INVALID');
46
+ return { ...event, status: input.status, gate: input.gate };
47
+ }
48
+
49
+ function message(scope, event) {
50
+ if (event.kind === 'intake-rejected') {
51
+ return `${scope.logicalPersonaId} | Zielannahme Nachricht ${event.messageId} | abgelehnt | ${event.error}`;
52
+ }
53
+ if (event.kind === 'result-rejected') {
54
+ return `${scope.logicalPersonaId} | Zielantwort ${event.goalId} / ${event.goalStepId ?? '-'} | abgelehnt ${event.attempt} | ${event.error}`;
55
+ }
56
+ return `${scope.logicalPersonaId} | Ziel ${event.goalId} | Schritt ${event.goalStepId ?? '-'} | ${event.status} | Gate ${event.gate}`;
57
+ }
58
+
59
+ function matches(binding, scope) {
60
+ return binding?.status === 'active' && binding.provider === 'telegram'
61
+ && binding.authority === 'explicit-local-channel-policy'
62
+ && binding.agentId === scope.runtimePersonaId && binding.projectId === scope.projectId
63
+ && binding.groupId === scope.groupId && binding.chatId === scope.chatId
64
+ && (binding.threadId ?? null) === scope.threadId
65
+ && binding.senderIds?.includes(scope.senderId) && binding.capabilities?.includes('reply');
66
+ }
67
+
68
+ function selectRoute(loaded, scope) {
69
+ const candidates = loaded?.policy?.bindings?.filter(binding => matches(binding, scope)) ?? [];
70
+ if (candidates.length !== 1) return null;
71
+ const binding = candidates[0];
72
+ if (routeKeys.some(key => key !== 'threadId' && (typeof binding[key] !== 'string' || !binding[key]))) return null;
73
+ return Object.fromEntries(routeKeys.map(key => [key, binding[key] ?? null]));
74
+ }
75
+
76
+ function sameRoute(left, right) {
77
+ return left && right && routeKeys.every(key => left[key] === right[key]);
78
+ }
79
+
80
+ async function assertRegular(file) {
81
+ try {
82
+ const meta = await lstat(file);
83
+ if (!meta.isFile() || meta.isSymbolicLink()) throw new Error('GOAL_STATUS_NOT_REGULAR');
84
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
85
+ }
86
+
87
+ async function loadState(file) {
88
+ await assertRegular(file);
89
+ let bytes;
90
+ try { bytes = await readFile(file); } catch (error) {
91
+ if (error.code === 'ENOENT') return { schema: SCHEMA, receipts: [] };
92
+ throw error;
93
+ }
94
+ try {
95
+ if (bytes.length > MAX_BYTES) throw new Error('oversized');
96
+ const state = JSON.parse(bytes);
97
+ if (state?.schema !== SCHEMA || !Array.isArray(state.receipts)) throw new Error('schema');
98
+ const keys = new Set();
99
+ for (const record of state.receipts) {
100
+ const scope = normalizeScope(record.scope);
101
+ const event = normalizeEvent(record.event);
102
+ if (JSON.stringify(scope) !== JSON.stringify(record.scope) || JSON.stringify(event) !== JSON.stringify(record.event)
103
+ || record.key !== digest({ scope, event }) || keys.has(record.key) || !STATES.has(record.status)
104
+ || !Number.isSafeInteger(record.nextAttemptAt) || record.nextAttemptAt < 0
105
+ || !Number.isSafeInteger(record.attempts) || record.attempts < 0
106
+ || (record.route !== null && (!record.route || routeKeys.some(key => !(key in record.route))))
107
+ || (record.status === 'delivered' && !/^telegram-message:\d+$/.test(record.receipt))) throw new Error('receipt');
108
+ keys.add(record.key);
109
+ }
110
+ return state;
111
+ } catch { throw new Error('GOAL_STATUS_STATE_INVALID'); }
112
+ }
113
+
114
+ async function saveState(file, state, assertOwned) {
115
+ const bytes = JSON.stringify(state) + '\n';
116
+ if (Buffer.byteLength(bytes) > MAX_BYTES) throw new Error('GOAL_STATUS_CAPACITY');
117
+ const temporary = `${file}.${randomUUID()}.tmp`;
118
+ const handle = await open(temporary, 'wx', 0o600);
119
+ try {
120
+ await handle.writeFile(bytes);
121
+ await handle.sync();
122
+ } finally { await handle.close(); }
123
+ try {
124
+ await assertOwned();
125
+ await assertRegular(file);
126
+ await rename(temporary, file);
127
+ } finally { await unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error; }); }
128
+ }
129
+
130
+ export function createGoalStatusReporter({ stateFile, scope: inputScope, withOwnedFileLock,
131
+ loadPolicy, adapter, now = Date.now, maxRecords = 512 }) {
132
+ const scope = normalizeScope(inputScope);
133
+ if (!path.isAbsolute(stateFile) || typeof withOwnedFileLock !== 'function'
134
+ || typeof loadPolicy !== 'function' || typeof adapter?.send !== 'function'
135
+ || !Number.isSafeInteger(maxRecords) || maxRecords < 1) throw new Error('GOAL_STATUS_CONFIG_INVALID');
136
+ const scopeKey = digest(scope);
137
+ const transact = async task => {
138
+ const directory = path.dirname(stateFile);
139
+ await mkdir(directory, { recursive: true });
140
+ if (path.relative(directory, await realpath(directory)) !== '') throw new Error('GOAL_STATUS_DIRECTORY_REDIRECTED');
141
+ const assertPath = async () => {
142
+ if (path.relative(directory, await realpath(directory)) !== '') throw new Error('GOAL_STATUS_DIRECTORY_REDIRECTED');
143
+ await assertRegular(`${stateFile}.lock`);
144
+ };
145
+ return withOwnedFileLock(`${stateFile}.lock`, async ({ assertOwned }) => {
146
+ const state = await loadState(stateFile);
147
+ return task(state, () => saveState(stateFile, state, assertOwned), assertOwned);
148
+ }, { assertPath });
149
+ };
150
+
151
+ const record = async input => {
152
+ const event = normalizeEvent(input);
153
+ return transact(async (state, save) => {
154
+ const key = digest({ scope, event });
155
+ const existing = state.receipts.find(row => row.key === key);
156
+ if (existing) return { key, status: existing.status };
157
+ // Retain the deduplication history and every unresolved delivery; never silently evict one.
158
+ if (state.receipts.length >= maxRecords) throw new Error('GOAL_STATUS_CAPACITY');
159
+ state.receipts.push({ key, scope, event, route: selectRoute(await loadPolicy(), scope),
160
+ status: 'pending', attempts: 0, nextAttemptAt: 0, receipt: null });
161
+ await save();
162
+ return { key, status: 'pending' };
163
+ });
164
+ };
165
+
166
+ const flush = () => transact(async (state, save, assertOwned) => {
167
+ const results = [];
168
+ for (const row of state.receipts) {
169
+ if (digest(row.scope) !== scopeKey || row.status === 'delivered') continue;
170
+ const result = status => { results.push({ key: row.key, status }); };
171
+ // A process may have died after Telegram accepted the request but before the receipt was saved.
172
+ if (row.status === 'dispatching') { row.status = 'uncertain'; await save(); }
173
+ if (row.status === 'uncertain') { result('uncertain'); continue; }
174
+ const current = selectRoute(await loadPolicy(), scope);
175
+ if (!current) { result(row.route ? 'route-changed' : 'unrouted'); continue; }
176
+ if (row.route && !sameRoute(row.route, current)) { result('route-changed'); continue; }
177
+ if (row.nextAttemptAt > now()) { result('waiting'); continue; }
178
+ row.route ??= current;
179
+ row.status = 'dispatching'; row.attempts++;
180
+ await save();
181
+ if (!sameRoute(row.route, selectRoute(await loadPolicy(), scope))) {
182
+ row.status = 'pending'; await save(); result('route-changed'); continue;
183
+ }
184
+ await assertOwned();
185
+ let outcome;
186
+ try {
187
+ outcome = await adapter.send({ provider: row.route.provider, bindingId: row.route.id,
188
+ tenantId: row.route.tenantId, accountId: row.route.accountId, chatId: row.route.chatId,
189
+ threadId: row.route.threadId, replyTo: null,
190
+ idempotencyKey: row.key, text: message(scope, row.event) });
191
+ } catch { outcome = { ok: false, effect: 'unknown' }; }
192
+ if (outcome?.ok === true && /^telegram-message:\d+$/.test(outcome.receipt)) {
193
+ row.status = 'delivered'; row.receipt = outcome.receipt;
194
+ } else if (outcome?.ok === false && outcome.effect === 'none') {
195
+ row.status = 'pending';
196
+ const wait = Number.isSafeInteger(outcome.retryAfterMs) && outcome.retryAfterMs > 0 ? outcome.retryAfterMs : 15000;
197
+ row.nextAttemptAt = Math.min(Number.MAX_SAFE_INTEGER, now() + wait);
198
+ } else { row.status = 'uncertain'; }
199
+ await save();
200
+ result(row.status);
201
+ }
202
+ return results;
203
+ });
204
+
205
+ const reconcile = async context => {
206
+ for (const goal of context.goals) {
207
+ if (goal.agentId !== scope.runtimePersonaId || goal.projectId !== scope.projectId
208
+ || goal.groupId !== scope.groupId || goal.ownerSubjectId !== scope.ownerSubjectId) continue;
209
+ const rows = context.queue.filter(row => row.goalId === goal.goalId && row.agentId === scope.runtimePersonaId
210
+ && row.projectId === scope.projectId && row.groupId === scope.groupId && row.attempts > 0);
211
+ const row = rows.toSorted((a, b) => a.updatedAt.localeCompare(b.updatedAt)).at(-1);
212
+ if (!row) continue;
213
+ const step = goal.plan?.steps.find(item => item.stepId === row.goalStepId);
214
+ if (row.goalStepId && !step) continue;
215
+ const identity = { eventId: `${row.queueId}:${row.attempts}`, goalId: goal.goalId, goalStepId: row.goalStepId ?? null };
216
+ const rejection = goalResultRejectionCode(row.lastError);
217
+ if (rejection) {
218
+ await record({ ...identity, kind: 'result-rejected', attempt: row.attempts, error: rejection });
219
+ }
220
+ const status = row.status === 'blocked' ? 'blocked' : step?.status ?? goal.status;
221
+ await record({ ...identity, kind: 'goal-status', status,
222
+ gate: goal.status === 'completed' ? 'bestanden' : status === 'blocked' ? 'blockiert' : 'offen' });
223
+ }
224
+ };
225
+ return Object.freeze({ record, flush, reconcile });
226
+ }