blun-king-cli 9.1.523 → 9.1.525

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 (52) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LIESMICH.txt +4 -2
  3. package/README.md +4 -2
  4. package/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
  5. package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
  6. package/agent-spine-plugin/.codex-plugin/plugin.json +1 -2
  7. package/agent-spine-plugin/CHANGELOG.md +81 -14
  8. package/agent-spine-plugin/CONTRIBUTING.md +52 -0
  9. package/agent-spine-plugin/README.md +6 -4
  10. package/agent-spine-plugin/SECURITY.md +47 -0
  11. package/agent-spine-plugin/blun.plugin.json +1 -1
  12. package/agent-spine-plugin/docs/acceptance.md +2 -2
  13. package/agent-spine-plugin/docs/architecture.md +1 -1
  14. package/agent-spine-plugin/docs/gateway-runtime.md +2 -2
  15. package/agent-spine-plugin/docs/host-integration.md +2 -2
  16. package/agent-spine-plugin/docs/learning.md +37 -2
  17. package/agent-spine-plugin/docs/preflight-recall.md +1 -1
  18. package/agent-spine-plugin/docs/quality-gates.md +1 -1
  19. package/agent-spine-plugin/docs/relationships.md +1 -1
  20. package/agent-spine-plugin/docs/session-briefing.md +1 -1
  21. package/agent-spine-plugin/docs/source-roots.md +1 -1
  22. package/agent-spine-plugin/hooks/version.json +1 -1
  23. package/agent-spine-plugin/package.json +1 -1
  24. package/agent-spine-plugin/scripts/check-hosts.js +2 -2
  25. package/agent-spine-plugin/skills/agent-spine/SKILL.md +3 -3
  26. package/agent-spine-plugin/src/cli.js +69 -5
  27. package/agent-spine-plugin/src/hook.js +41 -23
  28. package/agent-spine-plugin/src/index.js +2 -1
  29. package/agent-spine-plugin/src/lib/acceptance.js +2 -3
  30. package/agent-spine-plugin/src/lib/attention.js +4 -4
  31. package/agent-spine-plugin/src/lib/audit.js +12 -3
  32. package/agent-spine-plugin/src/lib/authentication.js +4 -2
  33. package/agent-spine-plugin/src/lib/briefing.js +20 -8
  34. package/agent-spine-plugin/src/lib/catalog.js +28 -3
  35. package/agent-spine-plugin/src/lib/channel-runtime.js +2 -2
  36. package/agent-spine-plugin/src/lib/continuity.js +18 -13
  37. package/agent-spine-plugin/src/lib/documents.js +23 -5
  38. package/agent-spine-plugin/src/lib/feed-transport.js +4 -2
  39. package/agent-spine-plugin/src/lib/graph.js +47 -12
  40. package/agent-spine-plugin/src/lib/learning.js +422 -48
  41. package/agent-spine-plugin/src/lib/paths.js +50 -1
  42. package/agent-spine-plugin/src/lib/persona-runtime.js +20 -14
  43. package/agent-spine-plugin/src/lib/preflight.js +50 -26
  44. package/agent-spine-plugin/src/lib/source-roots.js +38 -7
  45. package/agent-spine-plugin/src/mcp.js +50 -4
  46. package/agent-spine-plugin/src/version.js +1 -1
  47. package/blun.mjs +91 -4
  48. package/package.json +6 -2
  49. package/scripts/check-bundled-agent-spine-regression.js +48 -0
  50. package/scripts/check-session-picker-resume-metrics-regression.js +97 -0
  51. package/scripts/check-shell-terminal-isolation-regression.js +81 -0
  52. package/scripts/check-telegram-loop-exactly-once-regression.js +71 -0
package/blun.mjs CHANGED
@@ -316411,7 +316411,7 @@ function buildLocalSpawnOptions(isWindows, cwd, env) {
316411
316411
  "pipe",
316412
316412
  "pipe"
316413
316413
  ],
316414
- detached: !isWindows,
316414
+ detached: true,
316415
316415
  windowsHide: true
316416
316416
  };
316417
316417
  }
