@north-light/crouter 0.3.197 → 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 +19 -19
- 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 +126 -33
- 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 +8 -8
- 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 +3 -3
- 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/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
- package/dist/clients/attach/chrome/bash-jobs.js +3 -3
- package/dist/clients/attach/render/chat-view.d.ts +29 -9
- package/dist/clients/attach/render/chat-view.js +64 -35
- package/dist/clients/attach/render/markdown-source.js +0 -6
- 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/render/tool-calls.js +16 -2
- 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 -982
- 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 -14
- package/dist/clients/inbox/controller.js +10 -16
- package/dist/clients/inbox/page-adapter.d.ts +3 -4
- 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 +101 -36
- package/dist/clients/inbox/tui/slots.d.ts +4 -4
- package/dist/clients/inbox/tui/slots.js +19 -44
- package/dist/clients/inbox/tui/types.d.ts +6 -10
- package/dist/clients/inbox/tui.js +2 -2
- package/dist/commands/__tests__/human.test.js +118 -24
- package/dist/commands/__tests__/node-message.test.js +14 -0
- package/dist/commands/attention.js +4 -4
- package/dist/commands/canvas-browse.js +1 -1
- 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/{doc.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 +17 -8
- package/dist/commands/human/prompts.js +217 -226
- package/dist/commands/human/queue.js +57 -88
- package/dist/commands/human/shared.d.ts +4 -15
- package/dist/commands/human/shared.js +19 -61
- package/dist/commands/human.js +32 -19
- 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__/daemon-boot.test.js +7 -4
- package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
- 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 +41 -45
- 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/broker-crash-teardown.test.js +13 -11
- package/dist/core/__tests__/seam/dormancy-release.test.js +103 -4
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -16
- 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/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +21 -3
- package/dist/core/canvas/browse/model.d.ts +8 -2
- package/dist/core/canvas/browse/model.js +11 -4
- package/dist/core/canvas/browse/render.d.ts +4 -0
- package/dist/core/canvas/browse/render.js +5 -1
- package/dist/core/canvas/canvas.d.ts +33 -0
- package/dist/core/canvas/canvas.js +92 -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 +8 -12
- package/dist/core/config.d.ts +47 -1
- package/dist/core/config.js +138 -4
- package/dist/core/help.d.ts +3 -0
- package/dist/core/help.js +1 -1
- package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-catalog.test.js +20 -0
- package/dist/core/human/__tests__/page-tickets.test.js +68 -34
- package/dist/core/human/__tests__/page.test.js +60 -119
- package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -16
- package/dist/core/human/answer-text.d.ts +3 -0
- package/dist/core/human/answer-text.js +98 -0
- package/dist/core/human/answer.d.ts +53 -0
- package/dist/core/human/answer.js +119 -0
- package/dist/core/human/component-docs.d.ts +18 -0
- package/dist/core/human/component-docs.js +434 -0
- 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 +56 -5
- package/dist/core/human/page-catalog.js +151 -22
- 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 +48 -116
- package/dist/core/human/page-schema.js +100 -72
- package/dist/core/human/page-synth.d.ts +2 -4
- package/dist/core/human/page-synth.js +20 -7
- package/dist/core/human/page.d.ts +13 -21
- package/dist/core/human/page.js +147 -391
- package/dist/core/human/review-schema.d.ts +1 -0
- package/dist/core/human/review-schema.js +1 -1
- 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 -3
- package/dist/core/human/scan.js +21 -21
- package/dist/core/human/summary.d.ts +2 -6
- package/dist/core/human/summary.js +3 -59
- package/dist/core/human/tickets.d.ts +22 -15
- package/dist/core/human/tickets.js +90 -28
- package/dist/core/human/types.d.ts +14 -2
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +1 -0
- 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 +12 -4
- 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/boot-root.js +6 -1
- 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 +25 -12
- package/dist/core/runtime/busy.d.ts +4 -3
- package/dist/core/runtime/busy.js +4 -3
- 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/spawn.js +4 -4
- 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 +36 -2
- package/dist/core/user-settings.js +56 -14
- 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 +48 -109
- 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 +29 -17
- package/dist/daemon/crtrd.js +1 -1
- package/dist/daemon/fleet.d.ts +22 -10
- package/dist/daemon/fleet.js +43 -24
- package/dist/daemon/human/finish.d.ts +12 -6
- package/dist/daemon/human/finish.js +134 -54
- package/dist/daemon/human/sweep.js +9 -3
- package/dist/daemon/profile-delete.d.ts +7 -0
- package/dist/daemon/profile-delete.js +306 -0
- package/dist/daemon/reconcilers/broker-supervision.js +14 -9
- package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
- package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
- package/dist/daemon/reconcilers/live-obligation.js +30 -2
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
- package/dist/daemon/reconcilers/storage-maintenance.js +28 -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/daemon/review/sweep.js +12 -1
- 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.d.ts +9 -1
- package/dist/pi-extensions/canvas-bash-valve.js +31 -13
- 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 +61 -5
- package/dist/types.js +8 -2
- package/package.json +4 -4
- package/runtime.lock.json +5 -33
- package/scripts/install-runtime.mjs +19 -24
- 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/clients/attach/render/html-markdown.d.ts +0 -13
- package/dist/clients/attach/render/html-markdown.js +0 -206
- package/dist/commands/human/doc.js +0 -144
- package/dist/commands/human/inbox.d.ts +0 -2
- package/dist/core/human/__tests__/page-render.test.js +0 -49
- package/dist/core/human/markdown-html.d.ts +0 -8
- package/dist/core/human/markdown-html.js +0 -581
- package/dist/core/human/page-render.d.ts +0 -3
- package/dist/core/human/page-render.js +0 -131
- package/dist/pages/bundle.css +0 -1
- package/dist/pages/bundle.js +0 -1347
- package/dist/pages/comments.d.ts +0 -43
- package/dist/pages/comments.js +0 -125
- package/dist/pages/elements/cards.d.ts +0 -17
- package/dist/pages/elements/cards.js +0 -802
- package/dist/pages/elements/chart.js +0 -648
- package/dist/pages/elements/options.d.ts +0 -21
- package/dist/pages/elements/options.js +0 -629
- package/dist/pages/elements/pages.d.ts +0 -24
- package/dist/pages/elements/pages.js +0 -355
- package/dist/pages/elements/slot.d.ts +0 -21
- package/dist/pages/elements/slot.js +0 -105
- package/dist/pages/elements/table.d.ts +0 -19
- package/dist/pages/elements/table.js +0 -935
- package/dist/pages/elements/text.d.ts +0 -25
- package/dist/pages/elements/text.js +0 -968
- package/dist/pages/entry.d.ts +0 -8
- package/dist/pages/entry.js +0 -8
- package/dist/pages/host.d.ts +0 -158
- package/dist/pages/host.js +0 -182
- package/dist/pages/register.d.ts +0 -2
- package/dist/pages/register.js +0 -3
- 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__/page-render.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
- /package/dist/{pages/elements/chart.d.ts → core/__tests__/plugin-page-components.test.d.ts} +0 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { cronIdsForProfileDeletion, cronIdsForProfileDetach, deleteCronsForProfile, deleteNodes, getNode, getRow, globalizeCronsForProfile, listNodesByProfile, nodesRoot, reviewDir, } from '../core/canvas/index.js';
|
|
4
|
+
import { usage } from '../core/errors.js';
|
|
5
|
+
import { emitEvent } from '../core/events/emit.js';
|
|
6
|
+
import { clearDefaultProfileId, defaultProfileDirs, } from '../core/profiles/default-binding.js';
|
|
7
|
+
import { reserveProfileDeletion } from '../core/profiles/deletion-reservation.js';
|
|
8
|
+
import { deleteProfile, loadProfileManifest, } from '../core/profiles/manifest.js';
|
|
9
|
+
import { deleteReviewsByOriginNodes, reviewIdsByOriginNodes, } from '../core/review/store.js';
|
|
10
|
+
import { boundFleet } from '../core/runtime/fleet.js';
|
|
11
|
+
import { headlessBrokerHost } from '../core/runtime/host.js';
|
|
12
|
+
import { transition } from '../core/runtime/lifecycle.js';
|
|
13
|
+
import { tearDownNode } from '../core/runtime/placement.js';
|
|
14
|
+
import { reshapeNode } from '../core/runtime/promote.js';
|
|
15
|
+
import { reviveNode } from '../core/runtime/revive.js';
|
|
16
|
+
const SELF_DELETE_GRACE_MS = 1_000;
|
|
17
|
+
const TICKET_FILES = new Set([
|
|
18
|
+
'page.json',
|
|
19
|
+
'page.tsx',
|
|
20
|
+
'page.html',
|
|
21
|
+
'page.js',
|
|
22
|
+
'review.json',
|
|
23
|
+
'response.json',
|
|
24
|
+
'progress.json',
|
|
25
|
+
'claim.json',
|
|
26
|
+
'reply-route.json',
|
|
27
|
+
'feedback-comments.json',
|
|
28
|
+
'feedback.json',
|
|
29
|
+
'.ticket-lock',
|
|
30
|
+
]);
|
|
31
|
+
const pending = new Map();
|
|
32
|
+
function requestObject(body) {
|
|
33
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
34
|
+
throw usage('profile deletion requires a JSON request body', {
|
|
35
|
+
error: 'force_required',
|
|
36
|
+
message: 'profile deletion requires --force',
|
|
37
|
+
next: 'Re-run `crtr profile delete <profile> --force` after reviewing its effects.',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return body;
|
|
41
|
+
}
|
|
42
|
+
function validateRequest(body) {
|
|
43
|
+
const value = requestObject(body);
|
|
44
|
+
if (value['force'] !== true) {
|
|
45
|
+
throw usage('profile deletion requires --force', {
|
|
46
|
+
error: 'force_required',
|
|
47
|
+
message: 'profile deletion requires --force',
|
|
48
|
+
next: 'Re-run `crtr profile delete <profile> --force` after reviewing its effects.',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (value['detach'] !== undefined && typeof value['detach'] !== 'boolean') {
|
|
52
|
+
throw usage('detach must be a boolean', {
|
|
53
|
+
error: 'invalid_detach',
|
|
54
|
+
message: 'detach must be a boolean',
|
|
55
|
+
next: 'Use --detach as a presence-only flag, or omit it.',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const caller = value['caller_node_id'];
|
|
59
|
+
if (caller !== undefined && caller !== null && (typeof caller !== 'string' || caller === '')) {
|
|
60
|
+
throw usage('caller_node_id must be a nonempty string or null');
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
force: true,
|
|
64
|
+
...(value['detach'] === true ? { detach: true } : {}),
|
|
65
|
+
...(caller === undefined ? {} : { caller_node_id: caller }),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function manifestSourceNodeId(path) {
|
|
69
|
+
if (!existsSync(path))
|
|
70
|
+
return null;
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
73
|
+
return typeof parsed.source?.nodeId === 'string' && parsed.source.nodeId !== ''
|
|
74
|
+
? parsed.source.nodeId
|
|
75
|
+
: null;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function ticketCleanupForNodes(nodeIds) {
|
|
82
|
+
const wanted = new Set(nodeIds);
|
|
83
|
+
const root = nodesRoot();
|
|
84
|
+
if (wanted.size === 0 || !existsSync(root))
|
|
85
|
+
return [];
|
|
86
|
+
const cleanup = [];
|
|
87
|
+
for (const dirent of readdirSync(root, { withFileTypes: true })) {
|
|
88
|
+
if (!dirent.isDirectory())
|
|
89
|
+
continue;
|
|
90
|
+
const dir = join(root, dirent.name);
|
|
91
|
+
const sourceIds = [
|
|
92
|
+
manifestSourceNodeId(join(dir, 'page.json')),
|
|
93
|
+
manifestSourceNodeId(join(dir, 'review.json')),
|
|
94
|
+
];
|
|
95
|
+
if (!sourceIds.some((sourceId) => sourceId !== null && wanted.has(sourceId)))
|
|
96
|
+
continue;
|
|
97
|
+
cleanup.push({
|
|
98
|
+
dir,
|
|
99
|
+
wholeDirectory: wanted.has(dirent.name) || getRow(dirent.name) === null,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return cleanup.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
103
|
+
}
|
|
104
|
+
function isTicketOwnedResidue(name) {
|
|
105
|
+
if (TICKET_FILES.has(name))
|
|
106
|
+
return true;
|
|
107
|
+
for (const base of TICKET_FILES) {
|
|
108
|
+
if (name.startsWith(`${base}.`) && (name.endsWith('.tmp') || name.endsWith('.lock'))) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
function cleanTickets(tickets) {
|
|
115
|
+
let cleaned = 0;
|
|
116
|
+
for (const ticket of tickets) {
|
|
117
|
+
if (!existsSync(ticket.dir))
|
|
118
|
+
continue;
|
|
119
|
+
if (ticket.wholeDirectory) {
|
|
120
|
+
rmSync(ticket.dir, { recursive: true, force: true });
|
|
121
|
+
cleaned += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
for (const dirent of readdirSync(ticket.dir, { withFileTypes: true })) {
|
|
125
|
+
if (!isTicketOwnedResidue(dirent.name))
|
|
126
|
+
continue;
|
|
127
|
+
rmSync(join(ticket.dir, dirent.name), { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
cleaned += 1;
|
|
130
|
+
}
|
|
131
|
+
return cleaned;
|
|
132
|
+
}
|
|
133
|
+
function openWorktreeError(nodes) {
|
|
134
|
+
const blocked = nodes
|
|
135
|
+
.filter((node) => node.managed_worktree?.state === 'open')
|
|
136
|
+
.map((node) => ({ node_id: node.node_id, path: node.managed_worktree.path }));
|
|
137
|
+
if (blocked.length === 0)
|
|
138
|
+
return null;
|
|
139
|
+
const message = `profile deletion is blocked by ${blocked.length} open managed worktree${blocked.length === 1 ? '' : 's'}`;
|
|
140
|
+
return usage(message, {
|
|
141
|
+
error: 'open_managed_worktree',
|
|
142
|
+
message,
|
|
143
|
+
nodes: blocked,
|
|
144
|
+
next: `Close ${blocked.map(({ node_id }) => node_id).join(', ')} with \`crtr node worktree close <node>\`, then retry.`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function prepareDeletion(entry, request) {
|
|
148
|
+
const nodes = listNodesByProfile(entry.profileId);
|
|
149
|
+
const blocked = openWorktreeError(nodes);
|
|
150
|
+
if (blocked !== null)
|
|
151
|
+
throw blocked;
|
|
152
|
+
const nodeIds = nodes.map(({ node_id }) => node_id);
|
|
153
|
+
const mode = request.detach === true ? 'detached' : 'deleted';
|
|
154
|
+
return {
|
|
155
|
+
entry,
|
|
156
|
+
mode,
|
|
157
|
+
nodes,
|
|
158
|
+
nodeIds,
|
|
159
|
+
tickets: mode === 'deleted' ? ticketCleanupForNodes(nodeIds) : [],
|
|
160
|
+
cronIds: mode === 'deleted'
|
|
161
|
+
? cronIdsForProfileDeletion(entry.profileId, nodeIds)
|
|
162
|
+
: cronIdsForProfileDetach(entry.profileId),
|
|
163
|
+
reviewIds: mode === 'deleted' ? reviewIdsByOriginNodes(nodeIds) : [],
|
|
164
|
+
defaultDirs: defaultProfileDirs(entry.profileId),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function assertDeletionPlanPreconditions(plan) {
|
|
168
|
+
const currentNodes = plan.nodeIds.flatMap((nodeId) => {
|
|
169
|
+
const node = getNode(nodeId);
|
|
170
|
+
return node === null ? [] : [node];
|
|
171
|
+
});
|
|
172
|
+
const blocked = openWorktreeError(currentNodes);
|
|
173
|
+
if (blocked !== null)
|
|
174
|
+
throw blocked;
|
|
175
|
+
}
|
|
176
|
+
function resultForPlan(plan) {
|
|
177
|
+
return {
|
|
178
|
+
profile_id: plan.entry.profileId,
|
|
179
|
+
name: plan.entry.manifest.name,
|
|
180
|
+
mode: plan.mode,
|
|
181
|
+
nodes: plan.nodeIds.length,
|
|
182
|
+
crons: plan.cronIds.length,
|
|
183
|
+
tickets: plan.tickets.length,
|
|
184
|
+
reviews: plan.reviewIds.length,
|
|
185
|
+
default_pins: plan.defaultDirs.length,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async function detachProfile(plan) {
|
|
189
|
+
assertDeletionPlanPreconditions(plan);
|
|
190
|
+
const existingNodeIds = plan.nodeIds.filter((nodeId) => getNode(nodeId) !== null);
|
|
191
|
+
const fleet = boundFleet();
|
|
192
|
+
const live = existingNodeIds.flatMap((nodeId) => {
|
|
193
|
+
const entry = fleet.get(nodeId);
|
|
194
|
+
return entry === undefined ? [] : [{ nodeId, exited: entry.handle.exited }];
|
|
195
|
+
});
|
|
196
|
+
const restartNodeIds = [];
|
|
197
|
+
for (const { nodeId } of live) {
|
|
198
|
+
const current = getNode(nodeId);
|
|
199
|
+
if (current?.status === 'active' || current?.status === 'idle') {
|
|
200
|
+
transition(nodeId, 'release');
|
|
201
|
+
restartNodeIds.push(nodeId);
|
|
202
|
+
}
|
|
203
|
+
headlessBrokerHost.teardown(nodeId);
|
|
204
|
+
}
|
|
205
|
+
await Promise.all(live.map(({ exited }) => exited));
|
|
206
|
+
const detachedNodeIds = [];
|
|
207
|
+
for (const nodeId of existingNodeIds) {
|
|
208
|
+
if (getNode(nodeId) === null)
|
|
209
|
+
continue;
|
|
210
|
+
reshapeNode(nodeId, { profileId: null });
|
|
211
|
+
detachedNodeIds.push(nodeId);
|
|
212
|
+
}
|
|
213
|
+
const cronIds = globalizeCronsForProfile(plan.entry.profileId);
|
|
214
|
+
const defaultDirs = clearDefaultProfileId(plan.entry.profileId);
|
|
215
|
+
deleteProfile(plan.entry.profileId);
|
|
216
|
+
const detached = new Set(detachedNodeIds);
|
|
217
|
+
for (const nodeId of restartNodeIds) {
|
|
218
|
+
if (detached.has(nodeId))
|
|
219
|
+
reviveNode(nodeId, { resume: true, recovery: true });
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
...resultForPlan(plan),
|
|
223
|
+
nodes: detachedNodeIds.length,
|
|
224
|
+
crons: cronIds.length,
|
|
225
|
+
default_pins: defaultDirs.length,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function hardDeleteProfile(plan) {
|
|
229
|
+
assertDeletionPlanPreconditions(plan);
|
|
230
|
+
const cronIds = deleteCronsForProfile(plan.entry.profileId, plan.nodeIds);
|
|
231
|
+
for (const nodeId of plan.nodeIds) {
|
|
232
|
+
headlessBrokerHost.teardown(nodeId);
|
|
233
|
+
tearDownNode(nodeId);
|
|
234
|
+
}
|
|
235
|
+
const reviewIds = deleteReviewsByOriginNodes(plan.nodeIds);
|
|
236
|
+
for (const reviewId of reviewIds)
|
|
237
|
+
rmSync(reviewDir(reviewId), { recursive: true, force: true });
|
|
238
|
+
const tickets = cleanTickets(plan.tickets);
|
|
239
|
+
const deletedNodeIds = deleteNodes(plan.nodeIds);
|
|
240
|
+
const defaultDirs = clearDefaultProfileId(plan.entry.profileId);
|
|
241
|
+
deleteProfile(plan.entry.profileId);
|
|
242
|
+
return {
|
|
243
|
+
...resultForPlan(plan),
|
|
244
|
+
nodes: deletedNodeIds.length,
|
|
245
|
+
crons: cronIds.length,
|
|
246
|
+
tickets,
|
|
247
|
+
reviews: reviewIds.length,
|
|
248
|
+
default_pins: defaultDirs.length,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async function executePlan(plan) {
|
|
252
|
+
return plan.mode === 'detached' ? detachProfile(plan) : hardDeleteProfile(plan);
|
|
253
|
+
}
|
|
254
|
+
function scheduleSelfDeletion(plan, releaseReservation) {
|
|
255
|
+
const result = {
|
|
256
|
+
...resultForPlan(plan),
|
|
257
|
+
accepted: true,
|
|
258
|
+
grace_ms: SELF_DELETE_GRACE_MS,
|
|
259
|
+
};
|
|
260
|
+
pending.set(plan.entry.profileId, { result });
|
|
261
|
+
setTimeout(() => {
|
|
262
|
+
void (async () => {
|
|
263
|
+
try {
|
|
264
|
+
await executePlan(plan);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
emitEvent({
|
|
268
|
+
level: 'error',
|
|
269
|
+
event: 'profile.delete.deferred_failed',
|
|
270
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
271
|
+
fields: { profile_id: plan.entry.profileId, mode: plan.mode },
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
pending.delete(plan.entry.profileId);
|
|
276
|
+
releaseReservation();
|
|
277
|
+
}
|
|
278
|
+
})();
|
|
279
|
+
}, SELF_DELETE_GRACE_MS);
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
/** Validate and enact one forced profile deletion. A request from a node that
|
|
283
|
+
* belongs to the target profile is acknowledged before its broker is stopped. */
|
|
284
|
+
export async function requestProfileDeletion(name, body) {
|
|
285
|
+
const request = validateRequest(body);
|
|
286
|
+
const entry = loadProfileManifest(name);
|
|
287
|
+
const existing = pending.get(entry.profileId);
|
|
288
|
+
if (existing !== undefined)
|
|
289
|
+
return { status: 202, body: existing.result };
|
|
290
|
+
const releaseReservation = reserveProfileDeletion(entry.profileId);
|
|
291
|
+
let deferred = false;
|
|
292
|
+
try {
|
|
293
|
+
const plan = prepareDeletion(entry, request);
|
|
294
|
+
const selfDeleting = request.caller_node_id != null && plan.nodeIds.includes(request.caller_node_id);
|
|
295
|
+
if (selfDeleting) {
|
|
296
|
+
const result = scheduleSelfDeletion(plan, releaseReservation);
|
|
297
|
+
deferred = true;
|
|
298
|
+
return { status: 202, body: result };
|
|
299
|
+
}
|
|
300
|
+
return { status: 200, body: await executePlan(plan) };
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
if (!deferred)
|
|
304
|
+
releaseReservation();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -3,7 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
-
import { getRow, getNode, subscribersOf } from '../../core/canvas/index.js';
|
|
6
|
+
import { getRow, getNode, isWarmSpare, subscribersOf } from '../../core/canvas/index.js';
|
|
7
7
|
import { contextDir, isSafeNodeId } from '../../core/canvas/paths.js';
|
|
8
8
|
import { fullName } from '../../core/canvas/labels.js';
|
|
9
9
|
import { recordedPidLiveness } from '../../core/canvas/pid.js';
|
|
@@ -20,7 +20,8 @@ import { operationIdContext } from '../../core/events/operation-id.js';
|
|
|
20
20
|
import { readCursor, readInboxSince } from '../../core/feed/inbox.js';
|
|
21
21
|
import { activeBackgroundBashJobs } from '../../core/bash-jobs.js';
|
|
22
22
|
import { focusOf } from '../../core/runtime/placement.js';
|
|
23
|
-
import {
|
|
23
|
+
import { hasAttachedBrokerViewers } from '../../core/runtime/broker/client-registry.js';
|
|
24
|
+
import { parkEvent } from './live-obligation.js';
|
|
24
25
|
const execFileAsync = promisify(execFile);
|
|
25
26
|
function configuredPositiveInteger(name, fallback) {
|
|
26
27
|
const raw = process.env[name];
|
|
@@ -174,6 +175,14 @@ export class BrokerSupervisionReconciler {
|
|
|
174
175
|
return thresholds;
|
|
175
176
|
}
|
|
176
177
|
handleUnattendedParking(id, row, livePid, now) {
|
|
178
|
+
// An unclaimed warm-pool spare is not a conversation: the booted broker it
|
|
179
|
+
// holds IS its whole value, and nobody is meant to be attending it. Its
|
|
180
|
+
// lifetime belongs to `reapStaleSpares` (warm-pool.ts) and its own TTL, so
|
|
181
|
+
// parking one here would empty the pool every 15 minutes.
|
|
182
|
+
if (isWarmSpare(id)) {
|
|
183
|
+
this.unattendedSince.delete(id);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
177
186
|
if (row === null
|
|
178
187
|
|| row.pi_session_id == null
|
|
179
188
|
|| row.pi_pid !== livePid
|
|
@@ -183,7 +192,8 @@ export class BrokerSupervisionReconciler {
|
|
|
183
192
|
|| row.intent !== null
|
|
184
193
|
|| isBusy(id)
|
|
185
194
|
|| activeBackgroundBashJobs(contextDir(id)).length > 0
|
|
186
|
-
|| focusOf(id) !== null
|
|
195
|
+
|| focusOf(id) !== null
|
|
196
|
+
|| hasAttachedBrokerViewers(id)) {
|
|
187
197
|
this.unattendedSince.delete(id);
|
|
188
198
|
return;
|
|
189
199
|
}
|
|
@@ -206,12 +216,7 @@ export class BrokerSupervisionReconciler {
|
|
|
206
216
|
if (now - since < unattendedParkMsForDaemon())
|
|
207
217
|
return;
|
|
208
218
|
this.unattendedSince.delete(id);
|
|
209
|
-
|
|
210
|
-
// A resident with nothing left to wake it has completed its conversation:
|
|
211
|
-
// terminalize it so history retention can eventually prune it. A resident
|
|
212
|
-
// still awaiting a child/controller/human/deadline/output stays wakeable, as does
|
|
213
|
-
// every terminal node (whose own stop guard owns semantic completion).
|
|
214
|
-
transition(id, row.lifecycle === 'resident' && !liveObligation ? 'finish' : 'release');
|
|
219
|
+
transition(id, parkEvent(id, row.lifecycle));
|
|
215
220
|
headlessBrokerHost.teardown(id);
|
|
216
221
|
}
|
|
217
222
|
async warnOverCapInTmux(count, warning, max) {
|
|
@@ -7,7 +7,7 @@ import { appendControllerDeathNotice, readCursor, readInboxSince, } from '../../
|
|
|
7
7
|
import { fanDoctrineWake } from '../../core/runtime/close.js';
|
|
8
8
|
import { transition } from '../../core/runtime/lifecycle.js';
|
|
9
9
|
import { reviveNode } from '../../core/runtime/revive.js';
|
|
10
|
-
import {
|
|
10
|
+
import { parkEvent } from './live-obligation.js';
|
|
11
11
|
// Idle-release/inbox retry cap (2026-07-10 diagnosis, round 2): the SAME
|
|
12
12
|
// livelock shape as the stranded-relaunch case, but on the separate pass that
|
|
13
13
|
// resumes a dormant idle-released node when its inbox gains an unseen entry.
|
|
@@ -80,11 +80,16 @@ export class DormantInboxReconciler {
|
|
|
80
80
|
if (!entries.some((entry) => entry.tier !== 'deferred')) {
|
|
81
81
|
this.relaunchState.delete(id);
|
|
82
82
|
const meta = getNode(id);
|
|
83
|
-
// An idle
|
|
84
|
-
// lifecycle settlement
|
|
85
|
-
// history rather than a permanently protected
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
// An idle row with no wake-worthy inbox entry still needs the same
|
|
84
|
+
// lifecycle settlement the live park clock applies: once nothing
|
|
85
|
+
// live remains, this is history rather than a permanently protected
|
|
86
|
+
// idle row. The row is already parked, so only a terminalizing
|
|
87
|
+
// verdict is worth writing. Unclaimed warm-pool spares never reach
|
|
88
|
+
// here — `listNodes` hides them from `ctx.rows`.
|
|
89
|
+
if (meta !== null) {
|
|
90
|
+
const event = parkEvent(id, meta.lifecycle);
|
|
91
|
+
if (event !== 'release')
|
|
92
|
+
transition(id, event);
|
|
88
93
|
}
|
|
89
94
|
return;
|
|
90
95
|
}
|
|
@@ -1,2 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
import { type Lifecycle } from '../../core/canvas/index.js';
|
|
2
|
+
/** Durable work that requires a node to remain automatically wakeable. */
|
|
2
3
|
export declare function hasLiveObligation(nodeId: string): boolean;
|
|
4
|
+
/** What parking an unattended node actually amounts to — the one rule both the
|
|
5
|
+
* live-broker park clock (broker-supervision) and the already-parked
|
|
6
|
+
* reconciliation (dormant-inbox) apply.
|
|
7
|
+
*
|
|
8
|
+
* `release` — something can still wake it, so parking is a pause.
|
|
9
|
+
* `finish` — a resident with nothing left to wake it has completed its
|
|
10
|
+
* conversation, and the row becomes prunable history.
|
|
11
|
+
* `cancel` — a terminal node that never pushed a final was torn down before
|
|
12
|
+
* finishing, which is what `canceled` records; it stays revivable
|
|
13
|
+
* and becomes prunable. A live subscriber still awaiting its
|
|
14
|
+
* report is an outstanding deliverable, so such a node keeps its
|
|
15
|
+
* wakeable idle row instead of being written off.
|
|
16
|
+
*
|
|
17
|
+
* Undelivered mail of ANY tier also holds the row. A deferred-only unseen set
|
|
18
|
+
* never wakes a node on its own — it rides the next natural cycle — but a
|
|
19
|
+
* terminalized row no longer takes that cycle, so writing one off would drop
|
|
20
|
+
* content the sender already handed over.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parkEvent(nodeId: string, lifecycle: Lifecycle): 'release' | 'finish' | 'cancel';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { hasActiveLiveSubscription, hasLiveMessageWait, hasPendingCancelOnWakeCron, } from '../../core/canvas/index.js';
|
|
1
|
+
import { hasActiveLiveSubscription, hasLiveMessageWait, hasLiveSubscriber, hasPendingCancelOnWakeCron, } from '../../core/canvas/index.js';
|
|
2
2
|
import { countTickets } from '../../core/canvas/attention.js';
|
|
3
|
+
import { readCursor, readInboxSince } from '../../core/feed/inbox.js';
|
|
3
4
|
import { readOutputRequest } from '../../core/runtime/structured-output.js';
|
|
4
|
-
/** Durable work that requires a
|
|
5
|
+
/** Durable work that requires a node to remain automatically wakeable. */
|
|
5
6
|
export function hasLiveObligation(nodeId) {
|
|
6
7
|
return hasActiveLiveSubscription(nodeId)
|
|
7
8
|
|| hasLiveMessageWait(nodeId)
|
|
@@ -9,3 +10,30 @@ export function hasLiveObligation(nodeId) {
|
|
|
9
10
|
|| countTickets(nodeId) > 0
|
|
10
11
|
|| readOutputRequest(nodeId).state !== 'absent';
|
|
11
12
|
}
|
|
13
|
+
/** What parking an unattended node actually amounts to — the one rule both the
|
|
14
|
+
* live-broker park clock (broker-supervision) and the already-parked
|
|
15
|
+
* reconciliation (dormant-inbox) apply.
|
|
16
|
+
*
|
|
17
|
+
* `release` — something can still wake it, so parking is a pause.
|
|
18
|
+
* `finish` — a resident with nothing left to wake it has completed its
|
|
19
|
+
* conversation, and the row becomes prunable history.
|
|
20
|
+
* `cancel` — a terminal node that never pushed a final was torn down before
|
|
21
|
+
* finishing, which is what `canceled` records; it stays revivable
|
|
22
|
+
* and becomes prunable. A live subscriber still awaiting its
|
|
23
|
+
* report is an outstanding deliverable, so such a node keeps its
|
|
24
|
+
* wakeable idle row instead of being written off.
|
|
25
|
+
*
|
|
26
|
+
* Undelivered mail of ANY tier also holds the row. A deferred-only unseen set
|
|
27
|
+
* never wakes a node on its own — it rides the next natural cycle — but a
|
|
28
|
+
* terminalized row no longer takes that cycle, so writing one off would drop
|
|
29
|
+
* content the sender already handed over.
|
|
30
|
+
*/
|
|
31
|
+
export function parkEvent(nodeId, lifecycle) {
|
|
32
|
+
if (hasLiveObligation(nodeId))
|
|
33
|
+
return 'release';
|
|
34
|
+
if (readInboxSince(nodeId, readCursor(nodeId)).length > 0)
|
|
35
|
+
return 'release';
|
|
36
|
+
if (lifecycle === 'resident')
|
|
37
|
+
return 'finish';
|
|
38
|
+
return hasLiveSubscriber(nodeId) ? 'release' : 'cancel';
|
|
39
|
+
}
|
|
@@ -4,7 +4,12 @@ export declare const DEAD_REAP_GRACE_MS: number;
|
|
|
4
4
|
export declare class StorageMaintenanceReconciler {
|
|
5
5
|
private lastPruneAt;
|
|
6
6
|
private lastSpareSweepAt;
|
|
7
|
+
private lastGhostSweepAt;
|
|
7
8
|
run(now: number): void;
|
|
9
|
+
/** Drop rows whose on-disk node dir is gone. Such a row can never be revived,
|
|
10
|
+
* focused, or closed — every one of those resolves the node through meta.json —
|
|
11
|
+
* so it sits in the roster jamming the surfaces that try. One readdir per sweep. */
|
|
12
|
+
private reapGhostRows;
|
|
8
13
|
/** Milliseconds since meta/telemetry was last written. Inbox activity is
|
|
9
14
|
* deliberately excluded: children can keep writing a dead manager's inbox. */
|
|
10
15
|
private msSinceDiskActivity;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { closeFocusRow, listFocuses, listNodes, pruneToLimit, } from '../../core/canvas/index.js';
|
|
3
|
+
import { closeFocusRow, listFocuses, listNodes, pruneToLimit, reapGhostRows, } from '../../core/canvas/index.js';
|
|
4
4
|
import { isSafeNodeId, jobDir, nodeDir } from '../../core/canvas/paths.js';
|
|
5
5
|
import { emitEvent } from '../../core/events/emit.js';
|
|
6
6
|
import { operationIdContext } from '../../core/events/operation-id.js';
|
|
@@ -12,20 +12,47 @@ export const DEAD_REAP_GRACE_MS = 10 * 60_000;
|
|
|
12
12
|
const AUTO_PRUNE_MAX = 2000;
|
|
13
13
|
const PRUNE_INTERVAL_MS = 60 * 60 * 1000;
|
|
14
14
|
const SPARE_SWEEP_INTERVAL_MS = 60 * 1000;
|
|
15
|
+
const GHOST_SWEEP_INTERVAL_MS = 60 * 1000;
|
|
15
16
|
/** Recurring storage cleanup. The in-memory throttle clocks intentionally reset
|
|
16
17
|
* on daemon boot, so the first tick runs both full sweeps. */
|
|
17
18
|
export class StorageMaintenanceReconciler {
|
|
18
19
|
lastPruneAt = Number.NEGATIVE_INFINITY;
|
|
19
20
|
lastSpareSweepAt = Number.NEGATIVE_INFINITY;
|
|
21
|
+
lastGhostSweepAt = Number.NEGATIVE_INFINITY;
|
|
20
22
|
run(now) {
|
|
21
23
|
// Placement reap MUST precede focus GC, so GC catches a focus row the reap
|
|
22
24
|
// strands. Both run after dormant-inbox reconciliation, so a same-tick
|
|
23
25
|
// revive has already had the chance to re-anchor its focus.
|
|
24
26
|
this.reapDeadResidue(now);
|
|
25
27
|
this.gcStaleFocuses();
|
|
28
|
+
this.reapGhostRows(now);
|
|
26
29
|
this.autoPrune(now);
|
|
27
30
|
this.gcWarmPool(now);
|
|
28
31
|
}
|
|
32
|
+
/** Drop rows whose on-disk node dir is gone. Such a row can never be revived,
|
|
33
|
+
* focused, or closed — every one of those resolves the node through meta.json —
|
|
34
|
+
* so it sits in the roster jamming the surfaces that try. One readdir per sweep. */
|
|
35
|
+
reapGhostRows(now) {
|
|
36
|
+
if (now - this.lastGhostSweepAt < GHOST_SWEEP_INTERVAL_MS)
|
|
37
|
+
return;
|
|
38
|
+
this.lastGhostSweepAt = now;
|
|
39
|
+
operationIdContext.fresh(() => {
|
|
40
|
+
try {
|
|
41
|
+
const reaped = reapGhostRows();
|
|
42
|
+
if (reaped.length > 0) {
|
|
43
|
+
emitEvent({
|
|
44
|
+
level: 'info',
|
|
45
|
+
event: 'canvas.ghost_rows.reaped',
|
|
46
|
+
outcome: 'succeeded',
|
|
47
|
+
fields: { reaped_count: reaped.length, reaped_ids: reaped },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
emitEvent({ level: 'error', event: 'canvas.ghost_rows.reap_failed', error: err });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
29
56
|
/** Milliseconds since meta/telemetry was last written. Inbox activity is
|
|
30
57
|
* deliberately excluded: children can keep writing a dead manager's inbox. */
|
|
31
58
|
msSinceDiskActivity(nodeId, now) {
|
|
@@ -3,8 +3,8 @@ import { formatReviewApproval } from '../../shared/generated-context.js';
|
|
|
3
3
|
import { deliverNodeMessage } from '../messaging/node-message.js';
|
|
4
4
|
function approvalBody(review, resultPath) {
|
|
5
5
|
const summary = review.changed
|
|
6
|
-
? `The
|
|
7
|
-
: `The
|
|
6
|
+
? `The user reviewed and approved \`${review.file}\`. The document changed during review; re-read it before continuing. Review conversation: node \`${review.companion_node_id}\`.`
|
|
7
|
+
: `The user reviewed and approved \`${review.file}\`. The document was not modified.`;
|
|
8
8
|
return `${summary}\n\nDurable review result: ${resultPath}`;
|
|
9
9
|
}
|
|
10
10
|
/** Render the immutable approval fact without replaying comment text. */
|
|
@@ -25,7 +25,7 @@ export async function deliverApproval(review, resultPath) {
|
|
|
25
25
|
const delivery = await deliverNodeMessage({
|
|
26
26
|
node_id: review.origin_node_id,
|
|
27
27
|
body,
|
|
28
|
-
label: '
|
|
28
|
+
label: 'review approved by the user',
|
|
29
29
|
from: 'crtrd',
|
|
30
30
|
mode: 'interactive',
|
|
31
31
|
data: { review_id: review.review_id, result_path: resultPath },
|
|
@@ -18,8 +18,6 @@ export interface ReviewCancellationFinishOutcome {
|
|
|
18
18
|
ticket_result?: TicketResult;
|
|
19
19
|
}
|
|
20
20
|
export type ReviewFinishOutcome = ReviewApprovalFinishOutcome | ReviewCancellationFinishOutcome;
|
|
21
|
-
/** Retire a companion only through a lifecycle transition legal for its current status. */
|
|
22
|
-
export declare function retireCompanion(nodeId: string, outcome: 'approved' | 'canceled'): 'retired' | 'already' | 'missing';
|
|
23
21
|
/** Approve on the human's behalf, waiting first when the companion is still
|
|
24
22
|
* working. Waiting is the only new outcome: an idle companion approves in the
|
|
25
23
|
* same pass it always did, and the wait never freezes the review — comments
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { withCanvasWrite } from '../../core/canvas/db.js';
|
|
2
|
-
import { getNode } from '../../core/canvas/canvas.js';
|
|
3
2
|
import { responsePath } from '../../core/human/convention.js';
|
|
4
3
|
import { ticketDir } from '../../core/human/root.js';
|
|
5
4
|
import { readTicketResult } from '../../core/human/tickets.js';
|
|
@@ -10,9 +9,8 @@ import { readReviewSource } from '../../core/review/document.js';
|
|
|
10
9
|
import { signalReviewActivity } from '../../core/review/signal.js';
|
|
11
10
|
import { approveReviewLocked, cancelReviewRow, getReview, getReviewByCompanion, markDelivered, markDeliveryError, markProjected, markProjectionError, requestReviewSubmit, requireVisibleReview, } from '../../core/review/store.js';
|
|
12
11
|
import { ReviewOperationError } from '../../core/review/types.js';
|
|
13
|
-
import { transition } from '../../core/runtime/lifecycle.js';
|
|
14
|
-
import { headlessBrokerHost } from '../../core/runtime/host.js';
|
|
15
12
|
import { emitEvent } from '../../core/events/emit.js';
|
|
13
|
+
import { retireCompanion } from '../companion-retire.js';
|
|
16
14
|
import { deliverNodeMessage } from '../messaging/node-message.js';
|
|
17
15
|
import { cancelHumanTicket, deliverTerminalResult } from '../human/finish.js';
|
|
18
16
|
import { companionHasQueuedWork, companionMidTurn, companionStillWorking } from './companion.js';
|
|
@@ -69,32 +67,6 @@ async function serializedFinish(reviewId, work) {
|
|
|
69
67
|
finishChains.delete(reviewId);
|
|
70
68
|
}
|
|
71
69
|
}
|
|
72
|
-
/** Retire a companion only through a lifecycle transition legal for its current status. */
|
|
73
|
-
export function retireCompanion(nodeId, outcome) {
|
|
74
|
-
const node = getNode(nodeId);
|
|
75
|
-
if (node === null)
|
|
76
|
-
return 'missing';
|
|
77
|
-
if (node.status === 'done' || node.status === 'canceled')
|
|
78
|
-
return 'already';
|
|
79
|
-
if (outcome === 'approved' && (node.status === 'active' || node.status === 'idle')) {
|
|
80
|
-
transition(nodeId, 'finish');
|
|
81
|
-
try {
|
|
82
|
-
headlessBrokerHost.teardown(nodeId);
|
|
83
|
-
}
|
|
84
|
-
catch { /* the terminal transition is authoritative */ }
|
|
85
|
-
return 'retired';
|
|
86
|
-
}
|
|
87
|
-
// Cancellation is legal from every status. An already-dead companion cannot
|
|
88
|
-
// be finished, so it is retired as canceled even when approval won.
|
|
89
|
-
transition(nodeId, 'cancel');
|
|
90
|
-
if (node.status === 'active' || node.status === 'idle') {
|
|
91
|
-
try {
|
|
92
|
-
headlessBrokerHost.teardown(nodeId);
|
|
93
|
-
}
|
|
94
|
-
catch { /* the terminal transition is authoritative */ }
|
|
95
|
-
}
|
|
96
|
-
return 'retired';
|
|
97
|
-
}
|
|
98
70
|
async function finishApproved(review, result) {
|
|
99
71
|
let current = requireVisibleReview(review.review_id);
|
|
100
72
|
if (current.projected_at === null) {
|
|
@@ -4,6 +4,8 @@ import { reviewPath } from '../../core/human/convention.js';
|
|
|
4
4
|
import { ticketDir } from '../../core/human/root.js';
|
|
5
5
|
import { submitReview } from '../../core/human/tickets.js';
|
|
6
6
|
import { reviewsRoot } from '../../core/canvas/paths.js';
|
|
7
|
+
import { getNode } from '../../core/canvas/index.js';
|
|
8
|
+
import { loadProfileManifest } from '../../core/profiles/manifest.js';
|
|
7
9
|
import { projectReviewResult } from '../../core/review/project.js';
|
|
8
10
|
import { realizeReview } from '../../core/review/realize.js';
|
|
9
11
|
import { listBindingReviews, listReviews, markProjected, markProjectionError, } from '../../core/review/store.js';
|
|
@@ -23,6 +25,12 @@ function republishTicketReview(review) {
|
|
|
23
25
|
const dir = ticketDir(review.bridge_node_id);
|
|
24
26
|
if (existsSync(reviewPath(dir)))
|
|
25
27
|
return;
|
|
28
|
+
const origin = getNode(review.origin_node_id);
|
|
29
|
+
if (origin === null)
|
|
30
|
+
throw new Error(`ticket review origin is missing: ${review.origin_node_id}`);
|
|
31
|
+
const profileName = origin.profile_id == null
|
|
32
|
+
? undefined
|
|
33
|
+
: loadProfileManifest(origin.profile_id).manifest.name;
|
|
26
34
|
submitReview({
|
|
27
35
|
dir,
|
|
28
36
|
review: {
|
|
@@ -30,7 +38,10 @@ function republishTicketReview(review) {
|
|
|
30
38
|
...(review.output_path === null ? {} : { output: review.output_path }),
|
|
31
39
|
title: basename(review.file),
|
|
32
40
|
subtitle: review.subtitle,
|
|
33
|
-
source: {
|
|
41
|
+
source: {
|
|
42
|
+
nodeId: review.origin_node_id,
|
|
43
|
+
...(profileName === undefined ? {} : { profileName }),
|
|
44
|
+
},
|
|
34
45
|
blockedSince: review.created,
|
|
35
46
|
},
|
|
36
47
|
});
|