blun-king-cli 9.1.424 → 9.1.426

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.
@@ -0,0 +1,443 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+
8
+ const { openCognitiveMemoryAdapter } = require('./cognitive-memory-adapter.cjs');
9
+ const { resolveCognitiveEvidenceGroup } = require('./cognitive-effective-view.cjs');
10
+ const { loadConfiguredCognitiveMemoryAdapter } = require('./cognitive-memory-provider.cjs');
11
+
12
+ const PACKAGE_VERSION = require('../package.json').version;
13
+ const SCHEMA = 'blun.cognitive-cross-portal-acceptance/v1';
14
+ const FIXED_TIME = '2031-04-05T09:00:00.000Z';
15
+ const TENANT_ID = 'tenant-v7';
16
+ const AGENT_ID = 'nova';
17
+ const USER_ID = 'mayk';
18
+ const PRIVATE_SCOPE = `relationship:${USER_ID}:private`;
19
+
20
+ function digest(value) {
21
+ return crypto.createHash('sha256').update(String(value)).digest('hex');
22
+ }
23
+
24
+ function requireCondition(condition, code) {
25
+ if (!condition) {
26
+ const error = new Error(code);
27
+ error.code = code;
28
+ throw error;
29
+ }
30
+ }
31
+
32
+ function addCheck(checks, id, detail, evidence) {
33
+ checks.push({
34
+ id,
35
+ status: 'PASS',
36
+ detail,
37
+ receipt: digest(`${SCHEMA}\0${id}\0${JSON.stringify(evidence)}`),
38
+ });
39
+ }
40
+
41
+ function identityLink({ portalId, actorId, method, authority, receiptId }) {
42
+ return {
43
+ version: 1,
44
+ tenantId: TENANT_ID,
45
+ canonicalUserId: USER_ID,
46
+ sourcePortal: portalId,
47
+ sourceSubjectId: actorId,
48
+ method,
49
+ status: 'active',
50
+ confirmedAt: FIXED_TIME,
51
+ authority,
52
+ receiptId,
53
+ };
54
+ }
55
+
56
+ function linkedAccess({ portalId, actorId, channelId, conversationId, method, authority, receiptId }) {
57
+ return {
58
+ tenantId: TENANT_ID,
59
+ userId: USER_ID,
60
+ actorId,
61
+ agentId: AGENT_ID,
62
+ portalId,
63
+ channelId,
64
+ conversationId,
65
+ requestedScope: PRIVATE_SCOPE,
66
+ permissionContext: {
67
+ authority: 'runtime_user_prompt_hook',
68
+ decision: 'passed',
69
+ receiptId: `operation-${receiptId}`,
70
+ identityReceiptId: receiptId,
71
+ },
72
+ identityLink: identityLink({ portalId, actorId, method, authority, receiptId }),
73
+ };
74
+ }
75
+
76
+ function directAccess({ agentId = AGENT_ID, userId, portalId, channelId, conversationId, requestedScope }) {
77
+ return {
78
+ tenantId: TENANT_ID,
79
+ userId,
80
+ actorId: userId,
81
+ agentId,
82
+ portalId,
83
+ channelId,
84
+ conversationId,
85
+ requestedScope,
86
+ permissionContext: {
87
+ authority: 'runtime_user_prompt_hook',
88
+ decision: 'passed',
89
+ receiptId: `operation-${conversationId}`,
90
+ },
91
+ };
92
+ }
93
+
94
+ function event({
95
+ agentId = AGENT_ID,
96
+ eventId,
97
+ expectedVersion,
98
+ occurredAt,
99
+ source,
100
+ observationId,
101
+ domain = 'open_thread',
102
+ key,
103
+ value,
104
+ scope,
105
+ supersedes,
106
+ withdraws,
107
+ retentionClass = 'relationship',
108
+ }) {
109
+ return {
110
+ tenantId: TENANT_ID,
111
+ agentId,
112
+ eventId,
113
+ expectedVersion,
114
+ occurredAt,
115
+ receivedAt: occurredAt,
116
+ retentionClass,
117
+ source,
118
+ observations: [{
119
+ observation_id: observationId,
120
+ domain,
121
+ key,
122
+ value,
123
+ confidence: 1,
124
+ epistemic_state: 'verified',
125
+ scope,
126
+ ...(supersedes ? { supersedes } : {}),
127
+ ...(withdraws ? { withdraws } : {}),
128
+ }],
129
+ };
130
+ }
131
+
132
+ function effectiveObservation(items, key, scope) {
133
+ const entries = items
134
+ .filter((item) => item.key === key && item.scope === scope)
135
+ .map((item, index) => ({
136
+ ...item,
137
+ observationId: item.observation_id,
138
+ occurredAt: Date.parse(item.occurred_at),
139
+ index,
140
+ }));
141
+ return resolveCognitiveEvidenceGroup(entries);
142
+ }
143
+
144
+ function sourceHashes(root, files) {
145
+ return Object.fromEntries(files.map((name) => [
146
+ name,
147
+ digest(fs.readFileSync(path.join(root, name))),
148
+ ]));
149
+ }
150
+
151
+ function renderCognitiveCrossPortalAcceptance(report) {
152
+ const lines = [`/memory acceptance ${report.ok ? 'PASS' : 'FAIL'} ${report.passed}/${report.total}`];
153
+ for (const check of report.checks) {
154
+ lines.push(`${check.id} ${check.status} receipt=${check.receipt.slice(0, 16)}`);
155
+ }
156
+ lines.push(`receipt=${report.receiptDigest}`);
157
+ lines.push('synthetic temporary state; evidence only; grants no authority');
158
+ return lines.join('\n');
159
+ }
160
+
161
+ function runCognitiveCrossPortalAcceptance() {
162
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'blun-v7-cross-portal-'));
163
+ const protectedFiles = ['SOUL.md', 'identity/manifest.json'];
164
+ let adapter;
165
+ try {
166
+ fs.mkdirSync(path.join(root, 'identity'), { recursive: true });
167
+ fs.writeFileSync(path.join(root, 'SOUL.md'), '# Nova\n\nWarm, precise, and honest.\n', 'utf8');
168
+ fs.writeFileSync(path.join(root, 'identity', 'manifest.json'), `${JSON.stringify({
169
+ version: 1,
170
+ tenant_id: TENANT_ID,
171
+ active_agent_id: AGENT_ID,
172
+ })}\n`, 'utf8');
173
+ const before = sourceHashes(root, protectedFiles);
174
+ adapter = openCognitiveMemoryAdapter({ home: root });
175
+ const checks = [];
176
+
177
+ const telegram = linkedAccess({
178
+ portalId: 'telegram',
179
+ actorId: 'telegram-42',
180
+ channelId: 'direct',
181
+ conversationId: 'dm-42',
182
+ method: 'explicit_confirmed',
183
+ authority: 'identity_confirmation_service',
184
+ receiptId: 'identity-telegram-42',
185
+ });
186
+ const web = linkedAccess({
187
+ portalId: 'web',
188
+ actorId: 'oauth-77',
189
+ channelId: 'chat',
190
+ conversationId: 'web-chat-77',
191
+ method: 'verified_oauth',
192
+ authority: 'oauth_verifier',
193
+ receiptId: 'identity-oauth-77',
194
+ });
195
+ const cli = linkedAccess({
196
+ portalId: 'cli',
197
+ actorId: 'cli-profile-7',
198
+ channelId: 'terminal',
199
+ conversationId: 'cli-session-7',
200
+ method: 'secure_account_link',
201
+ authority: 'account_link_service',
202
+ receiptId: 'identity-cli-profile-7',
203
+ });
204
+ const initialEvent = event({
205
+ eventId: 'event-telegram-topic',
206
+ expectedVersion: 0,
207
+ occurredAt: '2031-04-05T09:01:00.000Z',
208
+ source: {
209
+ provider: 'telegram',
210
+ channel_id: 'direct',
211
+ actor_id: 'telegram-42',
212
+ context_id: 'dm-42',
213
+ message_id: 'telegram-message-1',
214
+ },
215
+ observationId: 'observation-shared-topic-v1',
216
+ key: 'shared-topic',
217
+ value: 'continue after the measured result',
218
+ scope: PRIVATE_SCOPE,
219
+ });
220
+ const firstCommit = adapter.commit(initialEvent, telegram);
221
+ const fromWeb = adapter.read({ tenantId: TENANT_ID, agentId: AGENT_ID }, web);
222
+ const webTopic = fromWeb.observations.find((item) => item.observation_id === 'observation-shared-topic-v1');
223
+ requireCondition(webTopic?.source?.provider === 'telegram'
224
+ && webTopic.source.channel_id === 'direct', 'ACCEPTANCE_PORTAL_PROVENANCE_FAILED');
225
+ addCheck(checks, 'portal-provenance', 'Telegram provenance remains visible after a Web read.', [
226
+ webTopic.source.provider,
227
+ webTopic.source.channel_id,
228
+ webTopic.source.message_id,
229
+ ]);
230
+
231
+ const fromCli = adapter.read({ tenantId: TENANT_ID, agentId: AGENT_ID }, cli);
232
+ requireCondition(fromWeb.observations.length === 1 && fromCli.observations.length === 1,
233
+ 'ACCEPTANCE_LINKED_IDENTITY_FAILED');
234
+ addCheck(checks, 'linked-identity', 'One confirmed identity reads the same memory in Web and CLI.', [
235
+ fromWeb.observations[0].observation_id,
236
+ fromCli.observations[0].observation_id,
237
+ ]);
238
+
239
+ const unlinkedWeb = directAccess({
240
+ userId: 'oauth-77',
241
+ portalId: 'web',
242
+ channelId: 'chat',
243
+ conversationId: 'web-chat-unlinked',
244
+ requestedScope: 'relationship:oauth-77:private',
245
+ });
246
+ const unlinked = adapter.read({ tenantId: TENANT_ID, agentId: AGENT_ID }, unlinkedWeb);
247
+ requireCondition(unlinked.observations.length === 0, 'ACCEPTANCE_UNLINKED_ISOLATION_FAILED');
248
+ addCheck(checks, 'unlinked-isolation', 'A similarly named but unlinked Web identity sees no private memory.', 0);
249
+
250
+ const atlasAccess = directAccess({
251
+ agentId: 'atlas',
252
+ userId: 'atlas-user',
253
+ portalId: 'cli',
254
+ channelId: 'terminal',
255
+ conversationId: 'atlas-session',
256
+ requestedScope: 'user:atlas-user:private',
257
+ });
258
+ adapter.commit(event({
259
+ agentId: 'atlas',
260
+ eventId: 'event-atlas-only',
261
+ expectedVersion: 0,
262
+ occurredAt: '2031-04-05T09:02:00.000Z',
263
+ source: {
264
+ provider: 'cli',
265
+ channel_id: 'terminal',
266
+ actor_id: 'atlas-user',
267
+ context_id: 'atlas-session',
268
+ message_id: 'atlas-message-1',
269
+ },
270
+ observationId: 'observation-atlas-only',
271
+ key: 'agent-only',
272
+ value: 'atlas private state',
273
+ scope: 'user:atlas-user:private',
274
+ }), atlasAccess);
275
+ requireCondition(adapter.read({ tenantId: TENANT_ID, agentId: AGENT_ID }, web)
276
+ .observations.every((item) => item.observation_id !== 'observation-atlas-only'),
277
+ 'ACCEPTANCE_AGENT_ISOLATION_FAILED');
278
+ addCheck(checks, 'agent-isolation', 'A second agent stream remains outside Nova private reads.', 'atlas');
279
+
280
+ const nordAccess = directAccess({
281
+ userId: 'nord-member',
282
+ portalId: 'web',
283
+ channelId: 'group',
284
+ conversationId: 'group-nord',
285
+ requestedScope: 'team:nord',
286
+ });
287
+ const solAccess = directAccess({
288
+ userId: 'sol-member',
289
+ portalId: 'web',
290
+ channelId: 'group',
291
+ conversationId: 'group-sol',
292
+ requestedScope: 'team:sol',
293
+ });
294
+ adapter.commit(event({
295
+ eventId: 'event-nord-blocker',
296
+ expectedVersion: 1,
297
+ occurredAt: '2031-04-05T09:03:00.000Z',
298
+ source: {
299
+ provider: 'web',
300
+ channel_id: 'group',
301
+ actor_id: 'nord-member',
302
+ context_id: 'group-nord',
303
+ message_id: 'nord-message-1',
304
+ },
305
+ observationId: 'observation-nord-blocker',
306
+ domain: 'team',
307
+ key: 'group-blocker',
308
+ value: 'nord synthetic blocker',
309
+ scope: 'team:nord',
310
+ retentionClass: 'operational',
311
+ }), nordAccess);
312
+ const nord = adapter.read({ tenantId: TENANT_ID, agentId: AGENT_ID }, nordAccess);
313
+ const sol = adapter.read({ tenantId: TENANT_ID, agentId: AGENT_ID }, solAccess);
314
+ requireCondition(nord.observations.length === 1 && sol.observations.length === 0,
315
+ 'ACCEPTANCE_GROUP_ISOLATION_FAILED');
316
+ addCheck(checks, 'group-isolation', 'The Nord scope receives its blocker while the Sol scope remains empty.', [
317
+ nord.observations[0].observation_id,
318
+ sol.observations.length,
319
+ ]);
320
+
321
+ const replay = adapter.commit(initialEvent, telegram);
322
+ requireCondition(replay.idempotent === true && replay.version === firstCommit.version,
323
+ 'ACCEPTANCE_DEDUPLICATION_FAILED');
324
+ addCheck(checks, 'deduplication', 'Replaying the same portal event creates no second memory event.', [
325
+ replay.version,
326
+ replay.idempotent,
327
+ ]);
328
+
329
+ adapter.commit(event({
330
+ eventId: 'event-web-correction',
331
+ expectedVersion: 2,
332
+ occurredAt: '2031-04-05T09:04:00.000Z',
333
+ source: {
334
+ provider: 'web',
335
+ channel_id: 'chat',
336
+ actor_id: 'oauth-77',
337
+ context_id: 'web-chat-77',
338
+ message_id: 'web-message-2',
339
+ },
340
+ observationId: 'observation-shared-topic-v2',
341
+ key: 'shared-topic',
342
+ value: 'continue after the verified result',
343
+ scope: PRIVATE_SCOPE,
344
+ supersedes: 'observation-shared-topic-v1',
345
+ }), web);
346
+ const correctedCli = adapter.read({ tenantId: TENANT_ID, agentId: AGENT_ID }, cli);
347
+ const corrected = effectiveObservation(correctedCli.observations, 'shared-topic', PRIVATE_SCOPE);
348
+ requireCondition(corrected?.value === 'continue after the verified result'
349
+ && corrected.supersedes === 'observation-shared-topic-v1', 'ACCEPTANCE_GLOBAL_CORRECTION_FAILED');
350
+ addCheck(checks, 'global-correction', 'A Web correction becomes the effective CLI value and keeps history.', [
351
+ corrected.observationId,
352
+ corrected.supersedes,
353
+ correctedCli.observations.length,
354
+ ]);
355
+
356
+ adapter.commit(event({
357
+ eventId: 'event-cli-deletion',
358
+ expectedVersion: 3,
359
+ occurredAt: '2031-04-05T09:05:00.000Z',
360
+ source: {
361
+ provider: 'cli',
362
+ channel_id: 'terminal',
363
+ actor_id: 'cli-profile-7',
364
+ context_id: 'cli-session-7',
365
+ message_id: 'cli-message-3',
366
+ },
367
+ observationId: 'observation-shared-topic-deleted',
368
+ key: 'shared-topic',
369
+ value: 'withdrawn',
370
+ scope: PRIVATE_SCOPE,
371
+ withdraws: 'observation-shared-topic-v2',
372
+ }), cli);
373
+ const deletedWeb = adapter.read({ tenantId: TENANT_ID, agentId: AGENT_ID }, web);
374
+ requireCondition(effectiveObservation(deletedWeb.observations, 'shared-topic', PRIVATE_SCOPE) === null
375
+ && deletedWeb.observations.some((item) => item.withdraws === 'observation-shared-topic-v2'),
376
+ 'ACCEPTANCE_GLOBAL_DELETION_FAILED');
377
+ addCheck(checks, 'global-deletion', 'A CLI deletion removes the active Web value while retaining its tombstone.', [
378
+ deletedWeb.observations.length,
379
+ 'observation-shared-topic-deleted',
380
+ ]);
381
+
382
+ const beforeProviderFailure = adapter.read({ tenantId: TENANT_ID, agentId: AGENT_ID }, web);
383
+ let providerError = '';
384
+ try {
385
+ loadConfiguredCognitiveMemoryAdapter({
386
+ modulePath: path.join(root, 'missing-provider.cjs'),
387
+ home: root,
388
+ tenantId: TENANT_ID,
389
+ agentId: AGENT_ID,
390
+ agentName: 'Nova',
391
+ });
392
+ } catch (error) {
393
+ providerError = error?.code ?? '';
394
+ }
395
+ const afterProviderFailure = adapter.read({ tenantId: TENANT_ID, agentId: AGENT_ID }, web);
396
+ requireCondition(providerError === 'COGNITIVE_MEMORY_PROVIDER_PATH_INVALID'
397
+ && JSON.stringify(beforeProviderFailure) === JSON.stringify(afterProviderFailure),
398
+ 'ACCEPTANCE_PROVIDER_FAIL_CLOSED_FAILED');
399
+ addCheck(checks, 'provider-fail-closed', 'An unavailable provider fails before changing the existing memory stream.', [
400
+ providerError,
401
+ afterProviderFailure.version,
402
+ ]);
403
+
404
+ const after = sourceHashes(root, protectedFiles);
405
+ const novaVerification = adapter.verify({ tenantId: TENANT_ID, agentId: AGENT_ID }, web);
406
+ const atlasVerification = adapter.verify({ tenantId: TENANT_ID, agentId: 'atlas' }, atlasAccess);
407
+ requireCondition(JSON.stringify(before) === JSON.stringify(after)
408
+ && novaVerification.valid === true && atlasVerification.valid === true,
409
+ 'ACCEPTANCE_BYTE_PRESERVATION_FAILED');
410
+ addCheck(checks, 'byte-preservation', 'Synthetic SOUL and identity source bytes remain unchanged and both chains verify.', [
411
+ after,
412
+ novaVerification.valid,
413
+ atlasVerification.valid,
414
+ ]);
415
+
416
+ const report = {
417
+ schema: SCHEMA,
418
+ version: PACKAGE_VERSION,
419
+ scenarioId: 'v7-cross-portal-memory-v1',
420
+ ok: true,
421
+ passed: checks.length,
422
+ total: checks.length,
423
+ portals: ['cli', 'telegram', 'web'],
424
+ checks,
425
+ protectedSourceHashes: after,
426
+ receiptDigest: digest(checks.map((check) => `${check.id}:${check.receipt}`).join('\n')),
427
+ scope: 'synthetic-temporary-state',
428
+ authority: 'acceptance-evidence-only',
429
+ };
430
+ adapter.close();
431
+ adapter = undefined;
432
+ return report;
433
+ } finally {
434
+ try { adapter?.close(); } catch {}
435
+ fs.rmSync(root, { recursive: true, force: true });
436
+ }
437
+ }
438
+
439
+ module.exports = {
440
+ renderCognitiveCrossPortalAcceptance,
441
+ runCognitiveCrossPortalAcceptance,
442
+ };
443
+
@@ -3,6 +3,10 @@
3
3
  const crypto = require('node:crypto');