@@ -424165,7 +424165,8 @@ async function handleLoopCommand(host, args) {
424165
424165
  host.setAppState?.({ loop });
424166
424166
  host.track("loop_start", { interval: loop.interval });
424167
424167
  host.showStatus(`/loop ${loop.status}`);
424168
- if (isStreaming(host) || host.state.appState.isCompacting) host.enqueueNormalUserInput(loop.prompt);
424168
+ if (host.telegramRemoteCommandContext !== void 0) host.sendNormalUserInput(loop.prompt);
424169
+ else if (isStreaming(host) || host.state.appState.isCompacting) host.enqueueNormalUserInput(loop.prompt);
424169
424170
  else host.sendNormalUserInput(loop.prompt);
424170
424171
  return;
424171
424172
  }
@@ -500660,6 +500661,10 @@ registerUiCatalogFragment({
500660
500661
  "sessionPicker.time.daysAgo": "{count}d ago",
500661
500662
  "sessionPicker.current": "current",
500662
500663
  "sessionPicker.imported": "imported",
500664
+ "sessionPicker.resume.metrics": "Resume {resumeSize} · {messages} messages · {tokens} tokens · Wire {wireSize}",
500665
+ "sessionPicker.resume.missing": "checkpoint missing",
500666
+ "sessionPicker.resume.stale": "checkpoint stale",
500667
+ "sessionPicker.resume.invalid": "checkpoint invalid",
500663
500668
  "sessionPicker.footer.matches": "Showing {from}-{to} of {loaded} loaded / {matches} matches",
500664
500669
  "sessionPicker.footer.sessions": "Showing {from}-{to} of {count} sessions",
500665
500670
  "sessionPicker.footer.loadedSessions": "Showing {from}-{to} of {loaded} loaded / {total} sessions"
@@ -500684,6 +500689,10 @@ registerUiCatalogFragment({
500684
500689
  "sessionPicker.time.daysAgo": "vor {count} T.",
500685
500690
  "sessionPicker.current": "aktuell",
500686
500691
  "sessionPicker.imported": "importiert",
500692
+ "sessionPicker.resume.metrics": "Resume {resumeSize} · {messages} Nachrichten · {tokens} Tokens · Wire {wireSize}",
500693
+ "sessionPicker.resume.missing": "Checkpoint fehlt",
500694
+ "sessionPicker.resume.stale": "Checkpoint veraltet",
500695
+ "sessionPicker.resume.invalid": "Checkpoint ungültig",
500687
500696
  "sessionPicker.footer.matches": "Anzeige {from}-{to} · {loaded} geladen · {matches} Treffer",
500688
500697
  "sessionPicker.footer.sessions": "Anzeige {from}-{to} von {count} Sitzungen",
500689
500698
  "sessionPicker.footer.loadedSessions": "Anzeige {from}-{to} · {loaded} von {total} Sitzungen geladen"
@@ -500708,6 +500717,10 @@ registerUiCatalogFragment({
500708
500717
  "sessionPicker.time.daysAgo": "hace {count} d",
500709
500718
  "sessionPicker.current": "actual",
500710
500719
  "sessionPicker.imported": "importada",
500720
+ "sessionPicker.resume.metrics": "Reanudación {resumeSize} · {messages} mensajes · {tokens} tokens · Wire {wireSize}",
500721
+ "sessionPicker.resume.missing": "Falta el punto de reanudación",
500722
+ "sessionPicker.resume.stale": "Punto de reanudación desactualizado",
500723
+ "sessionPicker.resume.invalid": "Punto de reanudación no válido",
500711
500724
  "sessionPicker.footer.matches": "Mostrando {from}-{to} · {loaded} cargadas · {matches} coincidencias",
500712
500725
  "sessionPicker.footer.sessions": "Mostrando {from}-{to} de {count} sesiones",
500713
500726
  "sessionPicker.footer.loadedSessions": "Mostrando {from}-{to} · {loaded} de {total} sesiones cargadas"
@@ -500732,6 +500745,10 @@ registerUiCatalogFragment({
500732
500745
  "sessionPicker.time.daysAgo": "il y a {count} j",
500733
500746
  "sessionPicker.current": "actuelle",
500734
500747
  "sessionPicker.imported": "importée",
500748
+ "sessionPicker.resume.metrics": "Reprise {resumeSize} · {messages} messages · {tokens} tokens · Wire {wireSize}",
500749
+ "sessionPicker.resume.missing": "Point de reprise manquant",
500750
+ "sessionPicker.resume.stale": "Point de reprise obsolète",
500751
+ "sessionPicker.resume.invalid": "Point de reprise non valide",
500735
500752
  "sessionPicker.footer.matches": "Affichage de {from}-{to} · {loaded} chargées · {matches} résultats",
500736
500753
  "sessionPicker.footer.sessions": "Affichage de {from}-{to} sur {count} sessions",
500737
500754
  "sessionPicker.footer.loadedSessions": "Affichage de {from}-{to} · {loaded} sessions chargées sur {total}"
@@ -500756,6 +500773,10 @@ registerUiCatalogFragment({
500756
500773
  "sessionPicker.time.daysAgo": "för {count} d sedan",
500757
500774
  "sessionPicker.current": "aktuell",
500758
500775
  "sessionPicker.imported": "importerad",
500776
+ "sessionPicker.resume.metrics": "Återupptagning {resumeSize} · {messages} meddelanden · {tokens} token · Wire {wireSize}",
500777
+ "sessionPicker.resume.missing": "Återupptagningspunkt saknas",
500778
+ "sessionPicker.resume.stale": "Återupptagningspunkt är inaktuell",
500779
+ "sessionPicker.resume.invalid": "Ogiltig återupptagningspunkt",
500759
500780
  "sessionPicker.footer.matches": "Visar {from}-{to} · {loaded} inlästa · {matches} träffar",
500760
500781
  "sessionPicker.footer.sessions": "Visar {from}-{to} av {count} sessioner",
500761
500782
  "sessionPicker.footer.loadedSessions": "Visar {from}-{to} · {loaded} av {total} sessioner inlästa"
@@ -500780,6 +500801,10 @@ registerUiCatalogFragment({
500780
500801
  "sessionPicker.time.daysAgo": "před {count} d",
500781
500802
  "sessionPicker.current": "aktuální",
500782
500803
  "sessionPicker.imported": "importováno",
500804
+ "sessionPicker.resume.metrics": "Obnovení {resumeSize} · {messages} zpráv · {tokens} tokenů · Wire {wireSize}",
500805
+ "sessionPicker.resume.missing": "Bod obnovení chybí",
500806
+ "sessionPicker.resume.stale": "Bod obnovení je zastaralý",
500807
+ "sessionPicker.resume.invalid": "Neplatný bod obnovení",
500783
500808
  "sessionPicker.footer.matches": "Zobrazeno {from}–{to}; načteno {loaded}, shod {matches}",
500784
500809
  "sessionPicker.footer.sessions": "Zobrazeno {from}–{to} z {count} relací",
500785
500810
  "sessionPicker.footer.loadedSessions": "Zobrazeno {from}–{to}; načteno {loaded} z {total} relací"
@@ -501034,6 +501059,21 @@ var SessionPickerComponent = class extends Container {
501034
501059
  const promptLine = indent + currentTheme.fg("textDim", promptMarker + promptText);
501035
501060
  card.push(promptLine);
501036
501061
  }
501062
+ const resume = session.resume;
501063
+ if (resume !== void 0) {
501064
+ if (resume.status === "missing") card.push(indent + currentTheme.fg("warning", uiText("sessionPicker.resume.missing")));
501065
+ else if (resume.status === "invalid") card.push(indent + currentTheme.fg("error", uiText("sessionPicker.resume.invalid")));
501066
+ else {
501067
+ const metrics = uiText("sessionPicker.resume.metrics", {
501068
+ resumeSize: formatByteSize(resume.resumeBytes),
501069
+ messages: resume.messages.toLocaleString(getCurrentUiLocale()),
501070
+ tokens: formatLiveTokenCount(resume.tokens),
501071
+ wireSize: formatByteSize(resume.wireBytes)
501072
+ });
501073
+ const suffix = resume.status === "stale" ? ` · ${uiText("sessionPicker.resume.stale")}` : "";
501074
+ card.push(indent + currentTheme.fg(resume.status === "stale" ? "warning" : "textMuted", truncateToWidth(metrics + suffix, Math.max(8, width - indentWidth), ELLIPSIS$4)));
501075
+ }
501076
+ }
501037
501077
  return card;
501038
501078
  }
501039
501079
  };
@@ -516682,9 +516722,56 @@ function sessionRowsForPicker(sessions, currentSessionId, currentSessionHasConte
516682
516722
  last_prompt: session.lastPrompt ?? null,
516683
516723
  work_dir: session.workDir,
516684
516724
  updated_at: session.updatedAt ?? session.createdAt ?? 0,
516685
- metadata: session.metadata
516725
+ metadata: session.metadata,
516726
+ session_dir: session.sessionDir
516686
516727
  }));
516687
516728
  }
516729
+ const SESSION_RESUME_METRICS_CONCURRENCY = 8;
516730
+ const SESSION_RESUME_STALE_AFTER_MS = 6e4;
516731
+ async function statSessionFile(filePath) {
516732
+ try {
516733
+ return await stat(filePath);
516734
+ } catch {
516735
+ return null;
516736
+ }
516737
+ }
516738
+ async function readSessionResumeMetrics(session) {
516739
+ if (typeof session.session_dir !== "string" || session.session_dir.length === 0) return { status: "missing" };
516740
+ const agentDir = join(session.session_dir, "agents", "main");
516741
+ const wirePath = join(agentDir, "wire.jsonl");
516742
+ const checkpointPath = join(agentDir, "resume-checkpoint.json");
516743
+ const [resumeStat, wireStat] = await Promise.all([
516744
+ statSessionFile(checkpointPath),
516745
+ statSessionFile(wirePath)
516746
+ ]);
516747
+ if (resumeStat === null) return { status: "missing" };
516748
+ if (wireStat === null) return { status: "invalid" };
516749
+ const checkpoint = await loadResumeCheckpoint({ wirePath }).catch(() => ({ ok: false }));
516750
+ if (!checkpoint.ok) return { status: "invalid" };
516751
+ const context = checkpoint.snapshot?.context;
516752
+ const messages = Array.isArray(context?.history) ? context.history.length : Number.isSafeInteger(context?.tokenCountCoveredMessageCount) ? context.tokenCountCoveredMessageCount : 0;
516753
+ const tokens = Number.isSafeInteger(context?.tokenCount) ? context.tokenCount : 0;
516754
+ const stale = wireStat.size > checkpoint.offset && wireStat.mtimeMs - resumeStat.mtimeMs > SESSION_RESUME_STALE_AFTER_MS;
516755
+ return {
516756
+ status: stale ? "stale" : "ok",
516757
+ resumeBytes: resumeStat.size,
516758
+ wireBytes: wireStat.size,
516759
+ messages,
516760
+ tokens
516761
+ };
516762
+ }
516763
+ async function enrichSessionRowsForPicker(rows) {
516764
+ let nextIndex = 0;
516765
+ const workerCount = Math.min(SESSION_RESUME_METRICS_CONCURRENCY, rows.length);
516766
+ await Promise.all(Array.from({ length: workerCount }, async () => {
516767
+ while (nextIndex < rows.length) {
516768
+ const index = nextIndex++;
516769
+ const row = rows[index];
516770
+ if (row !== void 0) row.resume = await readSessionResumeMetrics(row);
516771
+ }
516772
+ }));
516773
+ return rows;
516774
+ }
516688
516775
  //#endregion
516689
516776
  //#region src/tui/utils/startup.ts
516690
516777
  function combineStartupNotice(existing, next) {
@@ -519889,7 +519976,7 @@ var BlunTUI = class {
519889
519976
  this.state.sessionsScope = scope;
519890
519977
  try {
519891
519978
  const sessions = scope === "all" ? await this.harness.listSessions({}) : await this.harness.listSessions({ workDir: this.state.appState.workDir });
519892
- this.state.sessions = sessionRowsForPicker(sessions, this.state.appState.sessionId, this.hasSessionContent());
519979
+ this.state.sessions = await enrichSessionRowsForPicker(sessionRowsForPicker(sessions, this.state.appState.sessionId, this.hasSessionContent()));
519893
519980
  } catch {} finally {
519894
519981
  this.state.loadingSessions = false;
519895
519982
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.523",
3
+ "version": "9.1.525",
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": {
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "test": "node --test test/*.test.js",
12
- "prepack": "node scripts/check-release-metadata.js && node scripts/check-todo-loop-regression.js && node scripts/check-todo-recovery-catalog-regression.js && node scripts/check-historical-tool-result-preview-regression.js && node scripts/check-session-start-hook-context-regression.js && node scripts/check-queue-controls-regression.js && node scripts/check-approval-queue-shortcuts-regression.js && node scripts/check-approval-observability-regression.js && node scripts/check-slash-escape-regression.js && node scripts/check-telegram-bridge-watchdog.js && node scripts/check-resume-replay-regression.js && node scripts/check-session-cancel-regression.js && node scripts/check-plugin-startup-regression.js && node scripts/check-active-profile-plugin-startup.js && node scripts/check-mcp-startup-wait-budget.js",
12
+ "prepack": "node scripts/check-release-metadata.js && node scripts/check-shell-terminal-isolation-regression.js && node scripts/check-todo-loop-regression.js && node scripts/check-telegram-loop-exactly-once-regression.js && node scripts/check-session-picker-resume-metrics-regression.js && node scripts/check-bundled-agent-spine-regression.js && node scripts/check-todo-recovery-catalog-regression.js && node scripts/check-historical-tool-result-preview-regression.js && node scripts/check-session-start-hook-context-regression.js && node scripts/check-queue-controls-regression.js && node scripts/check-approval-queue-shortcuts-regression.js && node scripts/check-approval-observability-regression.js && node scripts/check-slash-escape-regression.js && node scripts/check-telegram-bridge-watchdog.js && node scripts/check-resume-replay-regression.js && node scripts/check-session-cancel-regression.js && node scripts/check-plugin-startup-regression.js && node scripts/check-active-profile-plugin-startup.js && node scripts/check-mcp-startup-wait-budget.js",
13
13
  "release:verify": "node scripts/check-release-metadata.js --external",
14
14
  "postinstall": "node scripts/fix-node-pty-perms.js"
15
15
  },
@@ -38,6 +38,7 @@
38
38
  "scripts/check-package-regression.js",
39
39
  "scripts/check-active-profile-plugin-startup.js",
40
40
  "scripts/check-approval-queue-shortcuts-regression.js",
41
+ "scripts/check-bundled-agent-spine-regression.js",
41
42
  "scripts/check-approval-observability-regression.js",
42
43
  "scripts/check-slash-escape-regression.js",
43
44
  "scripts/check-mcp-startup-wait-budget.js",
@@ -46,9 +47,12 @@
46
47
  "scripts/check-telegram-bridge-watchdog.js",
47
48
  "scripts/check-resume-replay-regression.js",
48
49
  "scripts/check-session-cancel-regression.js",
50
+ "scripts/check-shell-terminal-isolation-regression.js",
51
+ "scripts/check-session-picker-resume-metrics-regression.js",
49
52
  "scripts/check-release-metadata.js",
50
53
  "scripts/check-historical-tool-result-preview-regression.js",
51
54
  "scripts/check-session-start-hook-context-regression.js",
55
+ "scripts/check-telegram-loop-exactly-once-regression.js",
52
56
  "scripts/check-todo-recovery-catalog-regression.js",
53
57
  "scripts/check-todo-loop-regression.js",
54
58
  "scripts/fix-node-pty-perms.js",
@@ -0,0 +1,48 @@
1
+ const assert = require('node:assert/strict');
2
+ const { execFileSync } = require('node:child_process');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const expectedVersion = '0.11.4';
7
+ const pluginRoot = process.env.BLUN_AGENT_SPINE_ROOT_UNDER_TEST
8
+ ? path.resolve(process.env.BLUN_AGENT_SPINE_ROOT_UNDER_TEST)
9
+ : path.resolve(__dirname, '..', 'agent-spine-plugin');
10
+
11
+ function readJson(relativePath) {
12
+ return JSON.parse(fs.readFileSync(path.join(pluginRoot, relativePath), 'utf8'));
13
+ }
14
+
15
+ for (const relativePath of [
16
+ 'package.json',
17
+ 'hooks/version.json',
18
+ '.claude-plugin/plugin.json',
19
+ '.codex-plugin/plugin.json',
20
+ 'blun.plugin.json',
21
+ ]) {
22
+ assert.equal(
23
+ readJson(relativePath).version,
24
+ expectedVersion,
25
+ `BUNDLED_AGENT_SPINE_REGRESSION: ${relativePath} must declare ${expectedVersion}`,
26
+ );
27
+ }
28
+
29
+ const runtimeVersion = execFileSync(
30
+ process.execPath,
31
+ [path.join(pluginRoot, 'bin', 'agentspine.js'), '--version'],
32
+ { encoding: 'utf8' },
33
+ ).trim();
34
+ assert.equal(
35
+ runtimeVersion,
36
+ expectedVersion,
37
+ 'BUNDLED_AGENT_SPINE_REGRESSION: packaged runtime version is stale',
38
+ );
39
+
40
+ for (const relativePath of ['CONTRIBUTING.md', 'SECURITY.md']) {
41
+ assert.equal(
42
+ fs.existsSync(path.join(pluginRoot, relativePath)),
43
+ true,
44
+ `BUNDLED_AGENT_SPINE_REGRESSION: README link target ${relativePath} is missing`,
45
+ );
46
+ }
47
+
48
+ console.log(`bundled-agent-spine-regression PASS (${expectedVersion})`);
@@ -0,0 +1,97 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+
5
+ const bundlePath = process.env.BLUN_BUNDLE_UNDER_TEST
6
+ ? path.resolve(process.env.BLUN_BUNDLE_UNDER_TEST)
7
+ : path.join(__dirname, '..', 'blun.mjs');
8
+ const bundle = fs.readFileSync(bundlePath, 'utf8');
9
+
10
+ const rowsMatch = /function sessionRowsForPicker\(sessions, currentSessionId, currentSessionHasContent\) \{([\s\S]*?)\n\}/.exec(bundle);
11
+ assert(rowsMatch, 'SESSION_PICKER_RESUME_METRICS: sessionRowsForPicker is missing');
12
+ const sessionRowsForPicker = Function(
13
+ `return function sessionRowsForPicker(sessions, currentSessionId, currentSessionHasContent) {${rowsMatch[1]}\n};`,
14
+ )();
15
+
16
+ const rows = sessionRowsForPicker([{
17
+ id: 'session-a',
18
+ title: 'A',
19
+ lastPrompt: 'work',
20
+ workDir: 'C:/work',
21
+ updatedAt: 1,
22
+ metadata: {},
23
+ sessionDir: 'C:/state/session-a',
24
+ }], 'other', true);
25
+ assert.equal(rows[0].session_dir, 'C:/state/session-a');
26
+
27
+ const metricsMatch = /async function readSessionResumeMetrics\(session\) \{([\s\S]*?)\n\}\nasync function enrichSessionRowsForPicker/.exec(bundle);
28
+ assert(metricsMatch, 'SESSION_PICKER_RESUME_METRICS: readSessionResumeMetrics is missing');
29
+
30
+ function buildReader({ resumeStat, wireStat, checkpoint }) {
31
+ return Function(
32
+ 'join',
33
+ 'statSessionFile',
34
+ 'loadResumeCheckpoint',
35
+ 'SESSION_RESUME_STALE_AFTER_MS',
36
+ `return async function readSessionResumeMetrics(session) {${metricsMatch[1]}\n};`,
37
+ )(
38
+ path.join,
39
+ async (filePath) => filePath.endsWith('resume-checkpoint.json') ? resumeStat : wireStat,
40
+ async () => checkpoint,
41
+ 60_000,
42
+ );
43
+ }
44
+
45
+ (async () => {
46
+ const missing = await buildReader({ resumeStat: null, wireStat: null, checkpoint: null })({
47
+ session_dir: 'C:/state/session-a',
48
+ });
49
+ assert.deepEqual(missing, { status: 'missing' });
50
+
51
+ const invalid = await buildReader({
52
+ resumeStat: { size: 1_024, mtimeMs: 1_000 },
53
+ wireStat: { size: 2_048, mtimeMs: 1_000 },
54
+ checkpoint: { ok: false },
55
+ })({ session_dir: 'C:/state/session-a' });
56
+ assert.deepEqual(invalid, { status: 'invalid' });
57
+
58
+ const ok = await buildReader({
59
+ resumeStat: { size: 1_024, mtimeMs: 1_000 },
60
+ wireStat: { size: 2_048, mtimeMs: 1_500 },
61
+ checkpoint: {
62
+ ok: true,
63
+ offset: 2_048,
64
+ snapshot: { context: { history: [{}, {}, {}], tokenCount: 1_200 } },
65
+ },
66
+ })({ session_dir: 'C:/state/session-a' });
67
+ assert.deepEqual(ok, {
68
+ status: 'ok',
69
+ resumeBytes: 1_024,
70
+ wireBytes: 2_048,
71
+ messages: 3,
72
+ tokens: 1_200,
73
+ });
74
+
75
+ const stale = await buildReader({
76
+ resumeStat: { size: 1_024, mtimeMs: 1_000 },
77
+ wireStat: { size: 4_096, mtimeMs: 62_000 },
78
+ checkpoint: {
79
+ ok: true,
80
+ offset: 2_048,
81
+ snapshot: { context: { history: [{}], tokenCount: 500 } },
82
+ },
83
+ })({ session_dir: 'C:/state/session-a' });
84
+ assert.equal(stale.status, 'stale');
85
+ assert.equal(stale.wireBytes, 4_096);
86
+
87
+ const renderBlock = /const resume = session\.resume;([\s\S]*?)\n\t\treturn card;/.exec(bundle);
88
+ assert(renderBlock, 'SESSION_PICKER_RESUME_METRICS: resume metrics are not rendered');
89
+ assert.match(renderBlock[1], /formatByteSize\(resume\.resumeBytes\)/);
90
+ assert.match(renderBlock[1], /formatByteSize\(resume\.wireBytes\)/);
91
+ assert.match(renderBlock[1], /sessionPicker\.resume\.stale/);
92
+
93
+ console.log('session-picker-resume-metrics-regression PASS');
94
+ })().catch((error) => {
95
+ console.error(error);
96
+ process.exitCode = 1;
97
+ });
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const vm = require('node:vm');
7
+ const { spawn } = require('node:child_process');
8
+
9
+ const packageRoot = path.resolve(__dirname, '..');
10
+ const bundlePath = process.argv[2] === undefined
11
+ ? path.join(packageRoot, 'blun.mjs')
12
+ : path.resolve(process.argv[2]);
13
+ const bundle = fs.readFileSync(bundlePath, 'utf8');
14
+ const source = /function buildLocalSpawnOptions\(isWindows, cwd, env\) \{[\s\S]*?\n\}/.exec(bundle)?.[0];
15
+
16
+ assert(source, 'SHELL_TERMINAL_ISOLATION: buildLocalSpawnOptions is missing');
17
+
18
+ const context = {};
19
+ vm.runInNewContext(`${source}; globalThis.buildLocalSpawnOptions = buildLocalSpawnOptions;`, context);
20
+ const buildLocalSpawnOptions = context.buildLocalSpawnOptions;
21
+ const cwd = packageRoot;
22
+ const env = { ...process.env, BLUN_SHELL_ISOLATION_PROBE: '1' };
23
+
24
+ for (const isWindows of [false, true]) {
25
+ const options = buildLocalSpawnOptions(isWindows, cwd, env);
26
+ assert.deepEqual(Array.from(options.stdio), ['pipe', 'pipe', 'pipe']);
27
+ assert.equal(options.detached, true,
28
+ `local shell must start in a separate process group when isWindows=${String(isWindows)}`);
29
+ assert.equal(options.windowsHide, true);
30
+ assert.equal(options.cwd, cwd);
31
+ assert.equal(options.env, env);
32
+ }
33
+
34
+ async function runCapturedProcessProbe() {
35
+ const options = buildLocalSpawnOptions(process.platform === 'win32', cwd, env);
36
+ const child = spawn(process.execPath, [
37
+ '-e',
38
+ [
39
+ "process.stdin.setEncoding('utf8')",
40
+ "process.stdin.on('data', () => { process.exitCode = 90 })",
41
+ "process.stdin.on('end', () => {",
42
+ " process.stdout.write('stdout-ok')",
43
+ " process.stderr.write('stderr-ok')",
44
+ '})',
45
+ ].join(';'),
46
+ ], options);
47
+
48
+ let stdout = '';
49
+ let stderr = '';
50
+ child.stdout.setEncoding('utf8');
51
+ child.stderr.setEncoding('utf8');
52
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
53
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
54
+ child.stdin.end();
55
+
56
+ const exit = await new Promise((resolve, reject) => {
57
+ const timer = setTimeout(() => {
58
+ child.kill();
59
+ reject(new Error('captured child did not observe stdin EOF'));
60
+ }, 10_000);
61
+ child.once('error', (error) => {
62
+ clearTimeout(timer);
63
+ reject(error);
64
+ });
65
+ child.once('close', (code, signal) => {
66
+ clearTimeout(timer);
67
+ resolve({ code, signal });
68
+ });
69
+ });
70
+
71
+ assert.deepEqual(exit, { code: 0, signal: null });
72
+ assert.equal(stdout, 'stdout-ok');
73
+ assert.equal(stderr, 'stderr-ok');
74
+ }
75
+
76
+ runCapturedProcessProbe().then(() => {
77
+ process.stdout.write('shell-terminal-isolation-regression PASS\n');
78
+ }).catch((error) => {
79
+ console.error(error);
80
+ process.exitCode = 1;
81
+ });
@@ -0,0 +1,71 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+
5
+ const bundlePath = process.env.BLUN_BUNDLE_UNDER_TEST
6
+ ? path.resolve(process.env.BLUN_BUNDLE_UNDER_TEST)
7
+ : path.join(__dirname, '..', 'blun.mjs');
8
+ const bundle = fs.readFileSync(bundlePath, 'utf8');
9
+ const match = /async function handleLoopCommand\(host, args\) \{([\s\S]*?)\n\}\nfunction hasRunnableSession/.exec(bundle);
10
+ assert(match, 'TELEGRAM_LOOP_EXACTLY_ONCE: handleLoopCommand is missing');
11
+
12
+ const handleLoopCommand = Function(
13
+ 'parseLoopCommand',
14
+ 'hasRunnableSession',
15
+ 'llmNotSetMessage',
16
+ 'isStreaming',
17
+ `return async function handleLoopCommand(host, args) {${match[1]}\n};`,
18
+ )(
19
+ () => ({ kind: 'create', interval: '*/5 * * * *', prompt: 'work once' }),
20
+ () => true,
21
+ () => 'missing model',
22
+ (host) => host.streaming,
23
+ );
24
+
25
+ function createHost({ telegram = false, streaming = true } = {}) {
26
+ const calls = { send: [], enqueue: [] };
27
+ const session = {
28
+ async createLoop({ interval, prompt }) {
29
+ return { interval, prompt, status: 'active' };
30
+ },
31
+ };
32
+ const host = {
33
+ streaming,
34
+ session,
35
+ telegramRemoteCommandContext: telegram ? { item: { telegramCommandName: 'loop' } } : undefined,
36
+ state: { appState: { isCompacting: false, model: 'blun/king' } },
37
+ requireSession() { return session; },
38
+ setAppState() {},
39
+ track() {},
40
+ showStatus() {},
41
+ showError(message) { throw new Error(message); },
42
+ sendNormalUserInput(text) { calls.send.push(text); },
43
+ enqueueNormalUserInput(text) { calls.enqueue.push(text); },
44
+ };
45
+ return { host, calls };
46
+ }
47
+
48
+ (async () => {
49
+ const telegram = createHost({ telegram: true, streaming: true });
50
+ await handleLoopCommand(telegram.host, '5 min work once');
51
+ assert.deepEqual(telegram.calls.send, ['work once']);
52
+ assert.deepEqual(
53
+ telegram.calls.enqueue,
54
+ [],
55
+ 'TELEGRAM_LOOP_EXACTLY_ONCE: Telegram loop payload must not survive in the normal queue',
56
+ );
57
+
58
+ const local = createHost({ telegram: false, streaming: true });
59
+ await handleLoopCommand(local.host, '5 min work once');
60
+ assert.deepEqual(local.calls.send, []);
61
+ assert.deepEqual(
62
+ local.calls.enqueue,
63
+ ['work once'],
64
+ 'TELEGRAM_LOOP_EXACTLY_ONCE: local busy-loop behavior must stay queued',
65
+ );
66
+
67
+ console.log('telegram-loop-exactly-once-regression PASS');
68
+ })().catch((error) => {
69
+ console.error(error);
70
+ process.exitCode = 1;
71
+ });