@north-light/crouter 0.3.221 → 0.3.223
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/dist/api/client.d.ts +21 -1
- package/dist/api/client.js +34 -0
- package/dist/api/dto/chat-inventory.d.ts +13 -0
- package/dist/api/dto/human-requests.d.ts +88 -0
- package/dist/api/dto/human-requests.js +4 -0
- package/dist/api/dto/human.d.ts +3 -0
- package/dist/api/dto/reviews.d.ts +2 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +7 -0
- package/dist/api/routes.js +10 -0
- package/dist/builtin-memory/00-runtime-base/00-authoring.md +31 -0
- package/dist/builtin-memory/00-runtime-base/01-escalation.md +14 -0
- package/dist/builtin-memory/{insights/listen.md → 00-runtime-base/02-insight-capture.md} +1 -0
- package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +27 -0
- package/dist/builtin-memory/{02-lifecycle/01-resident.md → 02-turn-lifecycle/02-resident.md} +5 -0
- package/dist/builtin-memory/04-base-worker.md +4 -8
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +1 -0
- package/dist/builtin-memory/05-kinds/design/00-base.md +2 -1
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +2 -1
- package/dist/builtin-memory/05-kinds/design/design-contract.md +19 -0
- package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/general/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/00-base.md +2 -1
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +2 -1
- package/dist/builtin-memory/05-kinds/plan/plan-contract.md +28 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/lens-contract.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -0
- package/dist/builtin-memory/05-kinds/review/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/review/companion/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/review/security-findings.md +1 -0
- package/dist/builtin-memory/05-kinds/spec/00-base.md +4 -3
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -0
- package/dist/builtin-memory/design/guide.md +35 -0
- package/dist/builtin-memory/design/roadmap.md +21 -0
- package/dist/builtin-memory/insights/capture.md +1 -1
- package/dist/builtin-memory/internal/memory-loading.md +4 -4
- package/dist/builtin-memory/internal/plugins.md +10 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/plan/roadmap.md +6 -28
- package/dist/builtin-memory/spec/guide.md +19 -8
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +28 -15
- package/dist/clients/attach/render/markdown-source.js +106 -1
- package/dist/clients/attach/session/file-links.d.ts +13 -4
- package/dist/clients/attach/session/file-links.js +54 -58
- package/dist/clients/attach/viewer.js +545 -543
- package/dist/clients/inbox/controller.js +1 -1
- package/dist/clients/inbox/page-adapter.js +2 -1
- package/dist/clients/inbox/resolve.d.ts +1 -0
- package/dist/clients/inbox/review/review-client.js +3 -1
- package/dist/clients/inbox/tui/panel.js +23 -4
- package/dist/clients/inbox/tui/render.js +6 -0
- package/dist/clients/inbox/tui/types.d.ts +8 -0
- package/dist/commands/__tests__/human.test.js +2 -2
- package/dist/commands/human/request.d.ts +2 -0
- package/dist/commands/human/request.js +281 -0
- package/dist/commands/human.js +5 -2
- package/dist/commands/memory/shared.d.ts +2 -2
- package/dist/commands/memory/shared.js +11 -6
- package/dist/commands/pkg/browse/doc-view.js +2 -0
- package/dist/commands/sys/config.js +2 -2
- package/dist/commands/sys/doctor.js +54 -2
- package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +7 -4
- package/dist/core/__tests__/fixtures/memory-slash-live-probe.d.ts +1 -0
- package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +71 -0
- package/dist/core/__tests__/human-action-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/human-action-delivery.test.js +140 -0
- package/dist/core/__tests__/human-actions.test.d.ts +1 -0
- package/dist/core/__tests__/human-actions.test.js +116 -0
- package/dist/core/__tests__/inline-memory-refs.test.js +1 -1
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +70 -3
- package/dist/core/__tests__/prospective-inventory-capability-parity.test.d.ts +1 -0
- package/dist/core/__tests__/prospective-inventory-capability-parity.test.js +91 -0
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.d.ts +1 -0
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +127 -0
- package/dist/core/__tests__/seam/prospective-inventory-stdout.test.d.ts +1 -0
- package/dist/core/__tests__/seam/prospective-inventory-stdout.test.js +31 -0
- package/dist/core/canvas/db.js +23 -0
- package/dist/core/canvas/human-deliveries.d.ts +53 -0
- package/dist/core/canvas/human-deliveries.js +75 -0
- package/dist/core/config.d.ts +13 -1
- package/dist/core/config.js +51 -1
- package/dist/core/feed/inbox.d.ts +6 -0
- package/dist/core/feed/inbox.js +9 -1
- package/dist/core/human/action-binding.d.ts +21 -0
- package/dist/core/human/action-binding.js +40 -0
- package/dist/core/human/completion.d.ts +38 -0
- package/dist/core/human/completion.js +27 -0
- package/dist/core/human/convention.d.ts +2 -0
- package/dist/core/human/convention.js +2 -0
- package/dist/core/human/tickets.d.ts +25 -6
- package/dist/core/human/tickets.js +19 -13
- package/dist/core/human/types.d.ts +5 -0
- package/dist/core/human-actions.d.ts +25 -0
- package/dist/core/human-actions.js +101 -0
- package/dist/core/memory-resolver.js +1 -1
- package/dist/core/profiles/select.d.ts +2 -0
- package/dist/core/profiles/select.js +21 -4
- package/dist/core/runtime/broker/frame-dispatch.js +2 -5
- package/dist/core/runtime/broker-inventory.d.ts +1 -2
- package/dist/core/runtime/broker-inventory.js +2 -77
- package/dist/core/runtime/broker-persona-guidance.js +1 -1
- package/dist/core/runtime/broker.js +4 -4
- package/dist/core/runtime/chat-inventory-rows.d.ts +8 -0
- package/dist/core/runtime/chat-inventory-rows.js +105 -0
- package/dist/core/runtime/command-surface.d.ts +8 -3
- package/dist/core/runtime/command-surface.js +42 -6
- package/dist/core/runtime/launch-target.d.ts +25 -0
- package/dist/core/runtime/launch-target.js +54 -0
- package/dist/core/runtime/persona.js +3 -3
- package/dist/core/runtime/prospective-inventory-cli.d.ts +1 -0
- package/dist/core/runtime/prospective-inventory-cli.js +61 -0
- package/dist/core/runtime/prospective-inventory.d.ts +10 -0
- package/dist/core/runtime/prospective-inventory.js +88 -0
- package/dist/core/runtime/spawn.d.ts +3 -1
- package/dist/core/runtime/spawn.js +5 -3
- package/dist/core/substrate/frontmatter-validation.d.ts +2 -5
- package/dist/core/substrate/frontmatter-validation.js +8 -4
- package/dist/core/substrate/gate.d.ts +4 -1
- package/dist/core/substrate/gate.js +5 -0
- package/dist/core/substrate/on-read.js +21 -32
- package/dist/core/substrate/render-node.d.ts +3 -2
- package/dist/core/substrate/render-node.js +3 -2
- package/dist/core/substrate/render.js +64 -37
- package/dist/core/substrate/schema.d.ts +17 -8
- package/dist/core/substrate/schema.js +14 -10
- package/dist/core/substrate/surface-match.d.ts +8 -7
- package/dist/core/substrate/surface-match.js +15 -14
- package/dist/core/user-settings.d.ts +4 -0
- package/dist/core/user-settings.js +1 -0
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +52 -3
- package/dist/daemon/api/handlers/human-requests.d.ts +2 -0
- package/dist/daemon/api/handlers/human-requests.js +409 -0
- package/dist/daemon/api/handlers/human.js +3 -0
- package/dist/daemon/api/handlers/inbox.js +3 -0
- package/dist/daemon/api/handlers/nodes.d.ts +1 -3
- package/dist/daemon/api/handlers/nodes.js +11 -46
- package/dist/daemon/api/handlers/prospective-chat-inventory.d.ts +2 -0
- package/dist/daemon/api/handlers/prospective-chat-inventory.js +59 -0
- package/dist/daemon/api/handlers/reviews.js +10 -2
- package/dist/daemon/api/server.js +4 -0
- package/dist/daemon/crtrd.js +6 -0
- package/dist/daemon/human/deliver-action.d.ts +16 -0
- package/dist/daemon/human/deliver-action.js +168 -0
- package/dist/daemon/human/finish.d.ts +8 -5
- package/dist/daemon/human/finish.js +45 -6
- package/dist/daemon/human/sweep.js +4 -1
- package/dist/daemon/reconcilers/human-delivery-lane.d.ts +10 -0
- package/dist/daemon/reconcilers/human-delivery-lane.js +41 -0
- package/dist/daemon/review/finish.d.ts +8 -3
- package/dist/daemon/review/finish.js +19 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/builtin-memory/00-runtime-base.md +0 -55
- package/dist/builtin-memory/design.md +0 -55
- /package/dist/builtin-memory/{02-lifecycle/00-terminal.md → 02-turn-lifecycle/01-terminal.md} +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { DefaultResourceLoader, ExtensionRunner, } from '@earendil-works/pi-coding-agent';
|
|
7
|
+
import { captureChatCapabilities, registrationChatSurface } from '../runtime/command-surface.js';
|
|
8
|
+
test('startup capability capture preserves same-name lifecycle replacements and excludes new lifecycle registrations', async () => {
|
|
9
|
+
const root = mkdtempSync(join(tmpdir(), 'crtr-prospective-capability-'));
|
|
10
|
+
const agentDir = join(root, '.pi');
|
|
11
|
+
const invoked = join(root, 'late-invoked');
|
|
12
|
+
mkdirSync(agentDir);
|
|
13
|
+
const first = join(root, 'first.js');
|
|
14
|
+
const second = join(root, 'second.js');
|
|
15
|
+
writeFileSync(first, `
|
|
16
|
+
import { writeFileSync } from 'node:fs';
|
|
17
|
+
export default function (pi) {
|
|
18
|
+
pi.registerCommand('collision', {
|
|
19
|
+
description: 'first factory command',
|
|
20
|
+
gateway: true,
|
|
21
|
+
argumentHint: '<first>',
|
|
22
|
+
handler: async () => {},
|
|
23
|
+
});
|
|
24
|
+
pi.on('session_start', () => {
|
|
25
|
+
pi.registerCommand('collision', {
|
|
26
|
+
description: 'lifecycle replacement',
|
|
27
|
+
argumentHint: '<replacement>',
|
|
28
|
+
handler: async () => {},
|
|
29
|
+
});
|
|
30
|
+
pi.registerCommand('late', {
|
|
31
|
+
description: 'lifecycle command',
|
|
32
|
+
gateway: true,
|
|
33
|
+
handler: async () => writeFileSync(${JSON.stringify(invoked)}, 'yes'),
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
`);
|
|
38
|
+
writeFileSync(second, `
|
|
39
|
+
export default function (pi) {
|
|
40
|
+
pi.registerCommand('collision', {
|
|
41
|
+
description: 'second factory command',
|
|
42
|
+
gateway: true,
|
|
43
|
+
argumentHint: '<second>',
|
|
44
|
+
handler: async () => {},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
`);
|
|
48
|
+
try {
|
|
49
|
+
const loader = new DefaultResourceLoader({
|
|
50
|
+
cwd: root,
|
|
51
|
+
agentDir,
|
|
52
|
+
additionalExtensionPaths: [first, second],
|
|
53
|
+
noContextFiles: true,
|
|
54
|
+
});
|
|
55
|
+
await loader.reload();
|
|
56
|
+
const loaded = loader.getExtensions();
|
|
57
|
+
assert.deepEqual(loaded.errors, []);
|
|
58
|
+
const runner = new ExtensionRunner(loaded.extensions, loaded.runtime, root, undefined, undefined);
|
|
59
|
+
const startupCommands = runner.getRegisteredCommands();
|
|
60
|
+
captureChatCapabilities({ commands: startupCommands, templates: [] });
|
|
61
|
+
const startupRows = startupCommands.map((command) => ({
|
|
62
|
+
name: command.invocationName,
|
|
63
|
+
...registrationChatSurface(command),
|
|
64
|
+
}));
|
|
65
|
+
assert.deepEqual(startupRows, [
|
|
66
|
+
{ name: 'collision:1', gatewayCapable: true, argumentHint: '<first>' },
|
|
67
|
+
{ name: 'collision:2', gatewayCapable: true, argumentHint: '<second>' },
|
|
68
|
+
]);
|
|
69
|
+
for (const extension of loaded.extensions) {
|
|
70
|
+
for (const handler of extension.handlers.get('session_start') ?? []) {
|
|
71
|
+
await handler({ type: 'session_start' }, {});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const reloadedRows = runner.getRegisteredCommands().map((command) => ({
|
|
75
|
+
name: command.invocationName,
|
|
76
|
+
...registrationChatSurface(command),
|
|
77
|
+
}));
|
|
78
|
+
assert.deepEqual(reloadedRows, [
|
|
79
|
+
{ name: 'collision:1', gatewayCapable: true, argumentHint: '<replacement>' },
|
|
80
|
+
{ name: 'late', gatewayCapable: false },
|
|
81
|
+
{ name: 'collision:2', gatewayCapable: true, argumentHint: '<second>' },
|
|
82
|
+
]);
|
|
83
|
+
const late = runner.getCommand('late');
|
|
84
|
+
assert.ok(late, 'the lifecycle command remains registered and addressable');
|
|
85
|
+
await late.handler('', {});
|
|
86
|
+
assert.equal(existsSync(invoked), true, 'the lifecycle command remains invokable');
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
rmSync(root, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { test } from 'node:test';
|
|
8
|
+
import { loadedCommandPreview } from '../../../clients/attach/slash/prompt-preview.js';
|
|
9
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
10
|
+
const EXTENSION = join(ROOT, 'dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts');
|
|
11
|
+
const LIVE_PROBE = join(ROOT, 'dist/core/__tests__/fixtures/memory-slash-live-probe.js');
|
|
12
|
+
const COMMAND_NAMES = ['acceptance-context-token', 'acceptance-node-token', 'acceptance-independent'];
|
|
13
|
+
function writeSlashDoc(memoryDir, name, description, body) {
|
|
14
|
+
writeFileSync(join(memoryDir, `${name}.md`), `---\nkind: knowledge\nwhen-and-why-to-read: Acceptance fixture.\nshort-form: ${description}\nslash: true\n---\n${body}`);
|
|
15
|
+
}
|
|
16
|
+
function liveProbe(input) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const child = spawn(process.execPath, [LIVE_PROBE, EXTENSION, JSON.stringify(input.invocations)], {
|
|
19
|
+
cwd: input.cwd,
|
|
20
|
+
env: {
|
|
21
|
+
...process.env,
|
|
22
|
+
CRTR_HOME: input.home,
|
|
23
|
+
CRTR_NODE_ID: input.nodeId,
|
|
24
|
+
CRTR_PROFILE_ID: '',
|
|
25
|
+
PI_CODING_AGENT_DIR: join(input.cwd, '.pi'),
|
|
26
|
+
},
|
|
27
|
+
stdio: ['ignore', 'pipe', 'pipe', 'pipe'],
|
|
28
|
+
});
|
|
29
|
+
let stdout = '';
|
|
30
|
+
let stderr = '';
|
|
31
|
+
let response = '';
|
|
32
|
+
const stdoutStream = child.stdout;
|
|
33
|
+
const stderrStream = child.stderr;
|
|
34
|
+
const responseStream = child.stdio[3];
|
|
35
|
+
if (stdoutStream === null || stderrStream === null || responseStream === null) {
|
|
36
|
+
child.kill('SIGKILL');
|
|
37
|
+
reject(new Error('live probe response channel unavailable'));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
stdoutStream.setEncoding('utf8');
|
|
41
|
+
stderrStream.setEncoding('utf8');
|
|
42
|
+
responseStream.setEncoding('utf8');
|
|
43
|
+
stdoutStream.on('data', (chunk) => { stdout += chunk; });
|
|
44
|
+
stderrStream.on('data', (chunk) => { stderr += chunk; });
|
|
45
|
+
responseStream.on('data', (chunk) => { response += chunk; });
|
|
46
|
+
child.on('error', reject);
|
|
47
|
+
child.on('close', (code, signal) => {
|
|
48
|
+
if (code !== 0) {
|
|
49
|
+
reject(new Error(`live probe exited ${code ?? signal ?? 'unknown'}: ${stderr}${stdout}`));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
resolve(JSON.parse(response));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function fixtureRows(rows) {
|
|
57
|
+
return COMMAND_NAMES.map((name) => {
|
|
58
|
+
const row = rows.find((candidate) => candidate.name === name);
|
|
59
|
+
assert.ok(row, `Gateway advertises /${name}`);
|
|
60
|
+
return row;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
test('node-relative memory slash commands omit preview metadata without changing live sends', async () => {
|
|
64
|
+
const cwd = mkdtempSync(join(tmpdir(), 'crtr-memory-slash-inventory-'));
|
|
65
|
+
const home = join(cwd, 'canvas');
|
|
66
|
+
const memoryDir = join(cwd, '.crouter', 'memory');
|
|
67
|
+
const nodeId = 'acceptance-live-node';
|
|
68
|
+
mkdirSync(memoryDir, { recursive: true });
|
|
69
|
+
writeSlashDoc(memoryDir, COMMAND_NAMES[0], 'Context-relative fixture.', 'Store $ARGUMENTS in $CRTR_CONTEXT_DIR/result.md.\n');
|
|
70
|
+
writeSlashDoc(memoryDir, COMMAND_NAMES[1], 'Node-relative fixture.', 'Node $CRTR_NODE_ID received $ARGUMENTS.\n');
|
|
71
|
+
writeSlashDoc(memoryDir, COMMAND_NAMES[2], 'Node-independent fixture.', 'Independent payload: $ARGUMENTS.\n');
|
|
72
|
+
try {
|
|
73
|
+
const live = await liveProbe({
|
|
74
|
+
cwd,
|
|
75
|
+
home,
|
|
76
|
+
nodeId,
|
|
77
|
+
invocations: COMMAND_NAMES.map((name) => ({ name, args: 'evidence' })),
|
|
78
|
+
});
|
|
79
|
+
assert.equal(live.stateBeforeProspective, false);
|
|
80
|
+
assert.equal(live.stateAfterProspective, false);
|
|
81
|
+
assert.deepEqual(live.commands, live.prospective.commands);
|
|
82
|
+
assert.deepEqual(live.memoryRefs, live.prospective.memory_refs);
|
|
83
|
+
const prospectiveRows = fixtureRows(live.prospective.commands);
|
|
84
|
+
const liveRows = fixtureRows(live.commands);
|
|
85
|
+
assert.deepEqual(liveRows, prospectiveRows);
|
|
86
|
+
assert.deepEqual(prospectiveRows, [
|
|
87
|
+
{ name: COMMAND_NAMES[0], description: 'Context-relative fixture.', source: 'command' },
|
|
88
|
+
{ name: COMMAND_NAMES[1], description: 'Node-relative fixture.', source: 'command' },
|
|
89
|
+
{
|
|
90
|
+
name: COMMAND_NAMES[2],
|
|
91
|
+
description: 'Node-independent fixture.',
|
|
92
|
+
source: 'command',
|
|
93
|
+
expansion: {
|
|
94
|
+
kind: 'memory-slash',
|
|
95
|
+
commandName: COMMAND_NAMES[2],
|
|
96
|
+
body: 'Independent payload: $ARGUMENTS.\n',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
const insightsInit = live.commands.find((row) => row.name === 'insights:init');
|
|
101
|
+
assert.ok(insightsInit, 'Gateway completion includes /insights:init');
|
|
102
|
+
assert.equal(insightsInit.expansion, undefined);
|
|
103
|
+
assert.equal(loadedCommandPreview('/insights:init agent-facing CLI', live.commands), undefined);
|
|
104
|
+
assert.equal(loadedCommandPreview('/acceptance-independent first evidence', live.commands)?.content, 'Independent payload: first evidence.\n');
|
|
105
|
+
assert.equal(loadedCommandPreview('/acceptance-independent changed evidence', live.commands)?.content, 'Independent payload: changed evidence.\n');
|
|
106
|
+
assert.deepEqual(live.notifications, []);
|
|
107
|
+
const sentContent = Object.fromEntries(live.sent.map((send) => [send.name, send.message.content]));
|
|
108
|
+
assert.deepEqual(sentContent, {
|
|
109
|
+
[COMMAND_NAMES[0]]: `Store evidence in ${join(home, 'nodes', nodeId, 'context', 'result.md')}.\n`,
|
|
110
|
+
[COMMAND_NAMES[1]]: `Node ${nodeId} received evidence.\n`,
|
|
111
|
+
[COMMAND_NAMES[2]]: 'Independent payload: evidence.\n',
|
|
112
|
+
});
|
|
113
|
+
for (const send of live.sent) {
|
|
114
|
+
assert.deepEqual(send.message, {
|
|
115
|
+
customType: 'memory-slash-command',
|
|
116
|
+
content: sentContent[send.name],
|
|
117
|
+
display: true,
|
|
118
|
+
});
|
|
119
|
+
assert.deepEqual(send.options, { triggerTurn: true });
|
|
120
|
+
}
|
|
121
|
+
assert.doesNotMatch(sentContent[COMMAND_NAMES[0]], /\$CRTR_CONTEXT_DIR|\$CRTR_NODE_ID/);
|
|
122
|
+
assert.doesNotMatch(sentContent[COMMAND_NAMES[1]], /\$CRTR_CONTEXT_DIR|\$CRTR_NODE_ID/);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
import { runProspectiveInventory } from '../../runtime/prospective-inventory.js';
|
|
7
|
+
test('prospective preflight keeps package stdout out of its response channel', async () => {
|
|
8
|
+
const cwd = mkdtempSync(join(tmpdir(), 'crtr-prospective-stdout-'));
|
|
9
|
+
const extension = join(cwd, 'loud-extension.mjs');
|
|
10
|
+
writeFileSync(extension, `
|
|
11
|
+
export default function (pi) {
|
|
12
|
+
console.log('harmless package diagnostic');
|
|
13
|
+
pi.registerCommand('stdout-probe', {
|
|
14
|
+
description: 'survives package stdout',
|
|
15
|
+
gateway: true,
|
|
16
|
+
handler: async () => {},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
`);
|
|
20
|
+
try {
|
|
21
|
+
const rows = await runProspectiveInventory({ cwd, profileId: null, extensions: [extension] });
|
|
22
|
+
assert.deepEqual(rows.commands.find((row) => row.name === 'stdout-probe'), {
|
|
23
|
+
name: 'stdout-probe',
|
|
24
|
+
description: 'survives package stdout',
|
|
25
|
+
source: 'command',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
});
|
package/dist/core/canvas/db.js
CHANGED
|
@@ -1153,6 +1153,28 @@ ALTER TABLE crons ADD COLUMN held INTEGER NOT NULL DEFAULT 0;
|
|
|
1153
1153
|
ALTER TABLE crons ADD COLUMN last_poke_at TEXT;
|
|
1154
1154
|
`);
|
|
1155
1155
|
}
|
|
1156
|
+
/** v33 — durable, daemon-owned attempts to deliver settled human completions. */
|
|
1157
|
+
function addHumanActionDeliveries(db) {
|
|
1158
|
+
db.exec(`
|
|
1159
|
+
CREATE TABLE human_action_deliveries (
|
|
1160
|
+
request_id TEXT PRIMARY KEY,
|
|
1161
|
+
state TEXT NOT NULL,
|
|
1162
|
+
attempt INTEGER NOT NULL DEFAULT 0,
|
|
1163
|
+
argv_json TEXT NOT NULL,
|
|
1164
|
+
cwd TEXT NOT NULL,
|
|
1165
|
+
completion_json TEXT NOT NULL,
|
|
1166
|
+
next_attempt_at INTEGER NOT NULL,
|
|
1167
|
+
claimed_at INTEGER,
|
|
1168
|
+
claim_owner TEXT,
|
|
1169
|
+
accepted_at INTEGER,
|
|
1170
|
+
permanent_failed_at INTEGER,
|
|
1171
|
+
last_failure_json TEXT,
|
|
1172
|
+
created_at INTEGER NOT NULL,
|
|
1173
|
+
updated_at INTEGER NOT NULL
|
|
1174
|
+
);
|
|
1175
|
+
CREATE INDEX idx_human_action_deliveries_due ON human_action_deliveries(state, next_attempt_at);
|
|
1176
|
+
`);
|
|
1177
|
+
}
|
|
1156
1178
|
/** The journaled lane of the state-migration registry, adapted to this chain's
|
|
1157
1179
|
* step shape. Registry order is preserved, so these versions stay stable as
|
|
1158
1180
|
* long as the registry is append-only (same contract as the local list). */
|
|
@@ -1198,6 +1220,7 @@ export const MIGRATIONS = [
|
|
|
1198
1220
|
/* v30 */ dropConsultOutbox,
|
|
1199
1221
|
/* v31 */ addReviewSubmitRequested,
|
|
1200
1222
|
/* v32 */ addCronHeldColumns,
|
|
1223
|
+
/* v33 */ addHumanActionDeliveries,
|
|
1201
1224
|
...JOURNALED_STATE_MIGRATIONS,
|
|
1202
1225
|
];
|
|
1203
1226
|
/** Migration indexes that manage their OWN transaction and therefore must not
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type ActionDeliveryState = 'pending' | 'running' | 'accepted' | 'permanent_failed';
|
|
2
|
+
export interface ActionDeliveryFailure {
|
|
3
|
+
kind: 'exit' | 'signal' | 'timeout' | 'spawn_error';
|
|
4
|
+
exitCode?: number;
|
|
5
|
+
signal?: string;
|
|
6
|
+
message?: string;
|
|
7
|
+
stderr?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ActionDeliveryRecord {
|
|
10
|
+
requestId: string;
|
|
11
|
+
state: ActionDeliveryState;
|
|
12
|
+
attempt: number;
|
|
13
|
+
argv: string[];
|
|
14
|
+
cwd: string;
|
|
15
|
+
completion: unknown;
|
|
16
|
+
nextAttemptAt: number;
|
|
17
|
+
claimedAt: number | null;
|
|
18
|
+
claimOwner: string | null;
|
|
19
|
+
acceptedAt: number | null;
|
|
20
|
+
permanentFailedAt: number | null;
|
|
21
|
+
lastFailure: ActionDeliveryFailure | null;
|
|
22
|
+
createdAt: number;
|
|
23
|
+
updatedAt: number;
|
|
24
|
+
}
|
|
25
|
+
/** Insert the one delivery row for a settled action-bound request. Idempotent
|
|
26
|
+
* on requestId: a replayed settlement must not create a second row. */
|
|
27
|
+
export declare function enqueueActionDelivery(row: {
|
|
28
|
+
requestId: string;
|
|
29
|
+
argv: string[];
|
|
30
|
+
cwd: string;
|
|
31
|
+
/** Complete HumanCompletionV1 with delivery.attempt set to 0. */
|
|
32
|
+
completion: unknown;
|
|
33
|
+
}): void;
|
|
34
|
+
/** Delivery state for `GET /v1/human/requests/:id`; null when no row exists. */
|
|
35
|
+
export declare function readActionDelivery(requestId: string): ActionDeliveryRecord | null;
|
|
36
|
+
/** Due pending records in deterministic order for the daemon's serial tick. */
|
|
37
|
+
export declare function dueActionDeliveries(now: number): ActionDeliveryRecord[];
|
|
38
|
+
/** Atomically start one due delivery attempt. A null result lost the claim. */
|
|
39
|
+
export declare function claimActionDelivery(requestId: string, now: number, claimOwner: string): ActionDeliveryRecord | null;
|
|
40
|
+
export type ActionDeliverySettlement = {
|
|
41
|
+
state: 'accepted';
|
|
42
|
+
} | {
|
|
43
|
+
state: 'permanent_failed';
|
|
44
|
+
failure: ActionDeliveryFailure;
|
|
45
|
+
} | {
|
|
46
|
+
state: 'pending';
|
|
47
|
+
nextAttemptAt: number;
|
|
48
|
+
failure: ActionDeliveryFailure;
|
|
49
|
+
};
|
|
50
|
+
/** Settle only the still-owned running attempt, so a stale process cannot overwrite a newer claim. */
|
|
51
|
+
export declare function settleActionDelivery(requestId: string, attempt: number, claimOwner: string, settlement: ActionDeliverySettlement, now: number): boolean;
|
|
52
|
+
/** A new daemon deliberately redelivers interrupted completions at least once. */
|
|
53
|
+
export declare function recoverRunningActionDeliveries(now: number): number;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Durable action-completion delivery state. The terminal ticket is composed
|
|
2
|
+
// before this row exists; this module schedules its already-complete document.
|
|
3
|
+
import { openDb, withCanvasWrite } from './db.js';
|
|
4
|
+
function actionDeliveryFrom(row) {
|
|
5
|
+
return {
|
|
6
|
+
requestId: row.request_id,
|
|
7
|
+
state: row.state,
|
|
8
|
+
attempt: row.attempt,
|
|
9
|
+
argv: JSON.parse(row.argv_json),
|
|
10
|
+
cwd: row.cwd,
|
|
11
|
+
completion: JSON.parse(row.completion_json),
|
|
12
|
+
nextAttemptAt: row.next_attempt_at,
|
|
13
|
+
claimedAt: row.claimed_at,
|
|
14
|
+
claimOwner: row.claim_owner,
|
|
15
|
+
acceptedAt: row.accepted_at,
|
|
16
|
+
permanentFailedAt: row.permanent_failed_at,
|
|
17
|
+
lastFailure: row.last_failure_json === null ? null : JSON.parse(row.last_failure_json),
|
|
18
|
+
createdAt: row.created_at,
|
|
19
|
+
updatedAt: row.updated_at,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** Insert the one delivery row for a settled action-bound request. Idempotent
|
|
23
|
+
* on requestId: a replayed settlement must not create a second row. */
|
|
24
|
+
export function enqueueActionDelivery(row) {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
withCanvasWrite((db) => {
|
|
27
|
+
db.prepare(`INSERT INTO human_action_deliveries
|
|
28
|
+
(request_id, state, attempt, argv_json, cwd, completion_json, next_attempt_at, created_at, updated_at)
|
|
29
|
+
VALUES (?, 'pending', 0, ?, ?, ?, ?, ?, ?)
|
|
30
|
+
ON CONFLICT(request_id) DO NOTHING`).run(row.requestId, JSON.stringify(row.argv), row.cwd, JSON.stringify(row.completion), now, now, now);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** Delivery state for `GET /v1/human/requests/:id`; null when no row exists. */
|
|
34
|
+
export function readActionDelivery(requestId) {
|
|
35
|
+
const row = openDb()
|
|
36
|
+
.prepare('SELECT * FROM human_action_deliveries WHERE request_id = ?')
|
|
37
|
+
.get(requestId);
|
|
38
|
+
return row === undefined ? null : actionDeliveryFrom(row);
|
|
39
|
+
}
|
|
40
|
+
/** Due pending records in deterministic order for the daemon's serial tick. */
|
|
41
|
+
export function dueActionDeliveries(now) {
|
|
42
|
+
return openDb()
|
|
43
|
+
.prepare(`SELECT * FROM human_action_deliveries
|
|
44
|
+
WHERE state = 'pending' AND next_attempt_at <= ?
|
|
45
|
+
ORDER BY next_attempt_at, created_at, request_id`)
|
|
46
|
+
.all(now).map(actionDeliveryFrom);
|
|
47
|
+
}
|
|
48
|
+
/** Atomically start one due delivery attempt. A null result lost the claim. */
|
|
49
|
+
export function claimActionDelivery(requestId, now, claimOwner) {
|
|
50
|
+
return withCanvasWrite((db) => {
|
|
51
|
+
const claimed = db.prepare(`UPDATE human_action_deliveries
|
|
52
|
+
SET state = 'running', attempt = attempt + 1, claimed_at = ?, claim_owner = ?, updated_at = ?
|
|
53
|
+
WHERE request_id = ? AND state = 'pending' AND next_attempt_at <= ?`).run(now, claimOwner, now, requestId, now);
|
|
54
|
+
if (Number(claimed.changes) === 0)
|
|
55
|
+
return null;
|
|
56
|
+
const row = db.prepare('SELECT * FROM human_action_deliveries WHERE request_id = ?').get(requestId);
|
|
57
|
+
return actionDeliveryFrom(row);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** Settle only the still-owned running attempt, so a stale process cannot overwrite a newer claim. */
|
|
61
|
+
export function settleActionDelivery(requestId, attempt, claimOwner, settlement, now) {
|
|
62
|
+
const failure = settlement.state === 'accepted' ? null : JSON.stringify(settlement.failure);
|
|
63
|
+
const result = withCanvasWrite((db) => db.prepare(`UPDATE human_action_deliveries
|
|
64
|
+
SET state = ?, next_attempt_at = ?, claimed_at = NULL, claim_owner = NULL,
|
|
65
|
+
accepted_at = ?, permanent_failed_at = ?, last_failure_json = ?, updated_at = ?
|
|
66
|
+
WHERE request_id = ? AND state = 'running' AND attempt = ? AND claim_owner = ?`).run(settlement.state, settlement.state === 'pending' ? settlement.nextAttemptAt : now, settlement.state === 'accepted' ? now : null, settlement.state === 'permanent_failed' ? now : null, failure, now, requestId, attempt, claimOwner));
|
|
67
|
+
return Number(result.changes) === 1;
|
|
68
|
+
}
|
|
69
|
+
/** A new daemon deliberately redelivers interrupted completions at least once. */
|
|
70
|
+
export function recoverRunningActionDeliveries(now) {
|
|
71
|
+
const result = withCanvasWrite((db) => db.prepare(`UPDATE human_action_deliveries
|
|
72
|
+
SET state = 'pending', next_attempt_at = ?, claimed_at = NULL, claim_owner = NULL, updated_at = ?
|
|
73
|
+
WHERE state = 'running'`).run(now, now));
|
|
74
|
+
return Number(result.changes);
|
|
75
|
+
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PageComponentRegistration, Scope, ScopeConfig, ScopeState, KindConfig, ModelLaddersConfig, ModelProvider, ModelStrength, ModelRouteConfig, ModelRoutingConfig } from '../types.js';
|
|
1
|
+
import type { PageComponentRegistration, Scope, ScopeConfig, ScopeState, HumanActionConfig, KindConfig, ModelLaddersConfig, ModelProvider, ModelStrength, ModelRouteConfig, ModelRoutingConfig } from '../types.js';
|
|
2
2
|
import { type BindingId } from './keybindings/catalog.js';
|
|
3
3
|
export declare function configPath(scope: Scope): string | null;
|
|
4
4
|
export declare function statePath(scope: Scope): string | null;
|
|
@@ -40,6 +40,18 @@ export declare function binDeclarationIssues(raw: unknown): BinDeclarationIssue[
|
|
|
40
40
|
* manifest's) to name → root-relative path. Invalid entries never become
|
|
41
41
|
* commands; `binDeclarationIssues` keeps them visible to Doctor. */
|
|
42
42
|
export declare function normalizeBin(raw: unknown): Record<string, string>;
|
|
43
|
+
export interface HumanActionDeclarationIssue {
|
|
44
|
+
name: string;
|
|
45
|
+
reason: string;
|
|
46
|
+
block?: true;
|
|
47
|
+
}
|
|
48
|
+
/** Validate `humanActions` declaration shape without touching the filesystem.
|
|
49
|
+
* Filesystem checks happen only when resolving an action or running Doctor. */
|
|
50
|
+
export declare function humanActionDeclarationIssues(raw: unknown): HumanActionDeclarationIssue[];
|
|
51
|
+
/** Normalize valid action declarations for scope-local config inspection.
|
|
52
|
+
* Invalid entries remain available through raw reads so resolution and Doctor
|
|
53
|
+
* can report them rather than silently treating them as absent. */
|
|
54
|
+
export declare function normalizeHumanActions(raw: unknown): Record<string, HumanActionConfig>;
|
|
43
55
|
/** STRICT install-time validation for a plugin-declared `bin` block. Target
|
|
44
56
|
* filesystem state is checked later by Doctor because it can change after
|
|
45
57
|
* install. */
|
package/dist/core/config.js
CHANGED
|
@@ -320,6 +320,55 @@ export function normalizeBin(raw) {
|
|
|
320
320
|
}
|
|
321
321
|
return out;
|
|
322
322
|
}
|
|
323
|
+
const HUMAN_ACTION_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
324
|
+
/** Validate `humanActions` declaration shape without touching the filesystem.
|
|
325
|
+
* Filesystem checks happen only when resolving an action or running Doctor. */
|
|
326
|
+
export function humanActionDeclarationIssues(raw) {
|
|
327
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
328
|
+
return [{ name: 'humanActions', reason: 'humanActions must be a JSON object mapping action names to {argv, cwd}', block: true }];
|
|
329
|
+
}
|
|
330
|
+
const issues = [];
|
|
331
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
332
|
+
const reasons = [];
|
|
333
|
+
if (!HUMAN_ACTION_NAME_PATTERN.test(name)) {
|
|
334
|
+
reasons.push(`action name must match ${HUMAN_ACTION_NAME_PATTERN.source}`);
|
|
335
|
+
}
|
|
336
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
337
|
+
reasons.push('action must be an object with argv and cwd');
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
const action = value;
|
|
341
|
+
const extras = Object.keys(action).filter((key) => key !== 'argv' && key !== 'cwd');
|
|
342
|
+
if (extras.length > 0)
|
|
343
|
+
reasons.push(`action has unsupported field${extras.length === 1 ? '' : 's'}: ${extras.join(', ')}`);
|
|
344
|
+
if (!Array.isArray(action.argv) || action.argv.length === 0 || !action.argv.every((arg) => typeof arg === 'string' && arg.length > 0)) {
|
|
345
|
+
reasons.push('argv must be a non-empty array of non-empty strings');
|
|
346
|
+
}
|
|
347
|
+
if (typeof action.cwd !== 'string' || action.cwd.length === 0) {
|
|
348
|
+
reasons.push('cwd is required and must be a non-empty string');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (reasons.length > 0)
|
|
352
|
+
issues.push({ name, reason: reasons.join('; ') });
|
|
353
|
+
}
|
|
354
|
+
return issues;
|
|
355
|
+
}
|
|
356
|
+
/** Normalize valid action declarations for scope-local config inspection.
|
|
357
|
+
* Invalid entries remain available through raw reads so resolution and Doctor
|
|
358
|
+
* can report them rather than silently treating them as absent. */
|
|
359
|
+
export function normalizeHumanActions(raw) {
|
|
360
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
361
|
+
return {};
|
|
362
|
+
const invalid = new Set(humanActionDeclarationIssues(raw).map((issue) => issue.name));
|
|
363
|
+
const out = {};
|
|
364
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
365
|
+
if (invalid.has(name))
|
|
366
|
+
continue;
|
|
367
|
+
const action = value;
|
|
368
|
+
out[name] = { argv: [...action.argv], cwd: action.cwd };
|
|
369
|
+
}
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
323
372
|
/** STRICT install-time validation for a plugin-declared `bin` block. Target
|
|
324
373
|
* filesystem state is checked later by Doctor because it can change after
|
|
325
374
|
* install. */
|
|
@@ -635,7 +684,8 @@ function mergeConfig(partial) {
|
|
|
635
684
|
const remoteCanvas = mergeRemoteCanvas(partial.remoteCanvas);
|
|
636
685
|
const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
|
|
637
686
|
const bin = normalizeBin(partial.bin);
|
|
638
|
-
|
|
687
|
+
const humanActions = normalizeHumanActions(partial.humanActions);
|
|
688
|
+
return { bin, humanActions, schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, completion_bell, working_gerunds, whip_mode, page_components, page_surface, whip_messages, whip_message_mode, mouse_mode_default, live_cycles, condensed_history, bash_tool_purpose, fold_finished_tools, summarize_tool_calls, detailed_tool_recaps, keybindings, tmux_passthrough, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
|
|
639
689
|
}
|
|
640
690
|
/** Raw (un-defaulted) partial config for one scope, or null if the scope has
|
|
641
691
|
* no root or no config.json. Used by `readMergedLaunchConfig` to layer
|
|
@@ -66,6 +66,12 @@ export declare function readCanceledEntryIds(nodeId: string): Set<InboxEntryId>;
|
|
|
66
66
|
* the interrupt's abort lands right behind, and covered handoffs are never
|
|
67
67
|
* re-delivered anyway. Returns the newly canceled entry ids. */
|
|
68
68
|
export declare function cancelPendingHumanMessages(nodeId: string): InboxEntryId[];
|
|
69
|
+
/** Cancel every not-yet-settled entry the predicate selects. Retraction, not
|
|
70
|
+
* deletion: the append-only inbox is untouched and the sidecar grows. Safe to
|
|
71
|
+
* call late — an entry already past the cursor is not pending, and the watcher
|
|
72
|
+
* re-reads the set at plan time so a cancel racing a buffered entry still
|
|
73
|
+
* lands. */
|
|
74
|
+
export declare function cancelPendingEntries(nodeId: string, select: (entry: InboxEntry) => boolean): InboxEntryId[];
|
|
69
75
|
/** Read and validate the persisted exact cursor identity. */
|
|
70
76
|
export declare function readCursor(nodeId: string): InboxEntryId | undefined;
|
|
71
77
|
/** Persist one validated exact cursor identity (atomic tmp+rename). */
|
package/dist/core/feed/inbox.js
CHANGED
|
@@ -245,11 +245,19 @@ export function readCanceledEntryIds(nodeId) {
|
|
|
245
245
|
* the interrupt's abort lands right behind, and covered handoffs are never
|
|
246
246
|
* re-delivered anyway. Returns the newly canceled entry ids. */
|
|
247
247
|
export function cancelPendingHumanMessages(nodeId) {
|
|
248
|
+
return cancelPendingEntries(nodeId, (e) => e.from === 'human' && e.kind === 'message');
|
|
249
|
+
}
|
|
250
|
+
/** Cancel every not-yet-settled entry the predicate selects. Retraction, not
|
|
251
|
+
* deletion: the append-only inbox is untouched and the sidecar grows. Safe to
|
|
252
|
+
* call late — an entry already past the cursor is not pending, and the watcher
|
|
253
|
+
* re-reads the set at plan time so a cancel racing a buffered entry still
|
|
254
|
+
* lands. */
|
|
255
|
+
export function cancelPendingEntries(nodeId, select) {
|
|
248
256
|
const cursor = readCursor(nodeId);
|
|
249
257
|
const pending = readInboxSince(nodeId, cursor);
|
|
250
258
|
const already = readCanceledEntryIds(nodeId);
|
|
251
259
|
const targets = pending
|
|
252
|
-
.filter((e) =>
|
|
260
|
+
.filter((e) => !already.has(e.entry_id) && select(e))
|
|
253
261
|
.map((e) => e.entry_id);
|
|
254
262
|
if (targets.length === 0)
|
|
255
263
|
return [];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const HUMAN_ACTION_NAME_PATTERN: RegExp;
|
|
2
|
+
export interface TicketActionBinding {
|
|
3
|
+
schema: 'crtr.human-action-binding/v1';
|
|
4
|
+
/** Declared action name, matching HUMAN_ACTION_NAME_PATTERN. */
|
|
5
|
+
name: string;
|
|
6
|
+
/** Frozen creation payload; opaque JSON, never inspected. */
|
|
7
|
+
payload: unknown;
|
|
8
|
+
/** The creator's absolute cwd, captured verbatim. */
|
|
9
|
+
creatorCwd: string;
|
|
10
|
+
/** Absolute path of the config file that won resolution. */
|
|
11
|
+
configOrigin: string;
|
|
12
|
+
/** Fully resolved argv; argv[0] is an absolute executable path. */
|
|
13
|
+
argv: string[];
|
|
14
|
+
/** Fully resolved absolute working directory. */
|
|
15
|
+
cwd: string;
|
|
16
|
+
}
|
|
17
|
+
/** Publish the binding before the page exists, so a visible request is never
|
|
18
|
+
* unbound. A second write is a caller bug, not a race to tolerate. */
|
|
19
|
+
export declare function publishTicketActionBinding(dir: string, binding: TicketActionBinding): void;
|
|
20
|
+
/** The frozen binding, or null for a request with no completion action. */
|
|
21
|
+
export declare function readTicketActionBinding(dir: string): TicketActionBinding | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The optional completion-action binding of a durable human request. Written
|
|
2
|
+
// once at creation and never again: `replace` revises the page, so the approved
|
|
3
|
+
// effect must not be reachable from any later write.
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { actionBindingPath, publishJsonExclusive } from './convention.js';
|
|
7
|
+
export const HUMAN_ACTION_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
8
|
+
const bindingSchema = z.object({
|
|
9
|
+
schema: z.literal('crtr.human-action-binding/v1'),
|
|
10
|
+
name: z.string().regex(HUMAN_ACTION_NAME_PATTERN),
|
|
11
|
+
payload: z.unknown(),
|
|
12
|
+
creatorCwd: z.string().min(1),
|
|
13
|
+
configOrigin: z.string().min(1),
|
|
14
|
+
argv: z.array(z.string().min(1)).min(1),
|
|
15
|
+
cwd: z.string().min(1),
|
|
16
|
+
}).strict();
|
|
17
|
+
/** Publish the binding before the page exists, so a visible request is never
|
|
18
|
+
* unbound. A second write is a caller bug, not a race to tolerate. */
|
|
19
|
+
export function publishTicketActionBinding(dir, binding) {
|
|
20
|
+
if (!publishJsonExclusive(actionBindingPath(dir), binding)) {
|
|
21
|
+
throw new Error(`action binding already exists: ${actionBindingPath(dir)}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** The frozen binding, or null for a request with no completion action. */
|
|
25
|
+
export function readTicketActionBinding(dir) {
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = JSON.parse(readFileSync(actionBindingPath(dir), 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code === 'ENOENT')
|
|
32
|
+
return null;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
const parsed = bindingSchema.parse(raw);
|
|
36
|
+
// `payload` is optional-by-absence in JSON but required in the type; z.unknown()
|
|
37
|
+
// accepts a missing key, so restore it explicitly as the JSON null the
|
|
38
|
+
// completion document must carry.
|
|
39
|
+
return { ...parsed, payload: raw.payload ?? null };
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { TicketActionBinding } from './action-binding.js';
|
|
2
|
+
import type { PageSource } from './page-schema.js';
|
|
3
|
+
import type { TicketResult } from './types.js';
|
|
4
|
+
export type HumanCompletionEvent = 'answered' | 'dismissed' | 'canceled';
|
|
5
|
+
export interface HumanCompletionV1 {
|
|
6
|
+
schema: 'crtr.human-completion/v1';
|
|
7
|
+
requestId: string;
|
|
8
|
+
event: HumanCompletionEvent;
|
|
9
|
+
settledAt: string;
|
|
10
|
+
/** Present only when `event` is `answered`; absent, never empty, otherwise. */
|
|
11
|
+
responses?: Record<string, Record<string, unknown>>;
|
|
12
|
+
action: {
|
|
13
|
+
name: string;
|
|
14
|
+
payload: unknown;
|
|
15
|
+
};
|
|
16
|
+
provenance: {
|
|
17
|
+
source: PageSource;
|
|
18
|
+
actor?: string;
|
|
19
|
+
};
|
|
20
|
+
/** The lane overwrites this with the real attempt before writing stdin. */
|
|
21
|
+
delivery: {
|
|
22
|
+
attempt: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** The one place a cancellation record without `disposition` reads as
|
|
26
|
+
* `canceled`. Every other reader treats both dispositions alike. */
|
|
27
|
+
export declare function completionEventFor(result: TicketResult): HumanCompletionEvent;
|
|
28
|
+
/** Build the complete document with `delivery.attempt` as the lane's placeholder. */
|
|
29
|
+
export declare function composeHumanCompletion(args: {
|
|
30
|
+
requestId: string;
|
|
31
|
+
result: PageTerminalResult;
|
|
32
|
+
binding: TicketActionBinding;
|
|
33
|
+
source: PageSource;
|
|
34
|
+
}): HumanCompletionV1;
|
|
35
|
+
/** A review result can never carry an action binding; the composer is total over the rest. */
|
|
36
|
+
export type PageTerminalResult = Extract<TicketResult, {
|
|
37
|
+
kind: 'page' | 'canceled';
|
|
38
|
+
}>;
|