4
4
  const { createRuntimeCognitiveTurnLifecycle } = require('./cognitive-turn-lifecycle.cjs');
5
5
  const { resolveCognitiveEvidenceGroup } = require('./cognitive-effective-view.cjs');
6
+ const {
7
+ renderCognitiveCrossPortalAcceptance,
8
+ runCognitiveCrossPortalAcceptance,
9
+ } = require('./cognitive-cross-portal-acceptance.cjs');
6
10
 
7
11
  const FOCUS_DOMAINS = new Set(['self', 'team', 'goal', 'open_thread', 'next_trigger', 'expected_evidence']);
8
12
  const POSITIVE_ID_RE = /^[1-9]\d*$/u;
@@ -25,12 +29,15 @@ function digestId(prefix, values) {
25
29
 
26
30
  function isCognitiveMemoryCommand(args) {
27
31
  const action = String(args ?? '').trim().split(/\s+/u, 1)[0].toLowerCase();
28
- return action === 'focus' || action === 'correct' || action === 'delete';
32
+ return action === 'focus' || action === 'correct' || action === 'delete' || action === 'acceptance';
29
33
  }
30
34
 
31
35
  function parseCognitiveMemoryCommand(args) {
32
36
  const text = String(args ?? '').trim();
33
37
  if (text.toLowerCase() === 'focus') return { action: 'focus' };
38
+ if (/^acceptance(?:\s+--json)?$/iu.test(text)) {
39
+ return { action: 'acceptance', json: /\s+--json$/iu.test(text) };
40
+ }
34
41
  const correction = /^correct\s+(\S+)\s+(.+)$/iu.exec(text);
35
42
  if (correction) {
36
43
  const target = clean(correction[1], 128);
@@ -200,6 +207,10 @@ function runCognitiveMemoryCommand({
200
207
  lifecycleFactory = createRuntimeCognitiveTurnLifecycle,
201
208
  } = {}) {
202
209
  const command = parseCognitiveMemoryCommand(args);
210
+ if (command.action === 'acceptance') {
211
+ const report = runCognitiveCrossPortalAcceptance();
212
+ return command.json ? JSON.stringify(report) : renderCognitiveCrossPortalAcceptance(report);
213
+ }
203
214
  const telegram = normalizeTelegramSource(channelSource);
204
215
  const lifecycle = openLifecycle(env, lifecycleFactory);
205
216
  try {
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+
3
+ const MAX_SUBAGENT_HANDOFF_ATTEMPTS = 1;
4
+ const MAX_PARTIAL_HANDOFF_CHARS = 6000;
5
+ const SUBAGENT_MAX_TOKENS_ERROR =
6
+ "Subagent turn failed before completing its final summary: reason=max_tokens";
7
+ const SUBAGENT_MAX_TOKENS_HANDOFF_PROMPT = [
8
+ "Your previous turn reached max_tokens before its final handoff.",
9
+ "Do not call any tools and do not repeat completed work.",
10
+ "Return one concise recovery handoff of at most 1200 words with:",
11
+ "1. completed work and concrete findings",
12
+ "2. exact files changed or created",
13
+ "3. checks already run and their actual results",
14
+ "4. unfinished work or blockers",
15
+ "5. the exact next action for the parent or this same agent",
16
+ "State clearly that the task is incomplete when anything remains.",
17
+ ].join("\n");
18
+
19
+ function shouldRequestSubagentHandoff(stopReason, attempts) {
20
+ return stopReason === "max_tokens"
21
+ && Number.isSafeInteger(attempts)
22
+ && attempts >= 0
23
+ && attempts < MAX_SUBAGENT_HANDOFF_ATTEMPTS;
24
+ }
25
+
26
+ function boundedPartialHandoff(partialSummary) {
27
+ const normalized = typeof partialSummary === "string" ? partialSummary.trim() : "";
28
+ if (normalized.length <= MAX_PARTIAL_HANDOFF_CHARS) return normalized;
29
+ return `${normalized.slice(0, MAX_PARTIAL_HANDOFF_CHARS)}\n[partial handoff truncated]`;
30
+ }
31
+
32
+ function buildSubagentMaxTokensFailure(partialSummary) {
33
+ const partial = boundedPartialHandoff(partialSummary);
34
+ const lines = [
35
+ `${SUBAGENT_MAX_TOKENS_ERROR}.`,
36
+ "The automatic concise handoff also reached max_tokens.",
37
+ ];
38
+ if (partial.length > 0) {
39
+ lines.push("", "[partial_handoff]", partial, "[/partial_handoff]");
40
+ }
41
+ lines.push(
42
+ "",
43
+ "Resume the same subagent instead of starting the task again; its context and completed tool work are preserved.",
44
+ );
45
+ return lines.join("\n");
46
+ }
47
+
48
+ module.exports = {
49
+ MAX_PARTIAL_HANDOFF_CHARS,
50
+ MAX_SUBAGENT_HANDOFF_ATTEMPTS,
51
+ SUBAGENT_MAX_TOKENS_ERROR,
52
+ SUBAGENT_MAX_TOKENS_HANDOFF_PROMPT,
53
+ buildSubagentMaxTokensFailure,
54
+ shouldRequestSubagentHandoff,
55
+ };
package/blun.mjs CHANGED
@@ -252261,7 +252261,21 @@ async function runChildTurnToCompletion(child, signal) {
252261
252261
  if (typeof statusCode === "number") error.statusCode = statusCode;
252262
252262
  throw error;
252263
252263
  }
252264
- if (completion.stopReason === "max_tokens") throw new Error(`${SUBAGENT_MAX_TOKENS_ERROR}.`);
252264
+ return completion.stopReason;
252265
+ }
252266
+ async function completeChildTurnWithMaxTokensHandoff(child, signal) {
252267
+ let stopReason = await runChildTurnToCompletion(child, signal);
252268
+ let handoffAttempts = 0;
252269
+ while (shouldRequestSubagentHandoff(stopReason, handoffAttempts)) {
252270
+ handoffAttempts += 1;
252271
+ signal.throwIfAborted();
252272
+ if (child.turn.prompt([{
252273
+ type: "text",
252274
+ text: SUBAGENT_MAX_TOKENS_HANDOFF_PROMPT
252275
+ }], SUBAGENT_PROMPT_ORIGIN) === null) throw new Error("Subagent could not start its max_tokens recovery handoff.");
252276
+ stopReason = await runChildTurnToCompletion(child, signal);
252277
+ }
252278
+ if (stopReason === "max_tokens") throw new Error(buildSubagentMaxTokensFailure(lastAssistantText(child)));
252265
252279
  }
252266
252280
  function providerRateLimitErrorFromPayload(error) {
252267
252281
  const requestId = typeof error.details?.["requestId"] === "string" ? error.details["requestId"] : null;
@@ -252280,7 +252294,7 @@ function shouldSuppressQueuedAttemptFailureEvent(options, error) {
252280
252294
  if (isProviderRateLimitError(error)) return true;
252281
252295
  return isAbortError$4(error) || options.signal.aborted;
252282
252296
  }
252283
- var DEFAULT_SUBAGENT_TIMEOUT_MS, resolveSubagentTimeoutMs, runAgentWithTimeoutContinuation, buildSubagentUsageDelta, SUMMARY_MIN_LENGTH, SUMMARY_CONTINUATION_ATTEMPTS, HOOK_TEXT_PREVIEW_LENGTH, SUBAGENT_MAX_TOKENS_ERROR, TOOL_CALL_DISABLED_MESSAGE, SUBAGENT_PROMPT_ORIGIN, SIDE_QUESTION_SYSTEM_REMINDER, SessionSubagentHost;
252297
+ var DEFAULT_SUBAGENT_TIMEOUT_MS, resolveSubagentTimeoutMs, runAgentWithTimeoutContinuation, buildSubagentUsageDelta, SUBAGENT_MAX_TOKENS_ERROR, SUBAGENT_MAX_TOKENS_HANDOFF_PROMPT, buildSubagentMaxTokensFailure, shouldRequestSubagentHandoff, SUMMARY_MIN_LENGTH, SUMMARY_CONTINUATION_ATTEMPTS, HOOK_TEXT_PREVIEW_LENGTH, TOOL_CALL_DISABLED_MESSAGE, SUBAGENT_PROMPT_ORIGIN, SIDE_QUESTION_SYSTEM_REMINDER, SessionSubagentHost;
252284
252298
  var init_subagent_host = __esmMin((() => {
252285
252299
  init_src$4();
252286
252300
  init_errors$8();
@@ -252294,10 +252308,15 @@ var init_subagent_host = __esmMin((() => {
252294
252308
  init_summary_continuation();
252295
252309
  ({ DEFAULT_SUBAGENT_TIMEOUT_MS, resolveSubagentTimeoutMs, runAgentWithTimeoutContinuation } = createRequire(import.meta.url)("./bin/subagent-timeout-policy.cjs"));
252296
252310
  ({ buildSubagentUsageDelta } = createRequire(import.meta.url)("./bin/subagent-usage-rollup-policy.cjs"));
252311
+ ({
252312
+ SUBAGENT_MAX_TOKENS_ERROR,
252313
+ SUBAGENT_MAX_TOKENS_HANDOFF_PROMPT,
252314
+ buildSubagentMaxTokensFailure,
252315
+ shouldRequestSubagentHandoff
252316
+ } = createRequire(import.meta.url)("./bin/subagent-max-tokens-handoff-policy.cjs"));
252297
252317
  SUMMARY_MIN_LENGTH = 200;
252298
252318
  SUMMARY_CONTINUATION_ATTEMPTS = 1;
252299
252319
  HOOK_TEXT_PREVIEW_LENGTH = 500;
252300
- SUBAGENT_MAX_TOKENS_ERROR = "Subagent turn failed before completing its final summary: reason=max_tokens";
252301
252320
  TOOL_CALL_DISABLED_MESSAGE = "Tool calls are disabled for side questions. Answer with text only.";
252302
252321
  SUBAGENT_PROMPT_ORIGIN = {
252303
252322
  kind: "system_trigger",
@@ -252561,7 +252580,7 @@ IMPORTANT:
252561
252580
  return this.waitForChildCompletion(parent, childId, child, profileName, options);
252562
252581
  }
252563
252582
  async waitForChildCompletion(parent, childId, child, profileName, options) {
252564
- await runChildTurnToCompletion(child, options.signal);
252583
+ await completeChildTurnWithMaxTokensHandoff(child, options.signal);
252565
252584
  let result = lastAssistantText(child);
252566
252585
  let remainingContinuations = SUMMARY_CONTINUATION_ATTEMPTS;
252567
252586
  while (remainingContinuations > 0 && result.length < SUMMARY_MIN_LENGTH) {
@@ -252571,7 +252590,7 @@ IMPORTANT:
252571
252590
  type: "text",
252572
252591
  text: summary_continuation_default
252573
252592
  }], SUBAGENT_PROMPT_ORIGIN);
252574
- await runChildTurnToCompletion(child, options.signal);
252593
+ await completeChildTurnWithMaxTokensHandoff(child, options.signal);
252575
252594
  result = lastAssistantText(child);
252576
252595
  }
252577
252596
  const usage = child.usage.data().total;
@@ -252709,7 +252728,7 @@ function formatForegroundAgentFailure(handle, message, timedOut) {
252709
252728
  "",
252710
252729
  `subagent error: ${message}`
252711
252730
  ];
252712
- if (timedOut) lines.push(`resume_hint: Continue with Agent(resume="${handle.agentId}", prompt="continue"). Use agent_id only; do not set subagent_type. The subagent retains its prior context; redo any unfinished tool call if its result was lost.`);
252731
+ if (timedOut || message.includes(SUBAGENT_MAX_TOKENS_ERROR)) lines.push(`resume_hint: Continue with Agent(resume="${handle.agentId}", prompt="continue"). Use agent_id only; do not set subagent_type. The subagent retains its prior context; redo any unfinished tool call if its result was lost.`);
252713
252732
  return lines.join("\n");
252714
252733
  }
252715
252734
  function launchErrorMessage(error, signal) {
@@ -423338,7 +423357,7 @@ async function handleMemoryCommand(host, args, dependencies) {
423338
423357
  }
423339
423358
  const parsed = parseMemoryCommand(args);
423340
423359
  if (parsed === void 0) {
423341
- host.showStatus("/memory status|on|off|focus|correct <id> <value>|delete <id>");
423360
+ host.showStatus("/memory status|on|off|focus|correct <id> <value>|delete <id>|acceptance [--json]");
423342
423361
  return;
423343
423362
  }
423344
423363
  const client = (dependencies ?? defaultDependencies()).createClient(host);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.424",
3
+ "version": "9.1.426",
4
4
  "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
5
  "license": "MIT",
6
6
  "bin": {