blun-king-cli 9.1.527 → 9.1.550

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 (108) hide show
  1. package/LIESMICH.txt +13 -834
  2. package/README.md +41 -799
  3. package/bin/agent-resume-snapshot.cjs +31 -0
  4. package/bin/assistant-message-offload-policy.cjs +23 -2
  5. package/bin/codebase-search-runtime.cjs +23 -0
  6. package/bin/context-performance-policy.cjs +2 -5
  7. package/bin/context-pressure-policy.cjs +20 -0
  8. package/bin/cron-run-output.cjs +45 -0
  9. package/bin/cron-run-store.cjs +145 -0
  10. package/bin/durable-task-resume-policy.cjs +130 -0
  11. package/bin/durable-task-resume-runtime.cjs +117 -0
  12. package/bin/durable-task-resume-store.cjs +88 -0
  13. package/bin/editable-tool-approval-policy.cjs +540 -0
  14. package/bin/editable-tool-approval-runtime.cjs +99 -0
  15. package/bin/empty-response-retry-policy.cjs +29 -0
  16. package/bin/history-offload-pressure-policy.cjs +33 -0
  17. package/bin/html-to-research-markdown.cjs +146 -0
  18. package/bin/programmatic-context-isolation.cjs +25 -0
  19. package/bin/programmatic-tool-runtime.mjs +627 -0
  20. package/bin/read-continuation-policy.cjs +36 -5
  21. package/bin/scoped-cron-run-policy.cjs +358 -0
  22. package/bin/skill-activation-performance-policy.cjs +9 -0
  23. package/bin/startup-preferences.cjs +3 -3
  24. package/bin/structured-agent-swarm-output.cjs +325 -0
  25. package/bin/structured-subagent-output.cjs +252 -0
  26. package/bin/subagent-context-fork-policy.cjs +155 -0
  27. package/bin/subagent-skill-policy.cjs +204 -0
  28. package/bin/telegram-approval-relay.cjs +2 -1
  29. package/bin/telegram-direct-focus-policy.cjs +25 -1
  30. package/bin/todo-list-turn-policy.cjs +111 -1
  31. package/bin/tool-result-offload-policy.cjs +43 -1
  32. package/bin/turn-thinking-policy.cjs +6 -15
  33. package/bin/turn-tool-performance-policy.cjs +5 -4
  34. package/bin/update-notice.js +14 -18
  35. package/bin/user-message-offload-policy.cjs +12 -1
  36. package/blun.mjs +1406 -265
  37. package/codebase-index/README.md +82 -0
  38. package/codebase-index/codebase_index.py +469 -0
  39. package/package.json +24 -38
  40. package/telegram-plugin/bin/telegram-typing-keepalive.cjs +89 -0
  41. package/telegram-plugin/dist/bridge.mjs +8 -1
  42. package/CHANGELOG.md +0 -260
  43. package/agent-spine-plugin/CHANGELOG.md +0 -406
  44. package/agent-spine-plugin/CONTRIBUTING.md +0 -52
  45. package/agent-spine-plugin/README.md +0 -344
  46. package/agent-spine-plugin/SECURITY.md +0 -47
  47. package/agent-spine-plugin/docs/acceptance.md +0 -61
  48. package/agent-spine-plugin/docs/architecture.md +0 -183
  49. package/agent-spine-plugin/docs/attention.md +0 -121
  50. package/agent-spine-plugin/docs/automatic-continuity.md +0 -79
  51. package/agent-spine-plugin/docs/channel-runtime.md +0 -92
  52. package/agent-spine-plugin/docs/coordination.md +0 -138
  53. package/agent-spine-plugin/docs/feed-transport.md +0 -99
  54. package/agent-spine-plugin/docs/gateway-runtime.md +0 -116
  55. package/agent-spine-plugin/docs/harness-reference.md +0 -45
  56. package/agent-spine-plugin/docs/host-integration.md +0 -129
  57. package/agent-spine-plugin/docs/https-transport.md +0 -116
  58. package/agent-spine-plugin/docs/learning.md +0 -133
  59. package/agent-spine-plugin/docs/object-transport.md +0 -93
  60. package/agent-spine-plugin/docs/peer-transport.md +0 -88
  61. package/agent-spine-plugin/docs/preflight-recall.md +0 -69
  62. package/agent-spine-plugin/docs/preservation-contract.md +0 -53
  63. package/agent-spine-plugin/docs/quality-gates.md +0 -50
  64. package/agent-spine-plugin/docs/relationships.md +0 -73
  65. package/agent-spine-plugin/docs/releasing.md +0 -83
  66. package/agent-spine-plugin/docs/roadmap.md +0 -307
  67. package/agent-spine-plugin/docs/selfstarter.md +0 -88
  68. package/agent-spine-plugin/docs/session-briefing.md +0 -74
  69. package/agent-spine-plugin/docs/shared-memory.md +0 -259
  70. package/agent-spine-plugin/docs/source-roots.md +0 -86
  71. package/agent-spine-plugin/docs/sqlite-transport.md +0 -76
  72. package/agent-spine-plugin/scripts/check-hosts.js +0 -195
  73. package/agent-spine-plugin/scripts/check-install.js +0 -569
  74. package/agent-spine-plugin/scripts/check-syntax.js +0 -29
  75. package/agent-spine-plugin/scripts/github-actions.js +0 -11
  76. package/agent-spine-plugin/scripts/release-check.js +0 -128
  77. package/agent-spine-plugin/scripts/run-acceptance.js +0 -19
  78. package/agent-spine-plugin/scripts/run-checks.js +0 -46
  79. package/agent-spine-plugin/scripts/run-tests-hermetic.js +0 -73
  80. package/agent-spine-plugin/spine-example/1-identity.md +0 -12
  81. package/agent-spine-plugin/spine-example/2-voice.md +0 -6
  82. package/agent-spine-plugin/spine-example/3-conduct.md +0 -8
  83. package/agent-spine-plugin/spine-example/4-history.md +0 -4
  84. package/bin/package-regression-policy.cjs +0 -77
  85. package/release-planned-removals.json +0 -15
  86. package/scripts/check-active-profile-plugin-startup.js +0 -36
  87. package/scripts/check-approval-observability-regression.js +0 -111
  88. package/scripts/check-approval-queue-shortcuts-regression.js +0 -65
  89. package/scripts/check-bundled-agent-spine-regression.js +0 -48
  90. package/scripts/check-copy-command-regression.js +0 -74
  91. package/scripts/check-historical-tool-result-preview-regression.js +0 -77
  92. package/scripts/check-mcp-startup-wait-budget.js +0 -48
  93. package/scripts/check-package-regression.js +0 -38
  94. package/scripts/check-plugin-startup-regression.js +0 -53
  95. package/scripts/check-queue-controls-regression.js +0 -189
  96. package/scripts/check-release-metadata.js +0 -103
  97. package/scripts/check-reload-agent-spine-regression.js +0 -76
  98. package/scripts/check-resume-replay-regression.js +0 -100
  99. package/scripts/check-session-cancel-regression.js +0 -43
  100. package/scripts/check-session-picker-resume-metrics-regression.js +0 -97
  101. package/scripts/check-session-start-hook-context-regression.js +0 -228
  102. package/scripts/check-shell-terminal-isolation-regression.js +0 -81
  103. package/scripts/check-slash-escape-regression.js +0 -89
  104. package/scripts/check-telegram-bridge-watchdog.js +0 -60
  105. package/scripts/check-telegram-loop-exactly-once-regression.js +0 -71
  106. package/scripts/check-todo-loop-regression.js +0 -78
  107. package/scripts/check-todo-recovery-catalog-regression.js +0 -50
  108. /package/{scripts → bin}/fix-node-pty-perms.js +0 -0
