blun-king-cli 9.1.536 → 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.
- package/LIESMICH.txt +13 -869
- package/README.md +41 -833
- package/bin/assistant-message-offload-policy.cjs +3 -1
- package/bin/context-performance-policy.cjs +2 -5
- package/bin/context-pressure-policy.cjs +20 -0
- package/bin/cron-run-output.cjs +45 -0
- package/bin/cron-run-store.cjs +145 -0
- package/bin/durable-task-resume-policy.cjs +130 -0
- package/bin/durable-task-resume-runtime.cjs +117 -0
- package/bin/durable-task-resume-store.cjs +88 -0
- package/bin/editable-tool-approval-policy.cjs +540 -0
- package/bin/editable-tool-approval-runtime.cjs +99 -0
- package/bin/html-to-research-markdown.cjs +146 -0
- package/bin/programmatic-tool-runtime.mjs +330 -4
- package/bin/read-continuation-policy.cjs +36 -5
- package/bin/scoped-cron-run-policy.cjs +358 -0
- package/bin/startup-preferences.cjs +3 -3
- package/bin/structured-agent-swarm-output.cjs +325 -0
- package/bin/subagent-context-fork-policy.cjs +155 -0
- package/bin/subagent-skill-policy.cjs +204 -0
- package/bin/telegram-approval-relay.cjs +2 -1
- package/bin/tool-result-offload-policy.cjs +14 -1
- package/bin/update-notice.js +14 -18
- package/bin/user-message-offload-policy.cjs +3 -1
- package/blun.mjs +1089 -477
- package/codebase-index/README.md +12 -0
- package/codebase-index/codebase_index.py +129 -18
- package/package.json +23 -58
- package/telegram-plugin/bin/telegram-typing-keepalive.cjs +89 -0
- package/telegram-plugin/dist/bridge.mjs +8 -1
- package/CHANGELOG.md +0 -321
- package/agent-spine-plugin/CHANGELOG.md +0 -406
- package/agent-spine-plugin/CONTRIBUTING.md +0 -52
- package/agent-spine-plugin/README.md +0 -344
- package/agent-spine-plugin/SECURITY.md +0 -47
- package/agent-spine-plugin/docs/acceptance.md +0 -61
- package/agent-spine-plugin/docs/architecture.md +0 -183
- package/agent-spine-plugin/docs/attention.md +0 -121
- package/agent-spine-plugin/docs/automatic-continuity.md +0 -79
- package/agent-spine-plugin/docs/channel-runtime.md +0 -92
- package/agent-spine-plugin/docs/coordination.md +0 -138
- package/agent-spine-plugin/docs/feed-transport.md +0 -99
- package/agent-spine-plugin/docs/gateway-runtime.md +0 -116
- package/agent-spine-plugin/docs/harness-reference.md +0 -45
- package/agent-spine-plugin/docs/host-integration.md +0 -129
- package/agent-spine-plugin/docs/https-transport.md +0 -116
- package/agent-spine-plugin/docs/learning.md +0 -133
- package/agent-spine-plugin/docs/object-transport.md +0 -93
- package/agent-spine-plugin/docs/peer-transport.md +0 -88
- package/agent-spine-plugin/docs/preflight-recall.md +0 -69
- package/agent-spine-plugin/docs/preservation-contract.md +0 -53
- package/agent-spine-plugin/docs/quality-gates.md +0 -50
- package/agent-spine-plugin/docs/relationships.md +0 -73
- package/agent-spine-plugin/docs/releasing.md +0 -83
- package/agent-spine-plugin/docs/roadmap.md +0 -307
- package/agent-spine-plugin/docs/selfstarter.md +0 -88
- package/agent-spine-plugin/docs/session-briefing.md +0 -74
- package/agent-spine-plugin/docs/shared-memory.md +0 -259
- package/agent-spine-plugin/docs/source-roots.md +0 -86
- package/agent-spine-plugin/docs/sqlite-transport.md +0 -76
- package/agent-spine-plugin/scripts/check-hosts.js +0 -195
- package/agent-spine-plugin/scripts/check-install.js +0 -569
- package/agent-spine-plugin/scripts/check-syntax.js +0 -29
- package/agent-spine-plugin/scripts/github-actions.js +0 -11
- package/agent-spine-plugin/scripts/release-check.js +0 -128
- package/agent-spine-plugin/scripts/run-acceptance.js +0 -19
- package/agent-spine-plugin/scripts/run-checks.js +0 -46
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +0 -73
- package/agent-spine-plugin/spine-example/1-identity.md +0 -12
- package/agent-spine-plugin/spine-example/2-voice.md +0 -6
- package/agent-spine-plugin/spine-example/3-conduct.md +0 -8
- package/agent-spine-plugin/spine-example/4-history.md +0 -4
- package/bin/fredrik-glm-provider.cjs +0 -256
- package/bin/package-regression-policy.cjs +0 -77
- package/fredrik-glm-profile.toml.example +0 -26
- package/release-planned-removals.json +0 -15
- package/scripts/check-active-profile-plugin-startup.js +0 -36
- package/scripts/check-active-work-steer-regression.js +0 -46
- package/scripts/check-approval-observability-regression.js +0 -111
- package/scripts/check-approval-queue-shortcuts-regression.js +0 -65
- package/scripts/check-bundled-agent-spine-regression.js +0 -48
- package/scripts/check-codebase-search-packaging-regression.js +0 -92
- package/scripts/check-copy-command-regression.js +0 -74
- package/scripts/check-current-turn-read-pin-mutation-regression.js +0 -72
- package/scripts/check-current-turn-read-pin-regression.js +0 -94
- package/scripts/check-deepseek-native-max-regression.js +0 -49
- package/scripts/check-empty-response-effort-downgrade-regression.js +0 -48
- package/scripts/check-fredrik-glm-mutation-regression.js +0 -18
- package/scripts/check-fredrik-glm-regression.js +0 -169
- package/scripts/check-historical-tool-result-preview-regression.js +0 -77
- package/scripts/check-history-pressure-offload-regression.js +0 -77
- package/scripts/check-mcp-startup-wait-budget.js +0 -48
- package/scripts/check-package-regression.js +0 -38
- package/scripts/check-plugin-startup-regression.js +0 -53
- package/scripts/check-programmatic-context-isolation-regression.js +0 -193
- package/scripts/check-programmatic-tool-regression.js +0 -294
- package/scripts/check-queue-controls-regression.js +0 -189
- package/scripts/check-release-metadata.js +0 -103
- package/scripts/check-reload-agent-spine-regression.js +0 -76
- package/scripts/check-resume-replay-regression.js +0 -102
- package/scripts/check-session-cancel-regression.js +0 -43
- package/scripts/check-session-picker-resume-metrics-regression.js +0 -97
- package/scripts/check-session-start-hook-context-regression.js +0 -228
- package/scripts/check-shell-terminal-isolation-regression.js +0 -81
- package/scripts/check-slash-escape-regression.js +0 -89
- package/scripts/check-startup-swarm-command-regression.js +0 -24
- package/scripts/check-structured-subagent-output-regression.js +0 -331
- package/scripts/check-telegram-bridge-watchdog.js +0 -60
- package/scripts/check-telegram-direct-work-resume-regression.js +0 -53
- package/scripts/check-telegram-loop-exactly-once-regression.js +0 -71
- package/scripts/check-todo-loop-regression.js +0 -78
- package/scripts/check-todo-progress-regression.js +0 -416
- package/scripts/check-todo-recovery-catalog-regression.js +0 -50
- package/scripts/check-tool-schema-capacity-regression.js +0 -40
- package/scripts/programmatic-tool-runtime.test.mjs +0 -365
- package/scripts/structured-subagent-output.test.cjs +0 -170
- /package/{scripts → bin}/fix-node-pty-perms.js +0 -0
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
|
|
6
|
-
const DEFAULT_MANIFEST_URL = 'https://chat.blun.ai/blun-code-version.json';
|
|
7
|
-
const DEFAULT_DOCS_URL = 'https://docs.blun.ai/changelog.html';
|
|
8
|
-
const DEFAULT_REGISTRY_URL = 'https://registry.npmjs.org';
|
|
9
|
-
const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
10
|
-
|
|
11
|
-
function assert(condition, message) {
|
|
12
|
-
if (!condition) throw new Error(message);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function readUtf8(filePath) {
|
|
16
|
-
return fs.readFileSync(filePath, 'utf8');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function firstChangelogVersion(changelog) {
|
|
20
|
-
return /^##\s+(\d+\.\d+\.\d+)\b/mu.exec(changelog)?.[1] || null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function checkLocalReleaseMetadata(root = path.resolve(__dirname, '..')) {
|
|
24
|
-
const packageJson = JSON.parse(readUtf8(path.join(root, 'package.json')));
|
|
25
|
-
const version = String(packageJson.version || '');
|
|
26
|
-
const packageName = String(packageJson.name || '');
|
|
27
|
-
assert(/^\d+\.\d+\.\d+$/u.test(version), 'RELEASE_VERSION_INVALID');
|
|
28
|
-
assert(packageName.length > 0, 'RELEASE_PACKAGE_NAME_MISSING');
|
|
29
|
-
|
|
30
|
-
const changelogVersion = firstChangelogVersion(readUtf8(path.join(root, 'CHANGELOG.md')));
|
|
31
|
-
assert(changelogVersion === version, `CHANGELOG_VERSION_MISMATCH:${changelogVersion || 'missing'}!=${version}`);
|
|
32
|
-
|
|
33
|
-
const installNeedle = `${packageName}@${version}`;
|
|
34
|
-
for (const fileName of ['README.md', 'LIESMICH.txt']) {
|
|
35
|
-
assert(readUtf8(path.join(root, fileName)).includes(installNeedle), `INSTALL_VERSION_MISMATCH:${fileName}`);
|
|
36
|
-
}
|
|
37
|
-
return { packageName, version };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function fetchLimited(url, kind) {
|
|
41
|
-
const response = await fetch(url, {
|
|
42
|
-
headers: { accept: kind === 'json' ? 'application/json' : 'text/html' },
|
|
43
|
-
redirect: 'follow',
|
|
44
|
-
signal: AbortSignal.timeout(15_000),
|
|
45
|
-
});
|
|
46
|
-
assert(response.ok, `RELEASE_FETCH_FAILED:${response.status}:${url}`);
|
|
47
|
-
const declaredLength = Number(response.headers.get('content-length'));
|
|
48
|
-
assert(!Number.isFinite(declaredLength) || declaredLength <= MAX_RESPONSE_BYTES, `RELEASE_RESPONSE_TOO_LARGE:${url}`);
|
|
49
|
-
const body = await response.text();
|
|
50
|
-
assert(Buffer.byteLength(body, 'utf8') <= MAX_RESPONSE_BYTES, `RELEASE_RESPONSE_TOO_LARGE:${url}`);
|
|
51
|
-
return kind === 'json' ? JSON.parse(body) : body;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function checkExternalReleaseMetadata(local, options = {}) {
|
|
55
|
-
const cacheBust = `cb=${Date.now()}`;
|
|
56
|
-
const registryUrl = options.registryUrl || DEFAULT_REGISTRY_URL;
|
|
57
|
-
const manifestUrl = `${options.manifestUrl || DEFAULT_MANIFEST_URL}?${cacheBust}`;
|
|
58
|
-
const docsUrl = `${options.docsUrl || DEFAULT_DOCS_URL}?${cacheBust}`;
|
|
59
|
-
const encodedName = encodeURIComponent(local.packageName);
|
|
60
|
-
const [tags, metadata, manifest, docs] = await Promise.all([
|
|
61
|
-
fetchLimited(`${registryUrl}/-/package/${encodedName}/dist-tags`, 'json'),
|
|
62
|
-
fetchLimited(`${registryUrl}/${encodedName}/${local.version}`, 'json'),
|
|
63
|
-
fetchLimited(manifestUrl, 'json'),
|
|
64
|
-
fetchLimited(docsUrl, 'html'),
|
|
65
|
-
]);
|
|
66
|
-
|
|
67
|
-
assert(tags.latest === local.version, `NPM_LATEST_MISMATCH:${tags.latest || 'missing'}!=${local.version}`);
|
|
68
|
-
assert(metadata.version === local.version, `NPM_VERSION_MISMATCH:${metadata.version || 'missing'}!=${local.version}`);
|
|
69
|
-
assert(manifest.name === local.packageName, `MANIFEST_PACKAGE_MISMATCH:${manifest.name || 'missing'}!=${local.packageName}`);
|
|
70
|
-
assert(manifest.latest === local.version, `MANIFEST_VERSION_MISMATCH:${manifest.latest || 'missing'}!=${local.version}`);
|
|
71
|
-
assert(typeof metadata.dist?.integrity === 'string' && metadata.dist.integrity.length > 0, 'NPM_INTEGRITY_MISSING');
|
|
72
|
-
assert(manifest.integrity === metadata.dist.integrity, 'MANIFEST_INTEGRITY_MISMATCH');
|
|
73
|
-
assert(docs.includes(`<title>BLUN King ${local.version} `), `PUBLIC_CHANGELOG_VERSION_MISMATCH:${local.version}`);
|
|
74
|
-
return { ...local, integrity: metadata.dist.integrity };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function parseArgs(argv) {
|
|
78
|
-
const external = argv.includes('--external');
|
|
79
|
-
const rootArg = argv.find((arg) => arg.startsWith('--root='));
|
|
80
|
-
return { external, root: rootArg ? path.resolve(rootArg.slice('--root='.length)) : undefined };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function main(argv = process.argv.slice(2)) {
|
|
84
|
-
const options = parseArgs(argv);
|
|
85
|
-
const local = checkLocalReleaseMetadata(options.root);
|
|
86
|
-
const result = options.external ? await checkExternalReleaseMetadata(local) : local;
|
|
87
|
-
process.stdout.write(`release-metadata PASS ${JSON.stringify(result)}\n`);
|
|
88
|
-
return result;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (require.main === module) {
|
|
92
|
-
main().catch((error) => {
|
|
93
|
-
process.stderr.write(`release-metadata BLOCK ${error.message}\n`);
|
|
94
|
-
process.exitCode = 1;
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
module.exports = {
|
|
99
|
-
checkExternalReleaseMetadata,
|
|
100
|
-
checkLocalReleaseMetadata,
|
|
101
|
-
firstChangelogVersion,
|
|
102
|
-
main,
|
|
103
|
-
};
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const assert = require('node:assert/strict');
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const os = require('node:os');
|
|
6
|
-
const path = require('node:path');
|
|
7
|
-
|
|
8
|
-
const rootArg = process.argv.find((value) => value.startsWith('--root='));
|
|
9
|
-
const packageRoot = rootArg ? path.resolve(rootArg.slice('--root='.length)) : path.resolve(__dirname, '..');
|
|
10
|
-
const bundle = fs.readFileSync(path.join(packageRoot, 'blun.mjs'), 'utf8');
|
|
11
|
-
const reloadStart = bundle.indexOf('async reloadSession(input) {');
|
|
12
|
-
const bootstrapCall = bundle.indexOf('installBundledAgentSpineForReload({ blunDir: this.homeDir });', reloadStart);
|
|
13
|
-
const pluginReload = bundle.indexOf('await this.reloadPlugins({});', reloadStart);
|
|
14
|
-
|
|
15
|
-
assert.notEqual(reloadStart, -1, 'reloadSession must exist');
|
|
16
|
-
assert.ok(
|
|
17
|
-
bootstrapCall > reloadStart && bootstrapCall < pluginReload,
|
|
18
|
-
'/reload must materialize bundled AgentSpine before reloading the plugin catalogue',
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
const helperPath = path.join(packageRoot, 'bin', 'reload-plugin-bootstrap.cjs');
|
|
22
|
-
assert.ok(fs.existsSync(helperPath), 'reload plugin bootstrap helper must be packaged');
|
|
23
|
-
const { installBundledAgentSpineForReload } = require(helperPath);
|
|
24
|
-
|
|
25
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'blun-reload-agent-spine-'));
|
|
26
|
-
const profile = path.join(root, 'profile');
|
|
27
|
-
const pluginsDir = path.join(profile, 'plugins');
|
|
28
|
-
const installedFile = path.join(pluginsDir, 'installed.json');
|
|
29
|
-
const unrelatedRoot = path.join(root, 'unrelated-plugin');
|
|
30
|
-
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
31
|
-
fs.mkdirSync(unrelatedRoot, { recursive: true });
|
|
32
|
-
fs.writeFileSync(installedFile, `${JSON.stringify({
|
|
33
|
-
version: 1,
|
|
34
|
-
plugins: [{
|
|
35
|
-
id: 'unrelated',
|
|
36
|
-
root: unrelatedRoot,
|
|
37
|
-
source: 'local-path',
|
|
38
|
-
enabled: false,
|
|
39
|
-
capabilities: {},
|
|
40
|
-
}],
|
|
41
|
-
}, null, 2)}\n`);
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const first = installBundledAgentSpineForReload({
|
|
45
|
-
packageRoot,
|
|
46
|
-
blunDir: profile,
|
|
47
|
-
now: '2026-09-01T00:00:00.000Z',
|
|
48
|
-
});
|
|
49
|
-
const installed = JSON.parse(fs.readFileSync(installedFile, 'utf8'));
|
|
50
|
-
const agentSpine = installed.plugins.find((plugin) => plugin.id === 'agent-spine');
|
|
51
|
-
const unrelated = installed.plugins.find((plugin) => plugin.id === 'unrelated');
|
|
52
|
-
|
|
53
|
-
assert.ok(agentSpine, 'reload bootstrap must add bundled AgentSpine');
|
|
54
|
-
assert.equal(agentSpine.root, first.pluginRoot);
|
|
55
|
-
assert.equal(agentSpine.enabled, true);
|
|
56
|
-
assert.equal(agentSpine.capabilities.mcpServers['agent-spine'].enabled, true);
|
|
57
|
-
assert.equal(unrelated.root, unrelatedRoot, 'reload bootstrap must preserve unrelated plugins');
|
|
58
|
-
assert.equal(unrelated.enabled, false, 'reload bootstrap must preserve unrelated plugin state');
|
|
59
|
-
assert.equal(
|
|
60
|
-
fs.readFileSync(path.join(agentSpine.root, 'package.json'), 'utf8'),
|
|
61
|
-
fs.readFileSync(path.join(packageRoot, 'agent-spine-plugin', 'package.json'), 'utf8'),
|
|
62
|
-
'managed AgentSpine must come from the current package bundle',
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
const beforeSecondRun = fs.readFileSync(installedFile, 'utf8');
|
|
66
|
-
const second = installBundledAgentSpineForReload({
|
|
67
|
-
packageRoot,
|
|
68
|
-
blunDir: profile,
|
|
69
|
-
now: '2026-09-01T00:01:00.000Z',
|
|
70
|
-
});
|
|
71
|
-
assert.equal(second.reused, true, 'an unchanged bundle must be reused');
|
|
72
|
-
assert.equal(fs.readFileSync(installedFile, 'utf8'), beforeSecondRun, 'idempotent reload must not rewrite installed.json');
|
|
73
|
-
process.stdout.write('Reload AgentSpine regression gate: PASS\n');
|
|
74
|
-
} finally {
|
|
75
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
76
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const fs = require('node:fs');
|
|
6
|
-
const path = require('node:path');
|
|
7
|
-
const { isWireOnlyProgrammaticEvent } = require('../bin/programmatic-context-isolation.cjs');
|
|
8
|
-
|
|
9
|
-
const bundlePath = process.env.BLUN_BUNDLE_UNDER_TEST
|
|
10
|
-
? path.resolve(process.env.BLUN_BUNDLE_UNDER_TEST)
|
|
11
|
-
: path.join(__dirname, '..', 'blun.mjs');
|
|
12
|
-
const bundle = fs.readFileSync(bundlePath, 'utf8');
|
|
13
|
-
|
|
14
|
-
function between(start, end) {
|
|
15
|
-
const startIndex = bundle.indexOf(start);
|
|
16
|
-
assert.notEqual(startIndex, -1, `missing bundle marker: ${start}`);
|
|
17
|
-
const endIndex = bundle.indexOf(end, startIndex + start.length);
|
|
18
|
-
assert.notEqual(endIndex, -1, `missing bundle marker: ${end}`);
|
|
19
|
-
return bundle.slice(startIndex, endIndex);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function compileMethod(name, nextName, dependencies = {}) {
|
|
23
|
-
const source = between(`\n\t\t${name}(`, `\n\t\t${nextName}(`).trim();
|
|
24
|
-
const names = Object.keys(dependencies);
|
|
25
|
-
const values = Object.values(dependencies);
|
|
26
|
-
return Function(...names, `"use strict"; return ({${source}}).${name};`)(...values);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function resolveStepEventUuid(openSteps, event) {
|
|
30
|
-
if (event.stepUuid !== undefined) return event.stepUuid;
|
|
31
|
-
let latest;
|
|
32
|
-
for (const uuid of openSteps.keys()) latest = uuid;
|
|
33
|
-
return latest;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function resolveCompletedStepUuid(openSteps, event) {
|
|
37
|
-
if (event.uuid !== undefined) return event.uuid;
|
|
38
|
-
let latest;
|
|
39
|
-
for (const uuid of openSteps.keys()) latest = uuid;
|
|
40
|
-
return latest;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const recoverMissingOpenStep = compileMethod(
|
|
44
|
-
'recoverMissingOpenStep',
|
|
45
|
-
'appendLoopEvent',
|
|
46
|
-
{ resolveStepEventUuid },
|
|
47
|
-
);
|
|
48
|
-
const appendLoopEvent = compileMethod(
|
|
49
|
-
'appendLoopEvent',
|
|
50
|
-
'appendMessage',
|
|
51
|
-
{
|
|
52
|
-
projectLoopEventForRecord: (event) => event,
|
|
53
|
-
isWireOnlyProgrammaticEvent,
|
|
54
|
-
resolveCompletedStepUuid,
|
|
55
|
-
createToolMessage: (toolCallId, content) => ({ toolCallId, content, toolCalls: [] }),
|
|
56
|
-
toolResultOutputForModel: (result) => result.output,
|
|
57
|
-
estimateTokensForMessages: () => 0,
|
|
58
|
-
},
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
const records = [];
|
|
62
|
-
const warnings = [];
|
|
63
|
-
const host = {
|
|
64
|
-
_history: [],
|
|
65
|
-
_tokenCount: 0,
|
|
66
|
-
tokenCountCoveredMessageCount: 0,
|
|
67
|
-
openSteps: new Map(),
|
|
68
|
-
pendingToolResultIds: new Set(),
|
|
69
|
-
agent: {
|
|
70
|
-
records: {
|
|
71
|
-
restoring: { time: Date.now() },
|
|
72
|
-
logRecord(record) { records.push(record); },
|
|
73
|
-
},
|
|
74
|
-
log: { warn(message, fields) { warnings.push({ message, fields }); } },
|
|
75
|
-
},
|
|
76
|
-
recoverMissingOpenStep,
|
|
77
|
-
pushHistory(...messages) { this._history.push(...messages); },
|
|
78
|
-
markPendingTokenEstimateDirty() {},
|
|
79
|
-
flushDeferredMessagesIfToolExchangeClosed() {},
|
|
80
|
-
closePendingToolResults() { return []; },
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// Exact shape measured after context.apply_compaction: no step.begin survives,
|
|
84
|
-
// then text, a tool call/result pair, and step.end continue the interrupted turn.
|
|
85
|
-
appendLoopEvent.call(host, { type: 'content.part', part: { type: 'text', text: 'continued after compaction' } });
|
|
86
|
-
appendLoopEvent.call(host, { type: 'tool.call', toolCallId: 'tool-1', name: 'Read', args: { path: 'SPEC.md' } });
|
|
87
|
-
appendLoopEvent.call(host, { type: 'tool.result', toolCallId: 'tool-1', result: { output: 'ok', isError: false } });
|
|
88
|
-
appendLoopEvent.call(host, { type: 'step.end', contextTokens: 100 });
|
|
89
|
-
|
|
90
|
-
assert.equal(warnings.length, 1, 'one missing step must produce one bounded repair warning');
|
|
91
|
-
assert.equal(warnings[0].fields.eventType, 'content.part');
|
|
92
|
-
assert.equal(warnings[0].fields.restoring, true);
|
|
93
|
-
assert.equal(host._history.length, 2, 'repair must retain the assistant continuation and its tool result');
|
|
94
|
-
assert.deepEqual(host._history[0].content, [{ type: 'text', text: 'continued after compaction' }]);
|
|
95
|
-
assert.equal(host._history[0].toolCalls.length, 1);
|
|
96
|
-
assert.equal(host._history[0].toolCalls[0].id, 'tool-1');
|
|
97
|
-
assert.equal(host._history[1].toolCallId, 'tool-1');
|
|
98
|
-
assert.equal(host.pendingToolResultIds.size, 0);
|
|
99
|
-
assert.equal(host.openSteps.size, 0);
|
|
100
|
-
assert.equal(records.filter((record) => record.type === 'context.append_loop_event').length, 4);
|
|
101
|
-
|
|
102
|
-
process.stdout.write('resume-replay-regression PASS\n');
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const fs = require('node:fs');
|
|
6
|
-
const path = require('node:path');
|
|
7
|
-
|
|
8
|
-
const bundlePath = process.env.BLUN_BUNDLE_UNDER_TEST
|
|
9
|
-
? path.resolve(process.env.BLUN_BUNDLE_UNDER_TEST)
|
|
10
|
-
: path.join(__dirname, '..', 'blun.mjs');
|
|
11
|
-
const bundle = fs.readFileSync(bundlePath, 'utf8');
|
|
12
|
-
|
|
13
|
-
function between(start, end) {
|
|
14
|
-
const startIndex = bundle.indexOf(start);
|
|
15
|
-
assert.notEqual(startIndex, -1, `missing bundle marker: ${start}`);
|
|
16
|
-
const endIndex = bundle.indexOf(end, startIndex + start.length);
|
|
17
|
-
assert.notEqual(endIndex, -1, `missing bundle marker: ${end}`);
|
|
18
|
-
return bundle.slice(startIndex, endIndex);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function assertSafeCancel(source, label) {
|
|
22
|
-
assert.match(source, /\.cancel\(\)\.catch\(\(error\)\s*=>/,
|
|
23
|
-
`${label} must observe the cancellation promise`);
|
|
24
|
-
assert.match(source, /error instanceof BlunError && error\.code === ErrorCodes\.SESSION_NOT_FOUND/,
|
|
25
|
-
`${label} must treat an already-missing session as cancelled`);
|
|
26
|
-
assert.match(source, /showError\(formatErrorMessage\$2\(error\)\)/,
|
|
27
|
-
`${label} must still surface non-session errors`);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
assertSafeCancel(
|
|
31
|
-
between('\n\tcancelCurrentStream() {', '\n\tcancelCurrentCompaction() {'),
|
|
32
|
-
'keyboard stream cancellation',
|
|
33
|
-
);
|
|
34
|
-
assertSafeCancel(
|
|
35
|
-
between('\n\tinjectTelegramRemoteCommand(', '\n\trunTelegramRemoteCommand('),
|
|
36
|
-
'preemptive Telegram reload cancellation',
|
|
37
|
-
);
|
|
38
|
-
assertSafeCancel(
|
|
39
|
-
between('\n\tenqueueSlashCommand(', '\n\tenqueueNormalUserInput('),
|
|
40
|
-
'preemptive slash-command cancellation',
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
process.stdout.write('session-cancel-regression PASS\n');
|
|
@@ -1,97 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const os = require('node:os');
|
|
6
|
-
const path = require('node:path');
|
|
7
|
-
const { spawnSync } = require('node:child_process');
|
|
8
|
-
|
|
9
|
-
const bundlePath = process.env.BLUN_BUNDLE_UNDER_TEST
|
|
10
|
-
? path.resolve(process.env.BLUN_BUNDLE_UNDER_TEST)
|
|
11
|
-
: path.resolve(__dirname, '..', 'blun.mjs');
|
|
12
|
-
const bundle = fs.readFileSync(bundlePath, 'utf8');
|
|
13
|
-
|
|
14
|
-
function assert(condition, message) {
|
|
15
|
-
if (!condition) throw new Error(message);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const renderer = bundle.match(
|
|
19
|
-
/function renderSessionStartHookResult\(results\) \{[\s\S]*?\n\}/,
|
|
20
|
-
);
|
|
21
|
-
const allowRenderer = bundle.match(
|
|
22
|
-
/function renderAllowHookResult\(event, results\) \{[\s\S]*?\n\}/,
|
|
23
|
-
);
|
|
24
|
-
const wrapperRenderer = bundle.match(
|
|
25
|
-
/function renderHookResult\(event, message\) \{[\s\S]*?\n\}/,
|
|
26
|
-
);
|
|
27
|
-
const messageSelector = bundle.match(
|
|
28
|
-
/function userPromptHookMessage\(result\) \{[\s\S]*?\n\}/,
|
|
29
|
-
);
|
|
30
|
-
const stringPredicate = bundle.match(
|
|
31
|
-
/function isNonEmptyString\$1\(value\) \{[\s\S]*?\n\}/,
|
|
32
|
-
);
|
|
33
|
-
const trigger = bundle.match(
|
|
34
|
-
/async triggerSessionStart\(source\) \{[\s\S]*?\n\t\t\}/,
|
|
35
|
-
);
|
|
36
|
-
const scheduler = bundle.match(
|
|
37
|
-
/scheduleSessionStart\(source\) \{[\s\S]*?\n\t\t\}/,
|
|
38
|
-
);
|
|
39
|
-
const createMain = bundle.match(
|
|
40
|
-
/async createMain\(\) \{[\s\S]*?\n\t\t\}/,
|
|
41
|
-
);
|
|
42
|
-
const resume = bundle.match(
|
|
43
|
-
/async resume\(\) \{[\s\S]*?\n\t\t\}/,
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
assert(renderer, 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart renderer is missing');
|
|
47
|
-
assert(allowRenderer, 'SESSION_START_HOOK_CONTEXT_REGRESSION: shared allow renderer is missing');
|
|
48
|
-
assert(wrapperRenderer, 'SESSION_START_HOOK_CONTEXT_REGRESSION: hook wrapper renderer is missing');
|
|
49
|
-
assert(messageSelector, 'SESSION_START_HOOK_CONTEXT_REGRESSION: hook message selector is missing');
|
|
50
|
-
assert(stringPredicate, 'SESSION_START_HOOK_CONTEXT_REGRESSION: hook string predicate is missing');
|
|
51
|
-
assert(trigger, 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart trigger is missing');
|
|
52
|
-
assert(scheduler, 'SESSION_START_HOOK_CONTEXT_REGRESSION: non-blocking SessionStart scheduler is missing');
|
|
53
|
-
assert(createMain, 'SESSION_START_HOOK_CONTEXT_REGRESSION: createMain is missing');
|
|
54
|
-
assert(resume, 'SESSION_START_HOOK_CONTEXT_REGRESSION: resume is missing');
|
|
55
|
-
assert(
|
|
56
|
-
/renderAllowHookResult\("SessionStart", results\)/.test(renderer[0]),
|
|
57
|
-
'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart output is not rendered',
|
|
58
|
-
);
|
|
59
|
-
assert(
|
|
60
|
-
/const results = await this\.hookEngine\.trigger\("SessionStart"/.test(trigger[0]),
|
|
61
|
-
'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart results are still discarded',
|
|
62
|
-
);
|
|
63
|
-
assert(
|
|
64
|
-
/renderSessionStartHookResult\(results\)/.test(trigger[0]),
|
|
65
|
-
'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart result does not reach the context path',
|
|
66
|
-
);
|
|
67
|
-
assert(
|
|
68
|
-
/appendSystemReminder\([\s\S]*variant: "session_start_hook"/.test(trigger[0]),
|
|
69
|
-
'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart result is not persisted as a scoped reminder',
|
|
70
|
-
);
|
|
71
|
-
assert(
|
|
72
|
-
/await mainAgent\.records\.flush\(\)/.test(trigger[0]),
|
|
73
|
-
'SESSION_START_HOOK_CONTEXT_REGRESSION: completed SessionStart reminder is not flushed',
|
|
74
|
-
);
|
|
75
|
-
assert(
|
|
76
|
-
/this\.scheduleSessionStart\("startup"\)/.test(createMain[0])
|
|
77
|
-
&& !/await this\.triggerSessionStart\("startup"\)/.test(createMain[0]),
|
|
78
|
-
'SESSION_START_HOOK_CONTEXT_REGRESSION: fresh-session input is still blocked by SessionStart',
|
|
79
|
-
);
|
|
80
|
-
assert(
|
|
81
|
-
/this\.scheduleSessionStart\("resume"\)/.test(resume[0])
|
|
82
|
-
&& !/await this\.triggerSessionStart\("resume"\)/.test(resume[0]),
|
|
83
|
-
'SESSION_START_HOOK_CONTEXT_REGRESSION: resumed-session input is still blocked by SessionStart',
|
|
84
|
-
);
|
|
85
|
-
assert(
|
|
86
|
-
/void this\.triggerSessionStart\(source\)\.catch\(/.test(scheduler[0]),
|
|
87
|
-
'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart is not detached from interactive startup',
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
const packageRoot = path.resolve(__dirname, '..');
|
|
91
|
-
const agentSpineRoot = path.join(packageRoot, 'agent-spine-plugin');
|
|
92
|
-
const agentSpineManifest = JSON.parse(
|
|
93
|
-
fs.readFileSync(path.join(agentSpineRoot, 'blun.plugin.json'), 'utf8'),
|
|
94
|
-
);
|
|
95
|
-
const agentSpineCodexHooks = JSON.parse(
|
|
96
|
-
fs.readFileSync(path.join(agentSpineRoot, 'hooks', 'codex.json'), 'utf8'),
|
|
97
|
-
);
|
|
98
|
-
const blunSessionStart = agentSpineManifest.hooks.find(
|
|
99
|
-
(hook) => hook.event === 'SessionStart',
|
|
100
|
-
);
|
|
101
|
-
const codexSessionStart = agentSpineCodexHooks.hooks.SessionStart?.[0]?.hooks?.[0];
|
|
102
|
-
|
|
103
|
-
assert(
|
|
104
|
-
blunSessionStart?.timeout >= 180,
|
|
105
|
-
'SESSION_START_HOOK_CONTEXT_REGRESSION: BLUN AgentSpine SessionStart timeout must cover the measured 123.6 s cold scan',
|
|
106
|
-
);
|
|
107
|
-
assert(
|
|
108
|
-
codexSessionStart?.timeout >= 180,
|
|
109
|
-
'SESSION_START_HOOK_CONTEXT_REGRESSION: Codex AgentSpine SessionStart timeout must match the BLUN runtime budget',
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
const probeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'blun-session-start-hook-'));
|
|
113
|
-
try {
|
|
114
|
-
fs.mkdirSync(path.join(probeRoot, '.git'));
|
|
115
|
-
fs.writeFileSync(
|
|
116
|
-
path.join(probeRoot, 'AGENTS.md'),
|
|
117
|
-
'# SessionStart probe\n\nLoad this project instruction automatically.\n',
|
|
118
|
-
);
|
|
119
|
-
const stateRoot = path.join(probeRoot, '.agentspine-state');
|
|
120
|
-
const hookRun = spawnSync(process.execPath, [path.join(agentSpineRoot, 'src', 'hook.js')], {
|
|
121
|
-
cwd: agentSpineRoot,
|
|
122
|
-
input: JSON.stringify({
|
|
123
|
-
hook_event_name: 'SessionStart',
|
|
124
|
-
session_id: 'session-start-regression',
|
|
125
|
-
cwd: probeRoot,
|
|
126
|
-
source: 'resume',
|
|
127
|
-
}),
|
|
128
|
-
encoding: 'utf8',
|
|
129
|
-
timeout: 30_000,
|
|
130
|
-
env: {
|
|
131
|
-
...process.env,
|
|
132
|
-
BLUN_HOME: probeRoot,
|
|
133
|
-
BLUN_PLUGIN_ROOT: agentSpineRoot,
|
|
134
|
-
AGENTSPINE_STATE_DIR: stateRoot,
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
assert(
|
|
138
|
-
hookRun.error === undefined && hookRun.status === 0,
|
|
139
|
-
`SESSION_START_HOOK_CONTEXT_REGRESSION: real AgentSpine hook failed: ${hookRun.error?.message || hookRun.stderr}`,
|
|
140
|
-
);
|
|
141
|
-
const hookOutput = JSON.parse(hookRun.stdout.trim());
|
|
142
|
-
assert(
|
|
143
|
-
hookOutput.hookSpecificOutput?.hookEventName === 'SessionStart'
|
|
144
|
-
&& hookOutput.hookSpecificOutput?.message?.includes('AgentSpine'),
|
|
145
|
-
`SESSION_START_HOOK_CONTEXT_REGRESSION: real AgentSpine process output is not injectable: ${hookRun.stdout.trim()}`,
|
|
146
|
-
);
|
|
147
|
-
} finally {
|
|
148
|
-
fs.rmSync(probeRoot, { recursive: true, force: true });
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const triggerFunction = trigger[0].replace(
|
|
152
|
-
/^async triggerSessionStart/,
|
|
153
|
-
'async function triggerSessionStart',
|
|
154
|
-
);
|
|
155
|
-
const schedulerFunction = scheduler[0].replace(
|
|
156
|
-
/^scheduleSessionStart/,
|
|
157
|
-
'function scheduleSessionStart',
|
|
158
|
-
);
|
|
159
|
-
const runTrigger = Function(
|
|
160
|
-
`${wrapperRenderer[0]}\n${messageSelector[0]}\n${stringPredicate[0]}\n${allowRenderer[0]}\n${renderer[0]}\n${triggerFunction}\nreturn triggerSessionStart;`,
|
|
161
|
-
)();
|
|
162
|
-
const scheduleTrigger = Function(
|
|
163
|
-
`${schedulerFunction}\nreturn scheduleSessionStart;`,
|
|
164
|
-
)();
|
|
165
|
-
|
|
166
|
-
(async () => {
|
|
167
|
-
const reminders = [];
|
|
168
|
-
let flushes = 0;
|
|
169
|
-
const fakeSession = {
|
|
170
|
-
hookEngine: {
|
|
171
|
-
async trigger(event, args) {
|
|
172
|
-
assert(event === 'SessionStart', 'runtime probe called the wrong hook event');
|
|
173
|
-
assert(args.matcherValue === 'resume', 'runtime probe lost the reload matcher');
|
|
174
|
-
return [{ action: 'allow', message: 'AgentSpine ready: 152 sources indexed.' }];
|
|
175
|
-
},
|
|
176
|
-
},
|
|
177
|
-
requireMainAgent() {
|
|
178
|
-
return {
|
|
179
|
-
context: {
|
|
180
|
-
appendSystemReminder(text, origin) {
|
|
181
|
-
reminders.push({ text, origin });
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
records: {
|
|
185
|
-
async flush() {
|
|
186
|
-
flushes += 1;
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
};
|
|
190
|
-
},
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
await runTrigger.call(fakeSession, 'resume');
|
|
194
|
-
assert(reminders.length === 1, 'runtime probe did not append exactly one reminder');
|
|
195
|
-
assert(flushes === 1, 'runtime probe did not flush exactly once');
|
|
196
|
-
assert(
|
|
197
|
-
reminders[0].text.includes('<hook_result hook_event="SessionStart">')
|
|
198
|
-
&& reminders[0].text.includes('AgentSpine ready: 152 sources indexed.'),
|
|
199
|
-
'runtime probe lost the AgentSpine SessionStart message',
|
|
200
|
-
);
|
|
201
|
-
assert(
|
|
202
|
-
reminders[0].origin?.kind === 'injection'
|
|
203
|
-
&& reminders[0].origin?.variant === 'session_start_hook',
|
|
204
|
-
'runtime probe used the wrong reminder origin',
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
let detachedCompleted = false;
|
|
208
|
-
const detachedStartedAt = Date.now();
|
|
209
|
-
scheduleTrigger.call({
|
|
210
|
-
async triggerSessionStart(source) {
|
|
211
|
-
assert(source === 'startup', 'detached runtime probe lost the startup source');
|
|
212
|
-
await new Promise((resolve) => setTimeout(resolve, 75));
|
|
213
|
-
detachedCompleted = true;
|
|
214
|
-
},
|
|
215
|
-
log: { warn() {} },
|
|
216
|
-
}, 'startup');
|
|
217
|
-
assert(
|
|
218
|
-
Date.now() - detachedStartedAt < 50 && detachedCompleted === false,
|
|
219
|
-
'SessionStart scheduler still waits for the hook before returning',
|
|
220
|
-
);
|
|
221
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
222
|
-
assert(detachedCompleted, 'detached SessionStart hook did not finish in the background');
|
|
223
|
-
|
|
224
|
-
process.stdout.write('session-start-hook-context-regression PASS\n');
|
|
225
|
-
})().catch((error) => {
|
|
226
|
-
process.stderr.write(`${error.stack || error.message}\n`);
|
|
227
|
-
process.exitCode = 1;
|
|
228
|
-
});
|