@north-light/crouter 0.3.198 → 0.3.199
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 +18 -18
- package/dist/api/client.js +29 -37
- package/dist/api/dto/broker-ops.d.ts +3 -1
- package/dist/api/dto/broker.d.ts +13 -2
- package/dist/api/dto/broker.js +4 -4
- package/dist/api/dto/human.d.ts +6 -5
- package/dist/api/dto/inbox.d.ts +81 -34
- package/dist/api/dto/nodes.d.ts +8 -2
- package/dist/api/dto/profiles.d.ts +23 -0
- package/dist/api/dto/profiles.js +2 -2
- package/dist/api/dto/worktree.d.ts +3 -5
- package/dist/api/routes.d.ts +3 -5
- package/dist/api/routes.js +4 -6
- package/dist/builtin-memory/00-runtime-base.md +6 -6
- package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
- package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
- package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
- package/dist/builtin-memory/insights/capture.md +2 -2
- package/dist/builtin-memory/insights/init.md +3 -3
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/memory-loading.md +1 -1
- package/dist/builtin-memory/internal/nodes-and-canvas.md +2 -2
- package/dist/builtin-memory/internal/plugins.md +59 -2
- package/dist/builtin-memory/internal/storage-tiers.md +3 -1
- package/dist/builtin-memory/spec/roadmap.md +2 -2
- package/dist/builtin-memory/testing.md +1 -1
- package/dist/clients/attach/chrome/bash-jobs.js +3 -3
- package/dist/clients/attach/render/chat-view.d.ts +26 -1
- package/dist/clients/attach/render/chat-view.js +61 -11
- package/dist/clients/attach/render/page-block.d.ts +34 -0
- package/dist/clients/attach/render/page-block.js +226 -0
- package/dist/clients/attach/session/whip.d.ts +2 -0
- package/dist/clients/attach/session/whip.js +2 -0
- package/dist/clients/attach/viewer.js +1419 -993
- package/dist/clients/conversation/projection.js +18 -3
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -21
- package/dist/clients/inbox/controller.js +10 -16
- package/dist/clients/inbox/page-adapter.d.ts +1 -3
- package/dist/clients/inbox/page-adapter.js +3 -4
- package/dist/clients/inbox/resolve.d.ts +1 -1
- package/dist/clients/inbox/resolve.js +3 -3
- package/dist/clients/inbox/review/document-surface.js +3 -1
- package/dist/clients/inbox/review/state.d.ts +2 -1
- package/dist/clients/inbox/review/state.js +5 -16
- package/dist/clients/inbox/tui/input.js +37 -19
- package/dist/clients/inbox/tui/panel.js +5 -31
- package/dist/clients/inbox/tui/render.d.ts +11 -0
- package/dist/clients/inbox/tui/render.js +71 -27
- package/dist/clients/inbox/tui/slots.d.ts +3 -1
- package/dist/clients/inbox/tui/slots.js +16 -12
- package/dist/clients/inbox/tui/types.d.ts +2 -7
- package/dist/clients/inbox/tui.js +2 -2
- package/dist/commands/__tests__/human.test.js +71 -114
- package/dist/commands/__tests__/node-message.test.js +14 -0
- package/dist/commands/attention.js +4 -4
- package/dist/commands/canvas-snapshot.js +1 -1
- package/dist/commands/canvas.js +1 -1
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/human/{html.d.ts → components.d.ts} +1 -1
- package/dist/commands/human/components.js +77 -0
- package/dist/commands/human/feedback.d.ts +2 -0
- package/dist/commands/human/feedback.js +108 -0
- package/dist/commands/human/prompts.d.ts +13 -5
- package/dist/commands/human/prompts.js +160 -97
- package/dist/commands/human/queue.js +57 -88
- package/dist/commands/human/shared.d.ts +2 -12
- package/dist/commands/human/shared.js +17 -11
- package/dist/commands/human.js +29 -21
- package/dist/commands/memory/origin.js +2 -2
- package/dist/commands/node/bash.js +4 -4
- package/dist/commands/node/inspect.js +3 -3
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node/message.js +2 -2
- package/dist/commands/node-worktree.js +8 -8
- package/dist/commands/pkg/browse.js +1 -1
- package/dist/commands/pkg/plugin-manage.js +129 -10
- package/dist/commands/profile/delete.js +41 -15
- package/dist/commands/profile.js +1 -1
- package/dist/commands/push.js +1 -1
- package/dist/commands/surface/node/focus.js +29 -14
- package/dist/commands/surface-edit.js +3 -3
- package/dist/commands/surface-inbox.d.ts +2 -0
- package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
- package/dist/commands/surface.js +2 -1
- package/dist/commands/sys/doctor.js +60 -3
- package/dist/commands/sys/feedback.js +79 -25
- package/dist/commands/sys/settings.js +1 -1
- package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
- package/dist/core/__tests__/extension-abort.test.js +5 -2
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
- package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
- package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
- package/dist/core/__tests__/human-deliver.test.js +31 -26
- package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
- package/dist/core/__tests__/plugin-page-components.test.js +157 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +40 -0
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -6
- package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
- package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
- package/dist/core/__tests__/serial/worktree.test.js +24 -198
- package/dist/core/__tests__/session-cycles.test.js +22 -0
- package/dist/core/__tests__/stop-guard.test.js +4 -4
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
- package/dist/core/bash-jobs.d.ts +6 -0
- package/dist/core/bash-jobs.js +10 -0
- package/dist/core/canvas/__tests__/attention.test.js +8 -5
- package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
- package/dist/core/canvas/attention.js +8 -6
- package/dist/core/canvas/browse/model.d.ts +8 -2
- package/dist/core/canvas/browse/model.js +11 -4
- package/dist/core/canvas/canvas.d.ts +7 -0
- package/dist/core/canvas/canvas.js +37 -0
- package/dist/core/canvas/crons.d.ts +9 -0
- package/dist/core/canvas/crons.js +51 -0
- package/dist/core/canvas/node-order.d.ts +22 -1
- package/dist/core/canvas/node-order.js +26 -1
- package/dist/core/canvas/render-source.d.ts +5 -0
- package/dist/core/canvas/render-source.js +1 -0
- package/dist/core/canvas/types.d.ts +9 -3
- package/dist/core/command-plugins/bundle.d.ts +6 -1
- package/dist/core/command-plugins/bundle.js +26 -13
- package/dist/core/command.js +2 -1
- package/dist/core/config.d.ts +47 -1
- package/dist/core/config.js +137 -4
- package/dist/core/help.js +1 -1
- package/dist/core/human/__tests__/page-catalog.test.js +4 -0
- package/dist/core/human/__tests__/page-tickets.test.js +65 -52
- package/dist/core/human/__tests__/page.test.js +56 -98
- package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -6
- package/dist/core/human/answer-text.js +3 -3
- package/dist/core/human/answer.d.ts +6 -4
- package/dist/core/human/answer.js +4 -10
- package/dist/core/human/component-docs.d.ts +11 -7
- package/dist/core/human/component-docs.js +361 -88
- package/dist/core/human/convention.d.ts +17 -3
- package/dist/core/human/convention.js +36 -5
- package/dist/core/human/feedback-companion.d.ts +20 -0
- package/dist/core/human/feedback-companion.js +98 -0
- package/dist/core/human/feedback.d.ts +65 -0
- package/dist/core/human/feedback.js +116 -0
- package/dist/core/human/page-catalog.d.ts +40 -5
- package/dist/core/human/page-catalog.js +102 -30
- package/dist/core/human/page-errors.d.ts +4 -0
- package/dist/core/human/page-errors.js +7 -0
- package/dist/core/human/page-eval.d.ts +20 -0
- package/dist/core/human/page-eval.js +123 -0
- package/dist/core/human/page-schema.d.ts +23 -191
- package/dist/core/human/page-schema.js +60 -89
- package/dist/core/human/page-synth.d.ts +1 -1
- package/dist/core/human/page-synth.js +15 -18
- package/dist/core/human/page.d.ts +12 -27
- package/dist/core/human/page.js +142 -474
- package/dist/core/human/root.d.ts +6 -2
- package/dist/core/human/root.js +12 -4
- package/dist/core/human/scan.d.ts +3 -4
- package/dist/core/human/scan.js +16 -15
- package/dist/core/human/summary.d.ts +1 -2
- package/dist/core/human/summary.js +2 -2
- package/dist/core/human/tickets.d.ts +20 -14
- package/dist/core/human/tickets.js +91 -31
- package/dist/core/human/types.d.ts +10 -2
- package/dist/core/preview-registry.js +8 -10
- package/dist/core/profiles/default-binding.d.ts +4 -0
- package/dist/core/profiles/default-binding.js +24 -0
- package/dist/core/profiles/deletion-reservation.d.ts +8 -0
- package/dist/core/profiles/deletion-reservation.js +38 -0
- package/dist/core/profiles/manifest.d.ts +3 -0
- package/dist/core/profiles/manifest.js +12 -0
- package/dist/core/profiles/select.js +2 -2
- package/dist/core/review/realize.js +3 -3
- package/dist/core/review/store.d.ts +6 -0
- package/dist/core/review/store.js +31 -0
- package/dist/core/runtime/bearings.d.ts +0 -4
- package/dist/core/runtime/bearings.js +4 -62
- package/dist/core/runtime/bin-contributions.d.ts +44 -0
- package/dist/core/runtime/bin-contributions.js +186 -0
- package/dist/core/runtime/broker/client-registry.d.ts +4 -0
- package/dist/core/runtime/broker/client-registry.js +13 -1
- package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
- package/dist/core/runtime/broker/extension-abort.js +4 -2
- package/dist/core/runtime/broker/fault-retry.js +4 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
- package/dist/core/runtime/broker/frame-dispatch.js +5 -6
- package/dist/core/runtime/broker/read-ops.d.ts +1 -1
- package/dist/core/runtime/broker/read-ops.js +2 -2
- package/dist/core/runtime/broker/rebind.d.ts +1 -0
- package/dist/core/runtime/broker/rebind.js +6 -1
- package/dist/core/runtime/broker-extension-render.js +19 -0
- package/dist/core/runtime/broker-protocol.d.ts +14 -0
- package/dist/core/runtime/broker.d.ts +1 -1
- package/dist/core/runtime/broker.js +17 -6
- package/dist/core/runtime/front-door-env.d.ts +5 -0
- package/dist/core/runtime/front-door-env.js +16 -0
- package/dist/core/runtime/front-door.d.ts +1 -5
- package/dist/core/runtime/front-door.js +2 -5
- package/dist/core/runtime/host.d.ts +1 -1
- package/dist/core/runtime/invocation.d.ts +6 -0
- package/dist/core/runtime/invocation.js +10 -0
- package/dist/core/runtime/kickoff.js +34 -0
- package/dist/core/runtime/launch.d.ts +2 -6
- package/dist/core/runtime/node-read.js +4 -1
- package/dist/core/runtime/nodes.js +2 -0
- package/dist/core/runtime/promote.d.ts +1 -0
- package/dist/core/runtime/promote.js +3 -1
- package/dist/core/runtime/session-cycles.d.ts +6 -0
- package/dist/core/runtime/session-cycles.js +15 -1
- package/dist/core/runtime/session-visibility.d.ts +17 -8
- package/dist/core/runtime/session-visibility.js +19 -10
- package/dist/core/runtime/spawn-env.d.ts +9 -1
- package/dist/core/runtime/spawn-env.js +62 -2
- package/dist/core/runtime/stop-guard.d.ts +2 -2
- package/dist/core/runtime/stop-guard.js +2 -2
- package/dist/core/runtime/stop-signals.d.ts +1 -1
- package/dist/core/runtime/stop-signals.js +2 -2
- package/dist/core/runtime/tmux-bindings.js +1 -1
- package/dist/core/session-model/session-state.d.ts +4 -3
- package/dist/core/session-model/session-state.js +14 -1
- package/dist/core/user-settings.d.ts +21 -2
- package/dist/core/user-settings.js +32 -11
- package/dist/core/worktree.d.ts +15 -45
- package/dist/core/worktree.js +59 -261
- package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/broker-ops.js +9 -5
- package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
- package/dist/daemon/api/handlers/feedback-comments.js +207 -0
- package/dist/daemon/api/handlers/human.js +24 -20
- package/dist/daemon/api/handlers/inbox.d.ts +10 -0
- package/dist/daemon/api/handlers/inbox.js +39 -115
- package/dist/daemon/api/handlers/profiles.js +11 -29
- package/dist/daemon/api/map.d.ts +6 -1
- package/dist/daemon/api/map.js +23 -0
- package/dist/daemon/api/server.js +3 -1
- package/dist/daemon/companion-retire.d.ts +2 -0
- package/dist/daemon/companion-retire.js +33 -0
- package/dist/daemon/cron-run.js +14 -16
- package/dist/daemon/human/finish.d.ts +12 -7
- package/dist/daemon/human/finish.js +101 -38
- package/dist/daemon/human/sweep.js +5 -1
- package/dist/daemon/profile-delete.d.ts +7 -0
- package/dist/daemon/profile-delete.js +306 -0
- package/dist/daemon/reconcilers/broker-supervision.js +3 -1
- package/dist/daemon/review/deliver.js +3 -3
- package/dist/daemon/review/finish.d.ts +0 -2
- package/dist/daemon/review/finish.js +1 -29
- package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
- package/dist/pi-extensions/canvas-bash-valve.js +19 -5
- package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
- package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
- package/dist/pi-extensions/canvas-review-boundary.js +18 -9
- package/dist/pi-extensions/canvas-stophook.js +4 -4
- package/dist/shared/generated-context.js +2 -2
- package/dist/types.d.ts +44 -2
- package/dist/types.js +7 -2
- package/package.json +4 -4
- package/runtime.lock.json +5 -33
- package/dist/builtin-memory/init.md +0 -38
- package/dist/builtin-memory/plan.md +0 -19
- package/dist/builtin-memory/spec.md +0 -19
- package/dist/commands/human/doc.d.ts +0 -2
- package/dist/commands/human/doc.js +0 -91
- package/dist/commands/human/html.js +0 -81
- package/dist/commands/human/inbox.d.ts +0 -2
- package/dist/core/human/__tests__/html-markdown.test.js +0 -52
- package/dist/core/human/__tests__/page-render.test.js +0 -66
- package/dist/core/human/html-markdown.d.ts +0 -3
- package/dist/core/human/html-markdown.js +0 -288
- package/dist/core/human/page-render.d.ts +0 -4
- package/dist/core/human/page-render.js +0 -105
- package/dist/pages/bundle.css +0 -1
- package/dist/pages/bundle.js +0 -1778
- package/dist/pages/comments.d.ts +0 -72
- package/dist/pages/comments.js +0 -176
- package/dist/pages/controls.d.ts +0 -98
- package/dist/pages/controls.js +0 -261
- package/dist/pages/elements/cards.d.ts +0 -28
- package/dist/pages/elements/cards.js +0 -915
- package/dist/pages/elements/chart.d.ts +0 -23
- package/dist/pages/elements/chart.js +0 -664
- package/dist/pages/elements/options.d.ts +0 -30
- package/dist/pages/elements/options.js +0 -742
- package/dist/pages/elements/pages.d.ts +0 -30
- package/dist/pages/elements/pages.js +0 -377
- package/dist/pages/elements/slot.d.ts +0 -26
- package/dist/pages/elements/slot.js +0 -112
- package/dist/pages/elements/table.d.ts +0 -36
- package/dist/pages/elements/table.js +0 -1043
- package/dist/pages/elements/text.d.ts +0 -31
- package/dist/pages/elements/text.js +0 -1175
- package/dist/pages/entry.d.ts +0 -8
- package/dist/pages/entry.js +0 -8
- package/dist/pages/host.d.ts +0 -134
- package/dist/pages/host.js +0 -165
- package/dist/pages/readonly.d.ts +0 -24
- package/dist/pages/readonly.js +0 -31
- package/dist/pages/register.d.ts +0 -2
- package/dist/pages/register.js +0 -3
- package/dist/pages/responses.d.ts +0 -37
- package/dist/pages/responses.js +0 -56
- package/dist/pages/slot-config.d.ts +0 -36
- package/dist/pages/slot-config.js +0 -50
- package/dist/pages/types.d.ts +0 -98
- package/dist/pages/types.js +0 -23
- /package/dist/{core/human/__tests__/html-markdown.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
- /package/dist/core/{human/__tests__/page-render.test.d.ts → __tests__/plugin-page-components.test.d.ts} +0 -0
|
@@ -105,3 +105,25 @@ test('cycleAwareMessages: a yielded cycle keeps terminal assistant content witho
|
|
|
105
105
|
assert.equal(yieldedAssistant.errorMessage, undefined, 'the provider abort text is omitted with the yield boundary');
|
|
106
106
|
assert.equal(out.filter((message) => message.customType === 'crtr-cycle-divider').length, 1);
|
|
107
107
|
});
|
|
108
|
+
// Observed shape: the yield's self-abort landed as the NEXT request was
|
|
109
|
+
// starting, so the provider caught a bare AbortError instead of seeing its own
|
|
110
|
+
// signal and stamped `stopReason: 'error'` on an empty turn. Pi's assistant
|
|
111
|
+
// component reserves a red `Error: …` line for exactly that stopReason, so the
|
|
112
|
+
// annotation must come off here too — a yield is not a failure.
|
|
113
|
+
test('cycleAwareMessages: a yield abort recorded as a provider error is still not a failed turn', () => {
|
|
114
|
+
const m1 = userMsg('m1', null, 'yield after this turn');
|
|
115
|
+
const m2 = assistantMsg('m2', 'm1', [], 'error', 'This operation was aborted');
|
|
116
|
+
const cy1 = {
|
|
117
|
+
type: 'custom',
|
|
118
|
+
id: 'cy1',
|
|
119
|
+
parentId: null,
|
|
120
|
+
timestamp: new Date(1).toISOString(),
|
|
121
|
+
customType: CRTR_CYCLE_CUSTOM_TYPE,
|
|
122
|
+
data: { cycle: 2, fromLeaf: 'm2' },
|
|
123
|
+
};
|
|
124
|
+
const m3 = userMsg('m3', 'cy1', 'fresh cycle');
|
|
125
|
+
const out = cycleAwareMessages(cycleManager([m1, m2, cy1, m3], 'm3'));
|
|
126
|
+
const yieldedAssistant = out.find((message) => message.role === 'assistant');
|
|
127
|
+
assert.equal(yieldedAssistant.stopReason, undefined, 'yield is not presented as a failed assistant turn');
|
|
128
|
+
assert.equal(yieldedAssistant.errorMessage, undefined, 'the aborted-request text is omitted with the yield boundary');
|
|
129
|
+
});
|
|
@@ -28,7 +28,7 @@ function node(id, over = {}) {
|
|
|
28
28
|
...over,
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
-
const noSignals = { pushedFinal: false,
|
|
31
|
+
const noSignals = { pushedFinal: false, requestedHumanReply: false };
|
|
32
32
|
before(() => {
|
|
33
33
|
home = mkdtempSync(join(tmpdir(), 'crtr-stopguard-'));
|
|
34
34
|
process.env['CRTR_HOME'] = home;
|
|
@@ -132,10 +132,10 @@ test('a TERMINAL node whose only worker is dead is stalled (no LIVE subscription
|
|
|
132
132
|
assert.equal(d.action, 'reprompt');
|
|
133
133
|
assert.equal(d.reason, 'stalled');
|
|
134
134
|
});
|
|
135
|
-
test('pushedFinal → finished;
|
|
135
|
+
test('pushedFinal → finished; requestedHumanReply → escalated (both short-circuit before lifecycle)', () => {
|
|
136
136
|
createNode(node('t', { parent: 'mgr', lifecycle: 'terminal' }));
|
|
137
|
-
assert.deepEqual(evaluateStop('t', { pushedFinal: true,
|
|
138
|
-
assert.deepEqual(evaluateStop('t', { pushedFinal: false,
|
|
137
|
+
assert.deepEqual(evaluateStop('t', { pushedFinal: true, requestedHumanReply: false }), { action: 'allow', reason: 'finished' });
|
|
138
|
+
assert.deepEqual(evaluateStop('t', { pushedFinal: false, requestedHumanReply: true }), { action: 'allow', reason: 'escalated' });
|
|
139
139
|
});
|
|
140
140
|
test('an active follow-up cycle with a canonical final is allowed without another final', () => {
|
|
141
141
|
createNode(node('followUp', {
|
|
@@ -14,7 +14,7 @@ after(() => {
|
|
|
14
14
|
rmSync(home, { recursive: true, force: true });
|
|
15
15
|
});
|
|
16
16
|
test('only a watchdog-stamped aborted settlement records an automatic connection fault', () => {
|
|
17
|
-
const settleAbort = (nodeId, stagedWatchdogAbort) => {
|
|
17
|
+
const settleAbort = (nodeId, stagedWatchdogAbort, stagedRefreshAbort, stopReason = 'aborted') => {
|
|
18
18
|
const session = {
|
|
19
19
|
sessionManager: { getLeafId: () => 'leaf-entry' },
|
|
20
20
|
model: undefined,
|
|
@@ -24,9 +24,14 @@ test('only a watchdog-stamped aborted settlement records an automatic connection
|
|
|
24
24
|
session,
|
|
25
25
|
fallback: { blocked: false },
|
|
26
26
|
stagedProviderAgentEnd: {
|
|
27
|
-
messages: [{
|
|
27
|
+
messages: [{
|
|
28
|
+
role: 'assistant',
|
|
29
|
+
stopReason,
|
|
30
|
+
errorMessage: stopReason === 'error' ? 'This operation was aborted' : 'Request was aborted',
|
|
31
|
+
}],
|
|
28
32
|
},
|
|
29
33
|
stagedWatchdogAbort,
|
|
34
|
+
stagedRefreshAbort,
|
|
30
35
|
stagedOverflowFailure: null,
|
|
31
36
|
executingTools: new Set(),
|
|
32
37
|
};
|
|
@@ -38,15 +43,24 @@ test('only a watchdog-stamped aborted settlement records an automatic connection
|
|
|
38
43
|
retry.publishSettledProviderFault(generation);
|
|
39
44
|
return { retry, generation };
|
|
40
45
|
};
|
|
41
|
-
const watchdog = settleAbort('watchdog-node', true);
|
|
46
|
+
const watchdog = settleAbort('watchdog-node', true, false);
|
|
42
47
|
const fault = readFault('watchdog-node');
|
|
43
48
|
assert.equal(fault?.link, 'pi→provider');
|
|
44
49
|
assert.equal(fault?.kind, 'connection');
|
|
45
50
|
assert.deepEqual(fault?.retry, { disposition: 'auto', by: 'daemon' });
|
|
46
51
|
assert.equal(watchdog.retry.isActiveAutoFault(fault), true);
|
|
47
52
|
assert.equal(watchdog.generation.stagedWatchdogAbort, false, 'settlement consumes the one-shot provenance');
|
|
48
|
-
settleAbort('human-node', false);
|
|
53
|
+
settleAbort('human-node', false, false);
|
|
49
54
|
assert.equal(readFault('human-node'), null, 'an otherwise-identical human abort must not arm recovery');
|
|
55
|
+
// Pi 0.84 currently serializes a host abort from `node yield` as an `error`
|
|
56
|
+
// settlement. Only the refresh-abort provenance suppresses that provider-looking error.
|
|
57
|
+
const refresh = settleAbort('refresh-node', false, true, 'error');
|
|
58
|
+
assert.equal(readFault('refresh-node'), null, 'a refresh-aborted error settlement must not enter provider fault recovery');
|
|
59
|
+
assert.equal(refresh.generation.stagedRefreshAbort, false, 'settlement consumes the one-shot refresh provenance');
|
|
60
|
+
settleAbort('provider-node', false, false, 'error');
|
|
61
|
+
const providerFault = readFault('provider-node');
|
|
62
|
+
assert.equal(providerFault?.kind, 'other', 'the same provider error without refresh provenance still classifies');
|
|
63
|
+
assert.deepEqual(providerFault?.retry, { disposition: 'fatal' }, 'the same provider error without refresh provenance still records its fault');
|
|
50
64
|
});
|
|
51
65
|
test('a failed retry settlement carries the episode forward instead of restarting it', () => {
|
|
52
66
|
// The episode marker a daemon retry records before re-prompting: original
|
|
@@ -68,6 +82,7 @@ test('a failed retry settlement carries the episode forward instead of restartin
|
|
|
68
82
|
messages: [{ role: 'assistant', stopReason: 'error', errorMessage: 'fetch failed' }],
|
|
69
83
|
},
|
|
70
84
|
stagedWatchdogAbort: false,
|
|
85
|
+
stagedRefreshAbort: false,
|
|
71
86
|
stagedOverflowFailure: null,
|
|
72
87
|
executingTools: new Set(),
|
|
73
88
|
};
|
package/dist/core/bash-jobs.d.ts
CHANGED
|
@@ -77,6 +77,12 @@ export declare function jobStartedAtMs(jobId: string): number | undefined;
|
|
|
77
77
|
* notice, the `node bash background` result/toast, and the attach cockpit)
|
|
78
78
|
* shows it: `<60s -> "12s"`; `60s-59m -> "3m 40s"`; `>=60m -> "1h 04m"`. */
|
|
79
79
|
export declare function formatBashElapsed(ms: number): string;
|
|
80
|
+
/** The command line worth SHOWING for a job. `cmd.sh` opens with the harness's
|
|
81
|
+
* injected environment preamble (`export CRTR_PREVIEW_RESULT_PATH=…`), so its
|
|
82
|
+
* literal first line names no command at all: skip leading assignment lines and
|
|
83
|
+
* return the first real one, whitespace-collapsed to a single line. A job whose
|
|
84
|
+
* command is nothing but assignments falls back to its first non-empty line. */
|
|
85
|
+
export declare function bashJobCommandLine(command: string): string;
|
|
80
86
|
export interface BashJobStatus {
|
|
81
87
|
jobId: string;
|
|
82
88
|
/** Full text of cmd.sh. */
|
package/dist/core/bash-jobs.js
CHANGED
|
@@ -189,6 +189,16 @@ export function formatBashElapsed(ms) {
|
|
|
189
189
|
const minutes = totalMinutes % 60;
|
|
190
190
|
return `${hours}h ${String(minutes).padStart(2, '0')}m`;
|
|
191
191
|
}
|
|
192
|
+
/** The command line worth SHOWING for a job. `cmd.sh` opens with the harness's
|
|
193
|
+
* injected environment preamble (`export CRTR_PREVIEW_RESULT_PATH=…`), so its
|
|
194
|
+
* literal first line names no command at all: skip leading assignment lines and
|
|
195
|
+
* return the first real one, whitespace-collapsed to a single line. A job whose
|
|
196
|
+
* command is nothing but assignments falls back to its first non-empty line. */
|
|
197
|
+
export function bashJobCommandLine(command) {
|
|
198
|
+
const lines = command.split('\n').map((line) => line.trim()).filter((line) => line !== '');
|
|
199
|
+
const real = lines.find((line) => !/^(export\s+)?[A-Za-z_][A-Za-z0-9_]*=/.test(line));
|
|
200
|
+
return (real ?? lines[0] ?? '').replace(/\s+/g, ' ');
|
|
201
|
+
}
|
|
192
202
|
/** Every live backgrounded bash job for this node — job.bg present, job.exit
|
|
193
203
|
* absent — whether handed off by the menu, the valve deadline, or broker
|
|
194
204
|
* termination. Read-only; never mutates. Skips malformed/partially-written
|
|
@@ -4,7 +4,7 @@ import assert from 'node:assert/strict';
|
|
|
4
4
|
import { mkdtempSync, rmSync, mkdirSync } from 'node:fs';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
import { atomicWriteJson, pageManifestPath } from '../../human/convention.js';
|
|
7
|
+
import { atomicWriteJson, bindTicketReplyRoute, pageManifestPath } from '../../human/convention.js';
|
|
8
8
|
import { createNode } from '../canvas.js';
|
|
9
9
|
import { closeDb } from '../db.js';
|
|
10
10
|
import { nodeDir } from '../paths.js';
|
|
@@ -22,16 +22,19 @@ function meta(id, cwd, extra) {
|
|
|
22
22
|
...extra,
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
+
// A ticket only counts as attention when it routes a reply back, so the fixture binds
|
|
26
|
+
// the reply route the same way `human send` does before writing the page manifest.
|
|
25
27
|
function seedPage(bridgeId) {
|
|
26
28
|
const dir = nodeDir(bridgeId);
|
|
27
29
|
mkdirSync(dir, { recursive: true });
|
|
30
|
+
bindTicketReplyRoute(dir, bridgeId);
|
|
28
31
|
atomicWriteJson(pageManifestPath(dir), {
|
|
29
|
-
schema: 'crtr.page/
|
|
30
|
-
dialect: '
|
|
32
|
+
schema: 'crtr.page/v2',
|
|
33
|
+
dialect: 'jsx',
|
|
31
34
|
title: bridgeId,
|
|
32
35
|
subtitle: 'Acknowledge this ticket so the request can proceed.',
|
|
33
|
-
placement: 'inline',
|
|
34
|
-
document: 'page.
|
|
36
|
+
delivery: { placement: 'inline', inbox: true, reply: true },
|
|
37
|
+
document: 'page.tsx',
|
|
35
38
|
steps: 1,
|
|
36
39
|
slots: [],
|
|
37
40
|
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// This is adversarial, not merely a clean-machine assertion: BEFORE calling
|
|
9
9
|
// the render functions, the test seeds real, CONFLICTING local state for the
|
|
10
10
|
// same node id `a` (a local focus, an active fault, telemetry, a goal file, a
|
|
11
|
-
// session transcript, and a pending human
|
|
11
|
+
// session transcript, and a pending human reply) — every historically-broken
|
|
12
12
|
// local read this suppression covers. If the production suppression ever
|
|
13
13
|
// regressed back to consulting local disk/SQLite for a remote source, the
|
|
14
14
|
// assertions below would observe the poisoned values instead of the safe
|
|
@@ -114,12 +114,12 @@ function seedPoisonedLocalState() {
|
|
|
114
114
|
const askDir = nodeDir('poison-ask');
|
|
115
115
|
mkdirSync(askDir, { recursive: true });
|
|
116
116
|
atomicWriteJson(pageManifestPath(askDir), {
|
|
117
|
-
schema: 'crtr.page/
|
|
118
|
-
dialect: '
|
|
117
|
+
schema: 'crtr.page/v2',
|
|
118
|
+
dialect: 'jsx',
|
|
119
119
|
title: 'poison',
|
|
120
120
|
subtitle: 'This local ticket must not affect the remote rendering.',
|
|
121
|
-
placement: 'inline',
|
|
122
|
-
document: 'page.
|
|
121
|
+
delivery: { placement: 'inline', inbox: true, reply: true },
|
|
122
|
+
document: 'page.tsx',
|
|
123
123
|
steps: 1,
|
|
124
124
|
slots: [],
|
|
125
125
|
});
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
// attention.ts — pending human-ticket counters across the canvas.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// node
|
|
6
|
-
// than duplicated attribution fields in page/review data.
|
|
7
|
-
import { basename } from 'node:path';
|
|
3
|
+
// Reply-bearing tickets carry an explicit bridge route. The bridge's parent is
|
|
4
|
+
// the asking node; standalone pages have no bridge and do not contribute to a
|
|
5
|
+
// node's pending-reply count.
|
|
8
6
|
import { scanInbox } from '../human/scan.js';
|
|
9
7
|
import { filterTerminalReviewTickets } from '../review/ticket-filter.js';
|
|
8
|
+
import { readTicketReplyRoute } from '../human/convention.js';
|
|
10
9
|
import { getNode, listNodes, view } from './canvas.js';
|
|
11
10
|
function askingNodeId(ticketDir) {
|
|
12
|
-
const
|
|
11
|
+
const replyRoute = readTicketReplyRoute(ticketDir);
|
|
12
|
+
if (replyRoute === null)
|
|
13
|
+
return null;
|
|
14
|
+
const bridge = getNode(replyRoute.bridge_node_id);
|
|
13
15
|
if (bridge === null)
|
|
14
16
|
return null;
|
|
15
17
|
return bridge.parent ?? bridge.node_id;
|
|
@@ -11,8 +11,14 @@ export type SortMode = 'tree' | 'attention' | 'relevance' | 'recency';
|
|
|
11
11
|
export declare const SORTS: readonly SortMode[];
|
|
12
12
|
/** Does a node belong to this tab's slice?
|
|
13
13
|
* All — every node.
|
|
14
|
-
* Live — active | idle
|
|
15
|
-
*
|
|
14
|
+
* Live — what you can pick up right now: active | idle, PLUS a resident
|
|
15
|
+
* agent that stopped without finishing (its broker exited, but
|
|
16
|
+
* Enter resumes the conversation in place — see `isFinishedWork`).
|
|
17
|
+
* Finished work never appears here.
|
|
18
|
+
* Dormant — done | dead | canceled: literally "no engine running". A stopped
|
|
19
|
+
* resident agent is deliberately on BOTH tabs — it is resumable
|
|
20
|
+
* (Live) and it is not running (Dormant); both are true and each
|
|
21
|
+
* tab is the one a different search starts from.
|
|
16
22
|
* Attention — needs a human: has pending asks OR is parked on a fault.
|
|
17
23
|
* Pinned — user-starred (a local, view-only preference; `isPinned`). */
|
|
18
24
|
export declare function tabPredicate(tab: Tab, row: DashboardRow, isPinned?: boolean): boolean;
|
|
@@ -4,19 +4,26 @@
|
|
|
4
4
|
// it is exhaustively unit-testable. The app layer (app.ts) wires the canvas data
|
|
5
5
|
// access (dashboardRowsAll / listNodes / subscriptionsOf) into buildTree, then
|
|
6
6
|
// drives flatten() on each keystroke.
|
|
7
|
-
import { statusRank } from '../node-order.js';
|
|
7
|
+
import { isFinishedWork, isLiveStatus, statusRank } from '../node-order.js';
|
|
8
8
|
export const TABS = ['All', 'Live', 'Dormant', 'Attention', 'Pinned'];
|
|
9
9
|
export const SORTS = ['tree', 'attention', 'relevance', 'recency'];
|
|
10
10
|
/** Does a node belong to this tab's slice?
|
|
11
11
|
* All — every node.
|
|
12
|
-
* Live — active | idle
|
|
13
|
-
*
|
|
12
|
+
* Live — what you can pick up right now: active | idle, PLUS a resident
|
|
13
|
+
* agent that stopped without finishing (its broker exited, but
|
|
14
|
+
* Enter resumes the conversation in place — see `isFinishedWork`).
|
|
15
|
+
* Finished work never appears here.
|
|
16
|
+
* Dormant — done | dead | canceled: literally "no engine running". A stopped
|
|
17
|
+
* resident agent is deliberately on BOTH tabs — it is resumable
|
|
18
|
+
* (Live) and it is not running (Dormant); both are true and each
|
|
19
|
+
* tab is the one a different search starts from.
|
|
14
20
|
* Attention — needs a human: has pending asks OR is parked on a fault.
|
|
15
21
|
* Pinned — user-starred (a local, view-only preference; `isPinned`). */
|
|
16
22
|
export function tabPredicate(tab, row, isPinned = false) {
|
|
17
23
|
switch (tab) {
|
|
18
24
|
case 'All': return true;
|
|
19
|
-
case 'Live': return row.status
|
|
25
|
+
case 'Live': return isLiveStatus(row.status)
|
|
26
|
+
|| !isFinishedWork({ lifecycle: row.lifecycle, finalized: row.finalized === true });
|
|
20
27
|
case 'Dormant': return row.status === 'done' || row.status === 'dead' || row.status === 'canceled';
|
|
21
28
|
case 'Attention': return row.asks > 0 || row.hanging != null;
|
|
22
29
|
case 'Pinned': return isPinned;
|
|
@@ -90,6 +90,10 @@ export declare function clearPid(nodeId: string): void;
|
|
|
90
90
|
export declare function migrateLegacyPidIdentities(captureIdentity?: (pid: number) => string | undefined): {
|
|
91
91
|
migrated: string[];
|
|
92
92
|
};
|
|
93
|
+
/** Every hydrated node whose durable profile identity equals `profileId`.
|
|
94
|
+
* Unlike `listNodes`, this includes unclaimed warm-pool spares: profile
|
|
95
|
+
* deletion owns every matching row, whether or not it is user-visible. */
|
|
96
|
+
export declare function listNodesByProfile(profileId: string): NodeMeta[];
|
|
93
97
|
/** All rows, optionally filtered by status. Unclaimed warm spares are excluded. */
|
|
94
98
|
export declare function listNodes(filter?: {
|
|
95
99
|
status?: NodeStatus | NodeStatus[];
|
|
@@ -306,6 +310,9 @@ export interface DeleteNodeResult {
|
|
|
306
310
|
* state are removed in one canvas write boundary. Best-effort dir removal
|
|
307
311
|
* follows the commit. */
|
|
308
312
|
export declare function deleteNode(nodeId: string): DeleteNodeResult;
|
|
313
|
+
/** Hard-delete a validated set of nodes in one row transaction, then remove
|
|
314
|
+
* their directories. The caller must tear down engines and viewers first. */
|
|
315
|
+
export declare function deleteNodes(nodeIds: readonly string[]): string[];
|
|
309
316
|
/** Retention sweep: remove TERMINAL nodes (status dead | done | canceled) whose
|
|
310
317
|
* `created` is older than `ttlDays`, bounding the otherwise-unbounded growth of
|
|
311
318
|
* node rows + dirs. Open managed-worktree nodes are retained so their checkout
|
|
@@ -336,6 +336,21 @@ export function migrateLegacyPidIdentities(captureIdentity = (pid) => capturePid
|
|
|
336
336
|
* yet — surfacing it would put a phantom empty conversation in the user's
|
|
337
337
|
* sidebar. Claiming deletes the marker, which is what makes the node visible. */
|
|
338
338
|
const NOT_A_SPARE = 'node_id NOT IN (SELECT node_id FROM warm_pool)';
|
|
339
|
+
/** Every hydrated node whose durable profile identity equals `profileId`.
|
|
340
|
+
* Unlike `listNodes`, this includes unclaimed warm-pool spares: profile
|
|
341
|
+
* deletion owns every matching row, whether or not it is user-visible. */
|
|
342
|
+
export function listNodesByProfile(profileId) {
|
|
343
|
+
const ids = openDb()
|
|
344
|
+
.prepare('SELECT node_id FROM nodes WHERE profile_id = ? ORDER BY created')
|
|
345
|
+
.all(profileId);
|
|
346
|
+
return ids.map(({ node_id }) => {
|
|
347
|
+
const meta = getNode(node_id);
|
|
348
|
+
if (meta === null) {
|
|
349
|
+
throw new Error(`profile deletion cannot hydrate indexed node ${node_id}`);
|
|
350
|
+
}
|
|
351
|
+
return meta;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
339
354
|
/** All rows, optionally filtered by status. Unclaimed warm spares are excluded. */
|
|
340
355
|
export function listNodes(filter) {
|
|
341
356
|
const db = openDb();
|
|
@@ -744,6 +759,28 @@ export function deleteNode(nodeId) {
|
|
|
744
759
|
rmSync(nodeDir(nodeId), { recursive: true, force: true });
|
|
745
760
|
return result;
|
|
746
761
|
}
|
|
762
|
+
/** Hard-delete a validated set of nodes in one row transaction, then remove
|
|
763
|
+
* their directories. The caller must tear down engines and viewers first. */
|
|
764
|
+
export function deleteNodes(nodeIds) {
|
|
765
|
+
const ids = [...new Set(nodeIds)];
|
|
766
|
+
if (ids.length === 0)
|
|
767
|
+
return [];
|
|
768
|
+
const blocked = ids.filter(hasOpenManagedWorktree);
|
|
769
|
+
if (blocked.length > 0) {
|
|
770
|
+
throw new Error(`refusing to delete nodes with open managed worktrees: ${blocked.join(', ')}`);
|
|
771
|
+
}
|
|
772
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
773
|
+
const deleted = withCanvasWrite((db) => {
|
|
774
|
+
const existing = db
|
|
775
|
+
.prepare(`SELECT node_id FROM nodes WHERE node_id IN (${placeholders}) ORDER BY created`)
|
|
776
|
+
.all(...ids).map(({ node_id }) => node_id);
|
|
777
|
+
db.prepare(`DELETE FROM nodes WHERE node_id IN (${placeholders})`).run(...ids);
|
|
778
|
+
return existing;
|
|
779
|
+
});
|
|
780
|
+
for (const id of deleted)
|
|
781
|
+
rmSync(nodeDir(id), { recursive: true, force: true });
|
|
782
|
+
return deleted;
|
|
783
|
+
}
|
|
747
784
|
/** Retention sweep: remove TERMINAL nodes (status dead | done | canceled) whose
|
|
748
785
|
* `created` is older than `ttlDays`, bounding the otherwise-unbounded growth of
|
|
749
786
|
* node rows + dirs. Open managed-worktree nodes are retained so their checkout
|
|
@@ -159,6 +159,15 @@ export declare function listCrons(scope: {
|
|
|
159
159
|
export declare function cancelCron(cron_id: string, options?: {
|
|
160
160
|
preserveRunningProcess?: boolean;
|
|
161
161
|
}): boolean;
|
|
162
|
+
/** Cron ids a hard profile deletion will remove: matching execution profile,
|
|
163
|
+
* plus every row anchored to a node being deleted. */
|
|
164
|
+
export declare function cronIdsForProfileDeletion(profileId: string, anchorNodeIds: readonly string[]): string[];
|
|
165
|
+
/** Kill any leased matching/anchored runs, then delete their rows together. */
|
|
166
|
+
export declare function deleteCronsForProfile(profileId: string, anchorNodeIds: readonly string[]): string[];
|
|
167
|
+
/** Cron ids whose snapshotted profile will be detached. */
|
|
168
|
+
export declare function cronIdsForProfileDetach(profileId: string): string[];
|
|
169
|
+
/** Preserve matching crons while removing their deleted profile dependency. */
|
|
170
|
+
export declare function globalizeCronsForProfile(profileId: string): string[];
|
|
162
171
|
/** DELETE one cron by id — one-shot consumption after its run settles.
|
|
163
172
|
* Returns whether a live row was removed, so a consumer racing an explicit
|
|
164
173
|
* cancel can no-op instead of double-settling. */
|
|
@@ -191,6 +191,57 @@ export function cancelCron(cron_id, options) {
|
|
|
191
191
|
return true;
|
|
192
192
|
});
|
|
193
193
|
}
|
|
194
|
+
function profileCronWhere(profileId, anchorNodeIds) {
|
|
195
|
+
const anchors = [...new Set(anchorNodeIds)];
|
|
196
|
+
if (anchors.length === 0)
|
|
197
|
+
return { sql: 'profile = ?', params: [profileId] };
|
|
198
|
+
return {
|
|
199
|
+
sql: `profile = ? OR anchor_node IN (${anchors.map(() => '?').join(',')})`,
|
|
200
|
+
params: [profileId, ...anchors],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** Cron ids a hard profile deletion will remove: matching execution profile,
|
|
204
|
+
* plus every row anchored to a node being deleted. */
|
|
205
|
+
export function cronIdsForProfileDeletion(profileId, anchorNodeIds) {
|
|
206
|
+
const where = profileCronWhere(profileId, anchorNodeIds);
|
|
207
|
+
return openDb()
|
|
208
|
+
.prepare(`SELECT cron_id FROM crons WHERE ${where.sql} ORDER BY created, cron_id`)
|
|
209
|
+
.all(...where.params).map(({ cron_id }) => cron_id);
|
|
210
|
+
}
|
|
211
|
+
/** Kill any leased matching/anchored runs, then delete their rows together. */
|
|
212
|
+
export function deleteCronsForProfile(profileId, anchorNodeIds) {
|
|
213
|
+
return withCanvasWrite((db) => {
|
|
214
|
+
const where = profileCronWhere(profileId, anchorNodeIds);
|
|
215
|
+
const rows = db
|
|
216
|
+
.prepare(`SELECT * FROM crons WHERE ${where.sql} ORDER BY created, cron_id`)
|
|
217
|
+
.all(...where.params).map(cronFrom);
|
|
218
|
+
for (const cron of rows) {
|
|
219
|
+
if (cron.run_state === 'running')
|
|
220
|
+
killLeasedCronRun(cron);
|
|
221
|
+
}
|
|
222
|
+
db.prepare(`DELETE FROM crons WHERE ${where.sql}`).run(...where.params);
|
|
223
|
+
return rows.map(({ cron_id }) => cron_id);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/** Cron ids whose snapshotted profile will be detached. */
|
|
227
|
+
export function cronIdsForProfileDetach(profileId) {
|
|
228
|
+
return openDb()
|
|
229
|
+
.prepare('SELECT cron_id FROM crons WHERE profile = ? ORDER BY created, cron_id')
|
|
230
|
+
.all(profileId).map(({ cron_id }) => cron_id);
|
|
231
|
+
}
|
|
232
|
+
/** Preserve matching crons while removing their deleted profile dependency. */
|
|
233
|
+
export function globalizeCronsForProfile(profileId) {
|
|
234
|
+
return withCanvasWrite((db) => {
|
|
235
|
+
const ids = db
|
|
236
|
+
.prepare('SELECT cron_id FROM crons WHERE profile = ? ORDER BY created, cron_id')
|
|
237
|
+
.all(profileId).map(({ cron_id }) => cron_id);
|
|
238
|
+
if (ids.length > 0) {
|
|
239
|
+
db.prepare("UPDATE crons SET profile = NULL, scope = 'global', updated = ? WHERE profile = ?")
|
|
240
|
+
.run(new Date().toISOString(), profileId);
|
|
241
|
+
}
|
|
242
|
+
return ids;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
194
245
|
/** DELETE one cron by id — one-shot consumption after its run settles.
|
|
195
246
|
* Returns whether a live row was removed, so a consumer racing an explicit
|
|
196
247
|
* cancel can no-op instead of double-settling. */
|
|
@@ -1,3 +1,24 @@
|
|
|
1
|
-
import type { NodeStatus } from './types.js';
|
|
1
|
+
import type { NodeStatus, Lifecycle } from './types.js';
|
|
2
2
|
/** Sort live work before terminal work; unknown statuses sort last. */
|
|
3
3
|
export declare function statusRank(status: NodeStatus | undefined): number;
|
|
4
|
+
/** True when the node has a broker the daemon supervises — the statuses that
|
|
5
|
+
* mean "a live engine is (or should be) running right now". */
|
|
6
|
+
export declare function isLiveStatus(status: NodeStatus | undefined): boolean;
|
|
7
|
+
/** True when a node's work is FINISHED — a terminal one-shot worker, or any node
|
|
8
|
+
* that pushed a final report.
|
|
9
|
+
*
|
|
10
|
+
* This is deliberately NOT "is it running": a non-live status alone conflates two
|
|
11
|
+
* unrelated situations. Finished work is archive — opening it is inspection, and
|
|
12
|
+
* relaunching it puts a completed worker back to work (the stop-guard reprompts a
|
|
13
|
+
* terminal node that never pushed a final). A RESIDENT node with no final report
|
|
14
|
+
* is the opposite: its `done` is only `markCleanExitDone` recording a clean broker
|
|
15
|
+
* exit, which the stop-guard itself rules a legitimate stop (`reason: 'dormant'`),
|
|
16
|
+
* so it never finished anything and resuming it is simply reopening a conversation.
|
|
17
|
+
*
|
|
18
|
+
* `lifecycle` is optional because DashboardRow only carries it on the full browser
|
|
19
|
+
* snapshot; absent, this answers `true` (treat as finished), keeping the caller on
|
|
20
|
+
* the conservative inspect-don't-revive path. */
|
|
21
|
+
export declare function isFinishedWork(node: {
|
|
22
|
+
lifecycle?: Lifecycle;
|
|
23
|
+
finalized: boolean;
|
|
24
|
+
}): boolean;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
// node-order.ts — canonical
|
|
1
|
+
// node-order.ts — canonical ordering and status/lifecycle classification for
|
|
2
|
+
// canvas nodes. Pure (type-only imports) so every surface — the browser model,
|
|
3
|
+
// command leaves, renderers — can share one answer to "is this node live?" and
|
|
4
|
+
// "did this node finish?" without reaching the db.
|
|
2
5
|
/** Sort live work before terminal work; unknown statuses sort last. */
|
|
3
6
|
export function statusRank(status) {
|
|
4
7
|
switch (status) {
|
|
@@ -10,3 +13,25 @@ export function statusRank(status) {
|
|
|
10
13
|
default: return 5;
|
|
11
14
|
}
|
|
12
15
|
}
|
|
16
|
+
/** True when the node has a broker the daemon supervises — the statuses that
|
|
17
|
+
* mean "a live engine is (or should be) running right now". */
|
|
18
|
+
export function isLiveStatus(status) {
|
|
19
|
+
return status === 'active' || status === 'idle';
|
|
20
|
+
}
|
|
21
|
+
/** True when a node's work is FINISHED — a terminal one-shot worker, or any node
|
|
22
|
+
* that pushed a final report.
|
|
23
|
+
*
|
|
24
|
+
* This is deliberately NOT "is it running": a non-live status alone conflates two
|
|
25
|
+
* unrelated situations. Finished work is archive — opening it is inspection, and
|
|
26
|
+
* relaunching it puts a completed worker back to work (the stop-guard reprompts a
|
|
27
|
+
* terminal node that never pushed a final). A RESIDENT node with no final report
|
|
28
|
+
* is the opposite: its `done` is only `markCleanExitDone` recording a clean broker
|
|
29
|
+
* exit, which the stop-guard itself rules a legitimate stop (`reason: 'dormant'`),
|
|
30
|
+
* so it never finished anything and resuming it is simply reopening a conversation.
|
|
31
|
+
*
|
|
32
|
+
* `lifecycle` is optional because DashboardRow only carries it on the full browser
|
|
33
|
+
* snapshot; absent, this answers `true` (treat as finished), keeping the caller on
|
|
34
|
+
* the conservative inspect-don't-revive path. */
|
|
35
|
+
export function isFinishedWork(node) {
|
|
36
|
+
return node.lifecycle !== 'resident' || node.finalized;
|
|
37
|
+
}
|
|
@@ -34,6 +34,11 @@ export interface DashboardRow {
|
|
|
34
34
|
* agent you come back to. Drives the browser's resident-only lifecycle filter.
|
|
35
35
|
* Only populated by dashboardRowsAll (the browser snapshot). */
|
|
36
36
|
lifecycle?: Lifecycle;
|
|
37
|
+
/** True when the node pushed a final report. With `lifecycle` this separates
|
|
38
|
+
* work that FINISHED from a resident agent that merely stopped, which the
|
|
39
|
+
* status alone cannot (see `isFinishedWork` in node-order.ts). Only populated
|
|
40
|
+
* by dashboardRowsAll (the browser snapshot). */
|
|
41
|
+
finalized?: boolean;
|
|
37
42
|
/** "Most recent activity" sort key — epoch ms. The cheap boot stats the node's
|
|
38
43
|
* job/telemetry.json (rewritten on every turn_end at a DETERMINISTIC path, so
|
|
39
44
|
* no meta read), falling back to `created`'s epoch when a node never ran a turn.
|
|
@@ -372,6 +372,7 @@ export function dashboardRowFromNodeRow(row, focusedNodeIds, remote) {
|
|
|
372
372
|
profile_id: row.profile_id,
|
|
373
373
|
created: row.created,
|
|
374
374
|
lifecycle: row.lifecycle,
|
|
375
|
+
finalized: row.final_report != null,
|
|
375
376
|
mtimeMs: sessionMtime(row.node_id, row.created, remote),
|
|
376
377
|
streaming: !remote && hanging === null && live ? isStreaming(row.node_id, row.pi_pid) : false,
|
|
377
378
|
hanging,
|
|
@@ -49,6 +49,9 @@ export interface LaunchSpec {
|
|
|
49
49
|
}
|
|
50
50
|
export interface ManagedWorktree {
|
|
51
51
|
state: 'open' | 'closed';
|
|
52
|
+
/** A closed worktree remains registered until its owner has exited, so Git
|
|
53
|
+
* never deletes the caller's live checkout beneath it. */
|
|
54
|
+
cleanup?: 'pending' | 'complete';
|
|
52
55
|
path: string;
|
|
53
56
|
branch: string;
|
|
54
57
|
repo_root: string;
|
|
@@ -57,10 +60,13 @@ export interface ManagedWorktree {
|
|
|
57
60
|
created: string;
|
|
58
61
|
closed?: string;
|
|
59
62
|
}
|
|
60
|
-
/** Immutable
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
+
/** Immutable companion provenance. The origin session/leaf/model coordinates
|
|
64
|
+
* remain canonical in the review row rather than being duplicated into node
|
|
65
|
+
* metadata. `kind` names the surface the companion serves: absent or `review`
|
|
66
|
+
* is a human document review; `page_feedback` binds a page ticket's feedback
|
|
67
|
+
* conversation, with review_id = ticket id and target_file = the page source. */
|
|
63
68
|
export interface ReviewBinding {
|
|
69
|
+
kind?: 'review' | 'page_feedback';
|
|
64
70
|
review_id: string;
|
|
65
71
|
origin_node_id: string;
|
|
66
72
|
branch_file: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ValidatedCommandManifest } from '../command-manifests/manifest.js';
|
|
2
|
-
import type { KindConfig } from '../../types.js';
|
|
2
|
+
import type { KindConfig, PageComponentRegistration } from '../../types.js';
|
|
3
3
|
export interface BundleIssue {
|
|
4
4
|
code: string;
|
|
5
5
|
path?: string;
|
|
@@ -15,6 +15,11 @@ export interface ValidatedBundle {
|
|
|
15
15
|
* manifest, where `readMergedLaunchConfig` layers it — see
|
|
16
16
|
* `PluginManifest.kinds`. */
|
|
17
17
|
kinds?: Record<string, Partial<KindConfig>>;
|
|
18
|
+
/** Page-component contributions declared in `bundle.json` (optional). The
|
|
19
|
+
* installer copies this validated block into the synthesized plugin
|
|
20
|
+
* manifest, where `resolvePageComponents` reads it — see
|
|
21
|
+
* `PluginManifest.page_components`. */
|
|
22
|
+
page_components?: PageComponentRegistration[];
|
|
18
23
|
commands: ValidatedCommandManifest;
|
|
19
24
|
commandsBytes: Uint8Array;
|
|
20
25
|
directories: ReadonlyArray<{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Parser } from 'tar';
|
|
2
2
|
import { validateCommandManifest } from '../command-manifests/manifest.js';
|
|
3
3
|
import { invalidPluginKindsReasons } from '../config.js';
|
|
4
|
+
import { invalidPageComponentsReasons, normalizePageComponents } from '../human/page-catalog.js';
|
|
4
5
|
function issue(code, message, received, expected, next, path) {
|
|
5
6
|
return { code, message, received, expected, next, ...(path !== undefined ? { path } : {}) };
|
|
6
7
|
}
|
|
@@ -143,24 +144,35 @@ function parseBundleMetadata(bytes) {
|
|
|
143
144
|
};
|
|
144
145
|
}
|
|
145
146
|
const record = raw;
|
|
146
|
-
const unknownKeys = Object.keys(record).filter((key) => key !== 'bundleVersion' && key !== 'kinds');
|
|
147
|
+
const unknownKeys = Object.keys(record).filter((key) => key !== 'bundleVersion' && key !== 'kinds' && key !== 'page_components');
|
|
147
148
|
if (record['bundleVersion'] !== 1 || unknownKeys.length > 0) {
|
|
148
149
|
return {
|
|
149
|
-
issue: bundleInvalid('bundle.json must contain bundleVersion 1 and at most
|
|
150
|
+
issue: bundleInvalid('bundle.json must contain bundleVersion 1 and at most kinds and page_components blocks', JSON.stringify(raw), 'an object with bundleVersion set to 1 and optionally kinds and page_components', 'Set bundle.json to {"bundleVersion":1} plus an optional kinds object and page_components array.', 'bundle.json'),
|
|
150
151
|
};
|
|
151
152
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
153
|
+
// Install-time strictness: a bad block fails the install loudly here, because
|
|
154
|
+
// the read side (`mergeKinds`) drops invalid kind entries silently and would
|
|
155
|
+
// otherwise ship a kind that never registers.
|
|
156
|
+
if (record['kinds'] !== undefined) {
|
|
157
|
+
const reasons = invalidPluginKindsReasons(record['kinds']);
|
|
158
|
+
if (reasons.length > 0) {
|
|
159
|
+
return {
|
|
160
|
+
issue: bundleInvalid('bundle.json kinds block is invalid', reasons.join('; '), 'each entry a full KindConfig (whenToUse required) or a sparse patch of launch fields', 'Fix the kinds entries in bundle.json.', 'bundle.json'),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
162
163
|
}
|
|
163
|
-
|
|
164
|
+
if (record['page_components'] !== undefined) {
|
|
165
|
+
const reasons = invalidPageComponentsReasons(record['page_components']);
|
|
166
|
+
if (reasons.length > 0) {
|
|
167
|
+
return {
|
|
168
|
+
issue: bundleInvalid('bundle.json page_components block is invalid', reasons.join('; '), 'an array of page component registrations, each a kind string or {kind, description?, useWhen?, doc?, display?}', 'Fix the page_components entries in bundle.json.', 'bundle.json'),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
...(record['kinds'] !== undefined ? { kinds: record['kinds'] } : {}),
|
|
174
|
+
...(record['page_components'] !== undefined ? { page_components: normalizePageComponents(record['page_components']) } : {}),
|
|
175
|
+
};
|
|
164
176
|
}
|
|
165
177
|
function parseCommands(bytes, reservedCoreNames, coreCommandPaths) {
|
|
166
178
|
let raw;
|
|
@@ -206,6 +218,7 @@ export async function validatePluginBundle(archive, options) {
|
|
|
206
218
|
bundle: {
|
|
207
219
|
bundleVersion: 1,
|
|
208
220
|
...(metadata.kinds !== undefined ? { kinds: metadata.kinds } : {}),
|
|
221
|
+
...(metadata.page_components !== undefined ? { page_components: metadata.page_components } : {}),
|
|
209
222
|
commands: commandValidation.commands,
|
|
210
223
|
commandsBytes: commands.bytes,
|
|
211
224
|
directories: parsed.members
|