@@ -1,12 +1,39 @@
1
1
  'use strict';
2
2
 
3
3
  const AGENT_RESUME_SNAPSHOT_SCHEMA_VERSION = 1;
4
+ const MAX_TODO_PROGRESS_EVIDENCE = 8;
5
+ const MAX_TODO_PROGRESS_EVIDENCE_CHARS = 200;
4
6
 
5
7
  function copyJsonObject(value, fallback) {
6
8
  if (value === undefined) return fallback;
7
9
  return JSON.parse(JSON.stringify(value));
8
10
  }
9
11
 
12
+ function normalizeTodoProgressState(value) {
13
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined;
14
+ const nonNegativeInteger = (candidate, maximum) => Number.isSafeInteger(candidate)
15
+ ? Math.max(0, Math.min(candidate, maximum))
16
+ : 0;
17
+ const evidenceRevision = nonNegativeInteger(value.evidenceRevision, Number.MAX_SAFE_INTEGER);
18
+ return {
19
+ workCallsSinceRefresh: nonNegativeInteger(value.workCallsSinceRefresh, 64),
20
+ refreshRequired: value.refreshRequired === true,
21
+ unchangedRefreshStreak: nonNegativeInteger(value.unchangedRefreshStreak, 3),
22
+ recentEvidence: Array.isArray(value.recentEvidence)
23
+ ? value.recentEvidence
24
+ .filter((entry) => typeof entry === 'string')
25
+ .slice(-MAX_TODO_PROGRESS_EVIDENCE)
26
+ .map((entry) => entry.slice(0, MAX_TODO_PROGRESS_EVIDENCE_CHARS))
27
+ : [],
28
+ evidenceRevision,
29
+ lastMaintenanceEvidenceRevision: Math.min(
30
+ nonNegativeInteger(value.lastMaintenanceEvidenceRevision, Number.MAX_SAFE_INTEGER),
31
+ evidenceRevision,
32
+ ),
33
+ allVisibleWorkCompleted: value.allVisibleWorkCompleted === true,
34
+ };
35
+ }
36
+
10
37
  function captureMessageTimes(agent) {
11
38
  const replayTimes = new Map();
12
39
  for (const record of agent.replayBuilder.records) {
@@ -63,6 +90,7 @@ function createAgentResumeSnapshot(agent) {
63
90
  allowedTools: agent.tools.allowedTools,
64
91
  excludedTools: [...agent.tools.excludedTools],
65
92
  },
93
+ todoProgress: normalizeTodoProgressState(agent.goalTodoPolicyState) ?? null,
66
94
  goal: agent.goal.state ?? null,
67
95
  turn: {
68
96
  turnId: agent.turn.turnId,
@@ -175,6 +203,8 @@ function restoreAgentResumeSnapshot(agent, snapshot) {
175
203
  : [];
176
204
  if (agent.config.hasProvider) agent.tools.initializeBuiltinTools();
177
205
 
206
+ agent.goalTodoPolicyState = normalizeTodoProgressState(snapshot.todoProgress);
207
+
178
208
  agent.goal.state = snapshot.goal === null
179
209
  ? undefined
180
210
  : copyJsonObject(snapshot.goal, undefined);
@@ -206,5 +236,6 @@ function restoreAgentResumeSnapshot(agent, snapshot) {
206
236
  module.exports = {
207
237
  AGENT_RESUME_SNAPSHOT_SCHEMA_VERSION,
208
238
  createAgentResumeSnapshot,
239
+ normalizeTodoProgressState,
209
240
  restoreAgentResumeSnapshot,
210
241
  };
@@ -1,5 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const {
4
+ hasHistoricalOffloadPressure,
5
+ historyTextChars,
6
+ } = require('./history-offload-pressure-policy.cjs');
7
+ const { hasContextPressure } = require('./context-pressure-policy.cjs');
8
+
3
9
  const ASSISTANT_MESSAGE_MAX_CHARS = 4_000;
4
10
  const ASSISTANT_MESSAGE_KEEP_RECENT_MESSAGES = 20;
5
11
  const ASSISTANT_MESSAGE_PREVIEW_LINES = 5;
@@ -34,6 +40,7 @@ function isSyntheticToolArguments(value) {
34
40
  function shouldOffloadHistoricalAssistantMessage(options = {}) {
35
41
  const historyIndex = Number(options.historyIndex);
36
42
  const historyLength = Number(options.historyLength);
43
+ const historyChars = Number(options.historyChars);
37
44
  const textChars = Number(options.textChars);
38
45
  if (
39
46
  !Number.isSafeInteger(historyIndex)
@@ -44,7 +51,10 @@ function shouldOffloadHistoricalAssistantMessage(options = {}) {
44
51
  || historyIndex >= historyLength
45
52
  ) return false;
46
53
  const cutoff = Math.max(0, historyLength - ASSISTANT_MESSAGE_KEEP_RECENT_MESSAGES);
47
- return historyIndex < cutoff && textChars > ASSISTANT_MESSAGE_MAX_CHARS;
54
+ return hasContextPressure(options)
55
+ && hasHistoricalOffloadPressure({ historyLength, historyChars })
56
+ && historyIndex < cutoff
57
+ && textChars > ASSISTANT_MESSAGE_MAX_CHARS;
48
58
  }
49
59
 
50
60
  function createAssistantMessagePreview(text) {
@@ -68,6 +78,7 @@ function createAssistantMessagePreview(text) {
68
78
  function shouldCompactHistoricalAssistantToolNarration(options = {}) {
69
79
  const historyIndex = Number(options.historyIndex);
70
80
  const historyLength = Number(options.historyLength);
81
+ const historyChars = Number(options.historyChars);
71
82
  const textChars = Number(options.textChars);
72
83
  const toolCallCount = Number(options.toolCallCount);
73
84
  const completedToolCallCount = Number(options.completedToolCallCount);
@@ -83,13 +94,18 @@ function shouldCompactHistoricalAssistantToolNarration(options = {}) {
83
94
  || toolCallCount <= 0
84
95
  ) return false;
85
96
  const cutoff = Math.max(0, historyLength - ASSISTANT_MESSAGE_KEEP_RECENT_MESSAGES);
86
- return historyIndex < cutoff
97
+ return hasHistoricalOffloadPressure({ historyLength, historyChars })
98
+ && historyIndex < cutoff
87
99
  && textChars > ASSISTANT_TOOL_NARRATION_MIN_CHARS
88
100
  && completedToolCallCount === toolCallCount;
89
101
  }
90
102
 
91
103
  function compactHistoricalPostTelegramReplyNarration(messages) {
92
104
  if (!Array.isArray(messages) || messages.length === 0) return messages;
105
+ if (!hasHistoricalOffloadPressure({
106
+ historyLength: messages.length,
107
+ historyChars: historyTextChars(messages),
108
+ })) return messages;
93
109
 
94
110
  const toolNameByCallId = new Map(messages.flatMap((message) => (
95
111
  message?.role === 'assistant' && Array.isArray(message.toolCalls)
@@ -175,6 +191,10 @@ function compactCompletedSubagentCall(call, completedIds) {
175
191
 
176
192
  function compactHistoricalCompletedSubagentPrompts(messages) {
177
193
  if (!Array.isArray(messages) || messages.length === 0) return messages;
194
+ if (!hasHistoricalOffloadPressure({
195
+ historyLength: messages.length,
196
+ historyChars: historyTextChars(messages),
197
+ })) return messages;
178
198
 
179
199
  const completedIds = completedSubagentResultIds(messages);
180
200
  if (completedIds.size === 0) return messages;
@@ -256,6 +276,7 @@ module.exports = {
256
276
  compactHistoricalCompletedSubagentPrompts,
257
277
  compactHistoricalPostTelegramReplyNarration,
258
278
  createAssistantMessagePreview,
279
+ historyTextChars,
259
280
  isSyntheticToolArguments,
260
281
  removeSyntheticToolArgumentFailures,
261
282
  shouldCompactHistoricalAssistantToolNarration,
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const { existsSync } = require('node:fs');
4
+ const { fileURLToPath } = require('node:url');
5
+ const { isAbsolute } = require('node:path');
6
+
7
+ function resolveCodebaseIndexScript(moduleUrl, envPath) {
8
+ if (typeof moduleUrl !== 'string' || moduleUrl.length === 0) return '';
9
+
10
+ if (typeof envPath === 'string' && envPath.length > 0) {
11
+ if (!isAbsolute(envPath) || !existsSync(envPath)) return '';
12
+ return envPath;
13
+ }
14
+
15
+ try {
16
+ const packaged = fileURLToPath(new URL('./codebase-index/codebase_index.py', moduleUrl));
17
+ return existsSync(packaged) ? packaged : '';
18
+ } catch {
19
+ return '';
20
+ }
21
+ }
22
+
23
+ module.exports = { resolveCodebaseIndexScript };
@@ -3,12 +3,9 @@
3
3
  const DEFAULT_COMPACTION_TRIGGER_MAX_TOKENS = 256_000;
4
4
  const DEFAULT_COMPACTION_BLOCK_MAX_TOKENS = 288_000;
5
5
 
6
- function capCompactionThreshold(threshold, phase) {
6
+ function capCompactionThreshold(threshold, _phase) {
7
7
  if (threshold === undefined) return undefined;
8
- const maximum = phase === "block"
9
- ? DEFAULT_COMPACTION_BLOCK_MAX_TOKENS
10
- : DEFAULT_COMPACTION_TRIGGER_MAX_TOKENS;
11
- return Math.min(threshold, maximum);
8
+ return threshold;
12
9
  }
13
10
 
14
11
  module.exports = {
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_CONTEXT_PRESSURE_RATIO = 0.75;
4
+
5
+ function hasContextPressure(options = {}) {
6
+ if (typeof options.underPressure === 'boolean') return options.underPressure;
7
+ const overridePercent = Number((options.env ?? process.env).BLUN_CONTEXT_PRESSURE_PERCENT);
8
+ const pressureRatio = Number.isFinite(overridePercent)
9
+ && overridePercent > 0
10
+ && overridePercent <= 100
11
+ ? overridePercent / 100
12
+ : DEFAULT_CONTEXT_PRESSURE_RATIO;
13
+ return !Number.isFinite(options.contextUsageRatio)
14
+ || options.contextUsageRatio >= pressureRatio;
15
+ }
16
+
17
+ module.exports = {
18
+ DEFAULT_CONTEXT_PRESSURE_RATIO,
19
+ hasContextPressure,
20
+ };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+
5
+ const DEFAULT_MAX_CHARS = 16_384;
6
+
7
+ function eventStepId(event, activeStepId) {
8
+ if (typeof event?.uuid === 'string') return event.uuid;
9
+ if (typeof event?.stepUuid === 'string') return event.stepUuid;
10
+ return activeStepId;
11
+ }
12
+
13
+ function readCronFinalMessage(wirePath, stepId, maxChars = DEFAULT_MAX_CHARS) {
14
+ if (typeof stepId !== 'string' || stepId.length === 0) return null;
15
+ if (!Number.isSafeInteger(maxChars) || maxChars < 1) throw new TypeError('maxChars must be positive');
16
+ let text = '';
17
+ let activeStepId = null;
18
+ const lines = fs.readFileSync(wirePath, 'utf8').split(/\r?\n/u);
19
+ for (const line of lines) {
20
+ if (line.length === 0) continue;
21
+ let record;
22
+ try {
23
+ record = JSON.parse(line);
24
+ } catch {
25
+ continue;
26
+ }
27
+ if (record?.type !== 'context.append_loop_event') continue;
28
+ const event = record.event;
29
+ if (event?.type === 'step.begin') {
30
+ activeStepId = event.uuid;
31
+ continue;
32
+ }
33
+ if (event?.type === 'step.end') {
34
+ if (eventStepId(event, activeStepId) === stepId) activeStepId = null;
35
+ continue;
36
+ }
37
+ if (event?.type !== 'content.part' || eventStepId(event, activeStepId) !== stepId) continue;
38
+ if (event.part?.type === 'text' && typeof event.part.text === 'string') text += event.part.text;
39
+ }
40
+ if (text.length === 0) return null;
41
+ if (text.length <= maxChars) return {text, truncated: false, chars: text.length};
42
+ return {text: `${text.slice(0, maxChars)}\n...[truncated]`, truncated: true, chars: text.length};
43
+ }
44
+
45
+ module.exports = {DEFAULT_MAX_CHARS, readCronFinalMessage};
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const crypto = require('node:crypto');
6
+ const {
7
+ isValidCronRunReceipt,
8
+ markCronJobDeleted,
9
+ transitionCronRunReceipt,
10
+ } = require('./scoped-cron-run-policy.cjs');
11
+
12
+ const RUN_ID_RE = /^[0-9a-f]{16}$/u;
13
+
14
+ function atomicWriteJson(filePath, value) {
15
+ fs.mkdirSync(path.dirname(filePath), {recursive: true, mode: 0o700});
16
+ const tempPath = `${filePath}.tmp.${process.pid}.${crypto.randomBytes(4).toString('hex')}`;
17
+ fs.writeFileSync(tempPath, `${JSON.stringify(value)}\n`, {encoding: 'utf8', mode: 0o600, flag: 'wx'});
18
+ fs.renameSync(tempPath, filePath);
19
+ }
20
+
21
+ function createCronRunStore(sessionDir, options = {}) {
22
+ const rootDir = path.join(sessionDir, 'cron-runs');
23
+ const now = options.now || (() => Date.now());
24
+ const runs = new Map();
25
+
26
+ function filePath(runId) {
27
+ if (!RUN_ID_RE.test(runId)) throw new TypeError('Invalid cron run id');
28
+ return path.join(rootDir, `${runId}.json`);
29
+ }
30
+
31
+ function persist(receipt) {
32
+ atomicWriteJson(filePath(receipt.runId), receipt);
33
+ }
34
+
35
+ function create(receipt) {
36
+ if (!isValidCronRunReceipt(receipt) || receipt.status !== 'buffered') {
37
+ throw new TypeError('Invalid new cron run receipt');
38
+ }
39
+ if (runs.has(receipt.runId)) throw new Error(`Cron run already exists: ${receipt.runId}`);
40
+ runs.set(receipt.runId, receipt);
41
+ persist(receipt);
42
+ return receipt;
43
+ }
44
+
45
+ function replace(receipt) {
46
+ if (!isValidCronRunReceipt(receipt)) throw new TypeError('Invalid cron run receipt');
47
+ if (!runs.has(receipt.runId)) throw new Error(`Unknown cron run: ${receipt.runId}`);
48
+ runs.set(receipt.runId, receipt);
49
+ persist(receipt);
50
+ return receipt;
51
+ }
52
+
53
+ function transition(runId, status, details) {
54
+ const current = runs.get(runId);
55
+ if (current === undefined) throw new Error(`Unknown cron run: ${runId}`);
56
+ return replace(transitionCronRunReceipt(current, status, details));
57
+ }
58
+
59
+ function markJobDeletedById(jobId) {
60
+ const changed = [];
61
+ for (const receipt of runs.values()) {
62
+ if (receipt.jobId !== jobId || receipt.jobDeleted === true) continue;
63
+ changed.push(replace(markCronJobDeleted(receipt)));
64
+ }
65
+ return changed;
66
+ }
67
+
68
+ function load() {
69
+ runs.clear();
70
+ if (!fs.existsSync(rootDir)) return {loaded: 0, lost: 0, lostRunIds: []};
71
+ let loaded = 0;
72
+ let lost = 0;
73
+ const lostRunIds = [];
74
+ for (const entry of fs.readdirSync(rootDir, {withFileTypes: true})) {
75
+ if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
76
+ const runId = entry.name.slice(0, -5);
77
+ if (!RUN_ID_RE.test(runId)) continue;
78
+ try {
79
+ const receipt = JSON.parse(fs.readFileSync(path.join(rootDir, entry.name), 'utf8'));
80
+ if (!isValidCronRunReceipt(receipt) || receipt.runId !== runId) continue;
81
+ let restored = receipt;
82
+ if (receipt.status === 'buffered' || receipt.status === 'running') {
83
+ restored = transitionCronRunReceipt(receipt, 'lost', {
84
+ endedAt: new Date(now()).toISOString(),
85
+ });
86
+ persist(restored);
87
+ lost += 1;
88
+ lostRunIds.push(runId);
89
+ }
90
+ runs.set(runId, restored);
91
+ loaded += 1;
92
+ } catch {}
93
+ }
94
+ return {loaded, lost, lostRunIds};
95
+ }
96
+
97
+ return Object.freeze({
98
+ create,
99
+ get: (runId) => runs.get(runId),
100
+ list: () => [...runs.values()],
101
+ load,
102
+ markJobDeletedById,
103
+ replace,
104
+ transition,
105
+ });
106
+ }
107
+
108
+ function createMemoryCronRunStore() {
109
+ const runs = new Map();
110
+ function replace(receipt) {
111
+ if (!isValidCronRunReceipt(receipt)) throw new TypeError('Invalid cron run receipt');
112
+ if (!runs.has(receipt.runId)) throw new Error(`Unknown cron run: ${receipt.runId}`);
113
+ runs.set(receipt.runId, receipt);
114
+ return receipt;
115
+ }
116
+ return Object.freeze({
117
+ create(receipt) {
118
+ if (!isValidCronRunReceipt(receipt) || receipt.status !== 'buffered') {
119
+ throw new TypeError('Invalid new cron run receipt');
120
+ }
121
+ if (runs.has(receipt.runId)) throw new Error(`Cron run already exists: ${receipt.runId}`);
122
+ runs.set(receipt.runId, receipt);
123
+ return receipt;
124
+ },
125
+ get: (runId) => runs.get(runId),
126
+ list: () => [...runs.values()],
127
+ load: () => ({loaded: 0, lost: 0, lostRunIds: []}),
128
+ markJobDeletedById(jobId) {
129
+ const changed = [];
130
+ for (const receipt of runs.values()) {
131
+ if (receipt.jobId !== jobId || receipt.jobDeleted === true) continue;
132
+ changed.push(replace(markCronJobDeleted(receipt)));
133
+ }
134
+ return changed;
135
+ },
136
+ replace,
137
+ transition(runId, status, details) {
138
+ const current = runs.get(runId);
139
+ if (current === undefined) throw new Error(`Unknown cron run: ${runId}`);
140
+ return replace(transitionCronRunReceipt(current, status, details));
141
+ },
142
+ });
143
+ }
144
+
145
+ module.exports = {createCronRunStore, createMemoryCronRunStore};
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+
5
+ const INTERRUPTED_TOOL_REMINDER = [
6
+ "The previous process ended during this agent's work.",
7
+ 'An in-flight tool call may have completed without its result being observed.',
8
+ 'Inspect current state before repeating any side effect.',
9
+ ].join(' ');
10
+
11
+ const TASK_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*-[0-9a-z]{8}$/u;
12
+ const DIGEST_PATTERN = /^[0-9a-f]{64}$/u;
13
+ const FAILURE_CATEGORIES = new Set(['validation', 'conflict', 'launcher', 'persistence', 'unknown']);
14
+ const INTENT_KEYS = new Set([
15
+ 'version',
16
+ 'status',
17
+ 'sourceTaskId',
18
+ 'replacementTaskId',
19
+ 'agentId',
20
+ 'resumeRootTaskId',
21
+ 'requestDigest',
22
+ 'createdAt',
23
+ 'startedAt',
24
+ ]);
25
+
26
+ function contractError(message, category = 'validation') {
27
+ const error = new Error(message);
28
+ error.code = 'ERR_DURABLE_TASK_RESUME';
29
+ error.category = FAILURE_CATEGORIES.has(category) ? category : 'unknown';
30
+ return error;
31
+ }
32
+
33
+ function assertTaskId(value, field = 'task ID') {
34
+ if (typeof value !== 'string' || !TASK_ID_PATTERN.test(value)) {
35
+ throw contractError(`${field} is invalid`);
36
+ }
37
+ return value;
38
+ }
39
+
40
+ function assertText(value, field, maxBytes) {
41
+ if (typeof value !== 'string' || value.trim().length === 0) {
42
+ throw contractError(`${field} is required`);
43
+ }
44
+ if (Buffer.byteLength(value, 'utf8') > maxBytes) {
45
+ throw contractError(`${field} is too large`);
46
+ }
47
+ return value;
48
+ }
49
+
50
+ function requestDigest(prompt, description) {
51
+ assertText(prompt, 'prompt', 32 * 1024);
52
+ assertText(description, 'description', 1024);
53
+ return crypto.createHash('sha256')
54
+ .update(JSON.stringify({ prompt, description }))
55
+ .digest('hex');
56
+ }
57
+
58
+ function isResumedSuccessor(task) {
59
+ return task.status === 'completed'
60
+ && task.resumed === true
61
+ && typeof task.replacesTaskId === 'string'
62
+ && typeof task.resumeRootTaskId === 'string';
63
+ }
64
+
65
+ function validateResumeSource(task) {
66
+ if (!task
67
+ || task.kind !== 'agent'
68
+ || typeof task.agentId !== 'string'
69
+ || task.agentId.length === 0
70
+ || (task.status !== 'lost' && !isResumedSuccessor(task))) {
71
+ throw contractError('Task is not a recoverable agent task. Only a lost agent task or a completed TaskResume successor can be resumed.');
72
+ }
73
+ assertTaskId(task.taskId);
74
+ return task;
75
+ }
76
+
77
+ function createPreparedIntent({ source, replacementTaskId, digest, now }) {
78
+ validateResumeSource(source);
79
+ assertTaskId(replacementTaskId, 'replacement task ID');
80
+ if (replacementTaskId === source.taskId) {
81
+ throw contractError('Replacement task ID must differ from source task ID', 'conflict');
82
+ }
83
+ if (typeof digest !== 'string' || !DIGEST_PATTERN.test(digest)) {
84
+ throw contractError('request digest is invalid');
85
+ }
86
+ if (!Number.isFinite(now)) throw contractError('intent timestamp is invalid');
87
+ return {
88
+ version: 1,
89
+ status: 'prepared',
90
+ sourceTaskId: source.taskId,
91
+ replacementTaskId,
92
+ agentId: source.agentId,
93
+ resumeRootTaskId: source.resumeRootTaskId || source.taskId,
94
+ requestDigest: digest,
95
+ createdAt: now,
96
+ };
97
+ }
98
+
99
+ function isResumeIntent(value) {
100
+ if (!value || typeof value !== 'object') return false;
101
+ if (Object.keys(value).some((key) => !INTENT_KEYS.has(key))) return false;
102
+ if (value.version !== 1 || (value.status !== 'prepared' && value.status !== 'started')) return false;
103
+ if (!TASK_ID_PATTERN.test(value.sourceTaskId) || !TASK_ID_PATTERN.test(value.replacementTaskId)) return false;
104
+ if (typeof value.agentId !== 'string' || value.agentId.length === 0) return false;
105
+ if (!TASK_ID_PATTERN.test(value.resumeRootTaskId) || !DIGEST_PATTERN.test(value.requestDigest)) return false;
106
+ if (!Number.isFinite(value.createdAt)) return false;
107
+ if (value.status === 'started' && !Number.isFinite(value.startedAt)) return false;
108
+ return true;
109
+ }
110
+
111
+ function decorateTaskInfo(task, intent) {
112
+ if (!task || !intent || intent.sourceTaskId !== task.taskId) return task;
113
+ return { ...task, replacementTaskId: intent.replacementTaskId };
114
+ }
115
+
116
+ function failureCategory(error) {
117
+ return FAILURE_CATEGORIES.has(error && error.category) ? error.category : 'launcher';
118
+ }
119
+
120
+ module.exports = {
121
+ INTERRUPTED_TOOL_REMINDER,
122
+ TASK_ID_PATTERN,
123
+ contractError,
124
+ createPreparedIntent,
125
+ decorateTaskInfo,
126
+ failureCategory,
127
+ isResumeIntent,
128
+ requestDigest,
129
+ validateResumeSource,
130
+ };
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ INTERRUPTED_TOOL_REMINDER,
5
+ contractError,
6
+ createPreparedIntent,
7
+ failureCategory,
8
+ requestDigest,
9
+ validateResumeSource,
10
+ } = require('./durable-task-resume-policy.cjs');
11
+
12
+ class DurableTaskResumeRuntime {
13
+ constructor(adapters) {
14
+ this.adapters = adapters;
15
+ this.inFlightSources = new Set();
16
+ }
17
+
18
+ async resolveIntent(source, digest, now) {
19
+ const existing = this.adapters.getIntent(source.taskId);
20
+ if (existing) {
21
+ const successor = this.adapters.getTask(existing.replacementTaskId);
22
+ if (successor || existing.status === 'started') {
23
+ throw contractError(`Task ${source.taskId} already has successor ${existing.replacementTaskId}`, 'conflict');
24
+ }
25
+ if (existing.requestDigest !== digest) {
26
+ throw contractError('Resume request differs from prepared intent', 'conflict');
27
+ }
28
+ return existing;
29
+ }
30
+ const replacementTaskId = this.adapters.reserveTaskId('agent');
31
+ const intent = createPreparedIntent({ source, replacementTaskId, digest, now });
32
+ await this.adapters.writeIntent(intent);
33
+ this.adapters.emitEvent({
34
+ type: 'background.task.resume_requested',
35
+ sourceTaskId: intent.sourceTaskId,
36
+ replacementTaskId: intent.replacementTaskId,
37
+ agentId: intent.agentId,
38
+ });
39
+ return intent;
40
+ }
41
+
42
+ async resume(input) {
43
+ const runSignal = input.controller ? input.controller.signal : input.signal;
44
+ runSignal.throwIfAborted();
45
+ if (this.inFlightSources.has(input.taskId)) {
46
+ throw contractError(`Task ${input.taskId} already has a resume request in flight`, 'conflict');
47
+ }
48
+ this.inFlightSources.add(input.taskId);
49
+ try {
50
+ const source = validateResumeSource(this.adapters.getTask(input.taskId));
51
+ const digest = requestDigest(input.prompt, input.description);
52
+ const now = this.adapters.now();
53
+ let intent;
54
+ try {
55
+ intent = await this.resolveIntent(source, digest, now);
56
+ runSignal.throwIfAborted();
57
+ const handle = await this.adapters.resumeAgent(intent.agentId, {
58
+ parentToolCallId: input.parentToolCallId,
59
+ prompt: input.prompt,
60
+ description: input.description,
61
+ runInBackground: true,
62
+ responseFormat: undefined,
63
+ toolScope: input.toolScope,
64
+ signal: runSignal,
65
+ systemReminder: INTERRUPTED_TOOL_REMINDER,
66
+ });
67
+ if (!handle || handle.agentId !== intent.agentId) {
68
+ throw contractError('Resume launcher changed the preserved agent identity', 'launcher');
69
+ }
70
+ const metadata = {
71
+ timeoutMinutes: input.timeoutMinutes,
72
+ parentToolCallId: input.parentToolCallId,
73
+ controller: input.controller,
74
+ replacesTaskId: intent.sourceTaskId,
75
+ resumeRootTaskId: intent.resumeRootTaskId,
76
+ };
77
+ const task = this.adapters.createTask(handle, input.description, metadata);
78
+ const registeredTaskId = this.adapters.registerTask(task, {
79
+ detached: true,
80
+ taskId: intent.replacementTaskId,
81
+ });
82
+ if (registeredTaskId !== intent.replacementTaskId) {
83
+ throw contractError('Background manager did not honor the reserved replacement task ID', 'launcher');
84
+ }
85
+ await this.adapters.awaitTaskPersisted(registeredTaskId);
86
+ await this.adapters.markIntentStarted(intent.sourceTaskId, now);
87
+ this.adapters.emitEvent({
88
+ type: 'background.task.resumed',
89
+ sourceTaskId: intent.sourceTaskId,
90
+ replacementTaskId: intent.replacementTaskId,
91
+ agentId: intent.agentId,
92
+ });
93
+ return {
94
+ taskId: registeredTaskId,
95
+ agentId: intent.agentId,
96
+ replacesTaskId: intent.sourceTaskId,
97
+ resumeRootTaskId: intent.resumeRootTaskId,
98
+ status: 'running',
99
+ };
100
+ } catch (error) {
101
+ if (intent) {
102
+ this.adapters.emitEvent({
103
+ type: 'background.task.resume_failed',
104
+ sourceTaskId: intent.sourceTaskId,
105
+ replacementTaskId: intent.replacementTaskId,
106
+ category: failureCategory(error),
107
+ });
108
+ }
109
+ throw error;
110
+ }
111
+ } finally {
112
+ this.inFlightSources.delete(input.taskId);
113
+ }
114
+ }
115
+ }
116
+
117
+ module.exports = { DurableTaskResumeRuntime };