@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
package/dist/daemon/cron-run.js
CHANGED
|
@@ -30,26 +30,24 @@
|
|
|
30
30
|
// promises for graceful-teardown drain but never blocks on a slow run.
|
|
31
31
|
import { spawn } from 'node:child_process';
|
|
32
32
|
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
33
|
-
import { join } from 'node:path';
|
|
34
33
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
35
34
|
import { rmSync } from 'node:fs';
|
|
36
35
|
import { synthesizedPageDocument, writeSynthesizedPage } from '../core/human/page-synth.js';
|
|
37
36
|
import { ticketDir } from '../core/human/root.js';
|
|
38
37
|
import { preparePage, publishPage } from '../core/human/tickets.js';
|
|
39
|
-
import {
|
|
38
|
+
import { resolvePageComponents } from '../core/config.js';
|
|
40
39
|
import { dueClockCrons, dueOverlapCrons, expiredIdleCrons, runningCrons, acquireCronRunLease, releaseCronRunLease, recordCronRun, listCronRuns, advanceCron, consumeCron, cancelCron, killLeasedCronRun, verifyLeasedCronRun, getCron, setCronState, setCronLastOutputHash, } from '../core/canvas/crons.js';
|
|
41
40
|
import { getNode } from '../core/canvas/canvas.js';
|
|
42
41
|
import { fullName } from '../core/canvas/labels.js';
|
|
43
42
|
import { killProcessGroup } from '../core/canvas/pid.js';
|
|
44
43
|
import { appendInbox } from '../core/feed/inbox.js';
|
|
45
|
-
import { atomicWriteJson } from '../core/fs-utils.js';
|
|
46
44
|
import { emitEvent } from '../core/events/emit.js';
|
|
47
45
|
import { loadProfileManifest } from '../core/profiles/manifest.js';
|
|
48
46
|
import { hasNoNaturalCycle } from '../core/runtime/revive-all.js';
|
|
49
47
|
import { spawnChild } from '../core/runtime/spawn.js';
|
|
50
48
|
import { cronWakeOrigin } from '../core/runtime/bearings.js';
|
|
51
49
|
import { nextSlotAfter, cadenceDisplay } from '../core/wake.js';
|
|
52
|
-
import { buildOperationalEnvBase } from '../core/runtime/spawn-env.js';
|
|
50
|
+
import { ADMITTED_HOST_PATH_ENV, buildOperationalEnvBase, resolveAdmittedHostCommand } from '../core/runtime/spawn-env.js';
|
|
53
51
|
import { parseSink } from './cron-sink.js';
|
|
54
52
|
import { readProfileEnvVars } from '../core/profiles/env-store.js';
|
|
55
53
|
/** Output head caps for the run log (spec: "capped, e.g. 4KB"). */
|
|
@@ -163,6 +161,16 @@ function resolveLiveProfile(c) {
|
|
|
163
161
|
return null;
|
|
164
162
|
}
|
|
165
163
|
}
|
|
164
|
+
function resolveLiveProfileName(c) {
|
|
165
|
+
if (c.profile == null)
|
|
166
|
+
return undefined;
|
|
167
|
+
try {
|
|
168
|
+
return loadProfileManifest(c.profile).manifest.name;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
166
174
|
// ---------------------------------------------------------------------------
|
|
167
175
|
// Delivery sinks
|
|
168
176
|
// ---------------------------------------------------------------------------
|
|
@@ -213,9 +221,9 @@ async function deliverSpawnSink(c, kind, stdout) {
|
|
|
213
221
|
});
|
|
214
222
|
return res.node.node_id;
|
|
215
223
|
}
|
|
216
|
-
/** Deliver stdout through crouter as a
|
|
217
|
-
*
|
|
218
|
-
*
|
|
224
|
+
/** Deliver stdout through crouter as a standalone human page. Mirrors
|
|
225
|
+
* `crtr human send --inbox` for a display-only page, minus the CLI-env source
|
|
226
|
+
* stamp: the page names the cron instead. */
|
|
219
227
|
function deliverHumanSink(c, stdout) {
|
|
220
228
|
const synthesized = writeSynthesizedPage(synthesizedPageDocument(`⏰ cron ${c.name}`, `Scheduled cron "${c.name}" delivered output for your attention.`, stdout, false));
|
|
221
229
|
// Cron stdout rides into the page verbatim, so it can carry invalid page
|
|
@@ -223,19 +231,21 @@ function deliverHumanSink(c, stdout) {
|
|
|
223
231
|
// half-built `nfy-*` directory behind.
|
|
224
232
|
let prepared;
|
|
225
233
|
try {
|
|
234
|
+
const profileName = resolveLiveProfileName(c);
|
|
226
235
|
prepared = preparePage({
|
|
227
236
|
sourceFile: synthesized.path,
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
237
|
+
source: {
|
|
238
|
+
sessionName: `cron ${c.name} (${c.cron_id})`,
|
|
239
|
+
...(profileName === undefined ? {} : { profileName }),
|
|
240
|
+
},
|
|
241
|
+
productKinds: resolvePageComponents(),
|
|
242
|
+
delivery: { placement: 'inline', inbox: true, reply: false },
|
|
231
243
|
});
|
|
232
244
|
}
|
|
233
245
|
finally {
|
|
234
246
|
rmSync(synthesized.dir, { recursive: true, force: true });
|
|
235
247
|
}
|
|
236
|
-
const id = `
|
|
237
|
-
const rc = { mode: 'notify' };
|
|
238
|
-
atomicWriteJson(join(ticketDir(id), 'run.json'), rc);
|
|
248
|
+
const id = `page-${randomBytes(4).toString('hex')}`;
|
|
239
249
|
publishPage(ticketDir(id), prepared);
|
|
240
250
|
}
|
|
241
251
|
/** Route stdout to the cron's armed sink. Returns the delivered string on
|
|
@@ -253,7 +263,7 @@ async function deliverToSink(c, stdout) {
|
|
|
253
263
|
return `delivered: spawned ${nodeId}`;
|
|
254
264
|
}
|
|
255
265
|
deliverHumanSink(c, stdout);
|
|
256
|
-
return 'delivered: human
|
|
266
|
+
return 'delivered: human send --inbox';
|
|
257
267
|
}
|
|
258
268
|
function failureSummary(ctx, deliveryFailure) {
|
|
259
269
|
if (deliveryFailure !== null)
|
|
@@ -314,7 +324,7 @@ function buildEscalationKickoff(c, ctx, deliveryFailure) {
|
|
|
314
324
|
`Diagnose why it failed and decide what to do:`,
|
|
315
325
|
`- Transient or fixable: fix the environment/command context, verify with \`crtr cron run ${c.cron_id}\` (fires now, out of band, shows you the result), then \`crtr cron resume ${c.cron_id}\` to re-arm it.`,
|
|
316
326
|
`- Obsolete: \`crtr cron cancel ${c.cron_id}\`.`,
|
|
317
|
-
`- The user's call (or the fix needs a change you cannot make): \`crtr human
|
|
327
|
+
`- The user's call (or the fix needs a change you cannot make): \`crtr human send\`.`,
|
|
318
328
|
'',
|
|
319
329
|
'The cron stays paused until someone resumes it — nothing refires while you work.',
|
|
320
330
|
].join('\n');
|
|
@@ -625,8 +635,9 @@ export function executeCron(c, opts) {
|
|
|
625
635
|
// Layering: profile store < the arm-time `--env` snapshot (an explicit
|
|
626
636
|
// choice) < crtr's own constructed CRTR_* names, which always win.
|
|
627
637
|
const profileId = resolveLiveProfile(c);
|
|
638
|
+
const operationalEnv = buildOperationalEnvBase({ targetCwd: c.cwd, targetProfileId: profileId });
|
|
628
639
|
const env = {
|
|
629
|
-
...
|
|
640
|
+
...operationalEnv,
|
|
630
641
|
...readProfileEnvVars(profileId),
|
|
631
642
|
...extraEnv,
|
|
632
643
|
...(profileId !== null ? { CRTR_PROFILE_ID: profileId } : {}),
|
|
@@ -646,11 +657,12 @@ export function executeCron(c, opts) {
|
|
|
646
657
|
}
|
|
647
658
|
if (c.anchor_node != null)
|
|
648
659
|
env['CRTR_ANCHOR'] = c.anchor_node;
|
|
660
|
+
env[ADMITTED_HOST_PATH_ENV] = operationalEnv[ADMITTED_HOST_PATH_ENV];
|
|
649
661
|
const startedAtMs = Date.now();
|
|
650
662
|
const startedAtIso = new Date(startedAtMs).toISOString();
|
|
651
663
|
let child;
|
|
652
664
|
try {
|
|
653
|
-
child = spawn('bash', ['-c', c.command], {
|
|
665
|
+
child = spawn(resolveAdmittedHostCommand('bash', env, c.cwd), ['-c', c.command], {
|
|
654
666
|
cwd: c.cwd,
|
|
655
667
|
env,
|
|
656
668
|
detached: true, // own process group — killProcessGroup(-pid) reaps the whole tree
|
package/dist/daemon/crtrd.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// The fleet registry's entry IS the node's liveness — no pid probe, no grace
|
|
9
9
|
// clock. A broker's real `exit` event enqueues an exit-policy job on the
|
|
10
10
|
// daemon's serial work lane (never inline on the Node event callback), where
|
|
11
|
-
// classifyDeadNode — one pure
|
|
11
|
+
// classifyDeadNode — one pure 8-row policy table over durable row state —
|
|
12
12
|
// decides forget / dormant / respawn / boot-failure. The startup recovery
|
|
13
13
|
// sweep classifies boot-time dead rows through the SAME table, so exit
|
|
14
14
|
// recovery and startup recovery can never diverge. Every broker's argv is
|
package/dist/daemon/fleet.d.ts
CHANGED
|
@@ -10,12 +10,16 @@ import type { HostHandle } from '../core/runtime/host.js';
|
|
|
10
10
|
* seeing a silent `dead`. */
|
|
11
11
|
export declare function surfaceBootFailure(meta: NodeMeta): Promise<void>;
|
|
12
12
|
/** Whether a dead relaunch should resume the saved session strictly, or retry
|
|
13
|
-
* as a fresh cycle instead. A cycling attempt (cycle_pending) that dies
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
13
|
+
* as a fresh cycle instead. A cycling attempt (cycle_pending) that dies before
|
|
14
|
+
* session_start must retry AS a cycle: the fresh branch it was opening is not
|
|
15
|
+
* confirmed, so resuming would silently continue the old leaf the refresh was
|
|
16
|
+
* meant to leave behind.
|
|
17
|
+
*
|
|
18
|
+
* An interrupted turn is NOT a reason to cycle. A strict reopen does restore
|
|
19
|
+
* the transcript at an idle prompt without restarting the dead turn — but the
|
|
20
|
+
* fix for that is the continuation prompt recovery delivers (see `#respawn`),
|
|
21
|
+
* not throwing the conversation away. */
|
|
22
|
+
export declare function retryResumeMode(meta: Pick<NodeMeta, 'cycle_pending'> | null): boolean;
|
|
19
23
|
export type DeadNodeAction = 'forget' | 'respawn-fresh' | 'dormant' | 'respawn-cycle' | 'respawn-resume' | 'boot-failure';
|
|
20
24
|
/** The narrow durable state the policy table reads. Structural (a Pick, not
|
|
21
25
|
* NodeMeta itself) so tests fabricate table rows directly. */
|
|
@@ -23,10 +27,18 @@ export type DeadNodeRow = Pick<NodeMeta, 'status' | 'intent' | 'pi_session_id' |
|
|
|
23
27
|
/** One pure function producing the action for a node known-dead. Used at
|
|
24
28
|
* exactly two call sites — an exit-policy job and the startup recovery sweep
|
|
25
29
|
* (design D-12) — so exit recovery and startup recovery can never diverge.
|
|
26
|
-
* Evaluated in table order; `busy` is the on-disk busy marker.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
+
* Evaluated in table order; `busy` is the on-disk busy marker.
|
|
31
|
+
*
|
|
32
|
+
* `busy` no longer selects the ACTION. Whether the engine died mid-turn or
|
|
33
|
+
* parked at an idle prompt, the saved transcript is equally resumable — pi
|
|
34
|
+
* persists whole entries (`appendFileSync` per line) so a killed generation
|
|
35
|
+
* simply leaves the file ending at its last complete entry, and the loader
|
|
36
|
+
* skips a malformed line rather than rejecting the session. A mid-turn death
|
|
37
|
+
* needs the turn re-driven, which is a continuation prompt on top of the
|
|
38
|
+
* resume (see `#respawn`), not a fresh branch. `busy` therefore survives here
|
|
39
|
+
* only to route a node that died before session_start ever recorded a session
|
|
40
|
+
* to a fresh relaunch instead of terminalizing it as a boot failure. */
|
|
41
|
+
export declare function classifyDeadNode(row: DeadNodeRow | null, busy: boolean): DeadNodeAction;
|
|
30
42
|
/** An exit with uptime at or above this resets the consecutive-failure
|
|
31
43
|
* counter before classification; a shorter-lived exit increments it. */
|
|
32
44
|
export declare const HEALTHY_UPTIME_MS = 60000;
|
package/dist/daemon/fleet.js
CHANGED
|
@@ -51,21 +51,33 @@ async function surfaceRecoveryFailure(meta, error) {
|
|
|
51
51
|
await pushUrgent(meta.node_id, body, { from: meta.node_id });
|
|
52
52
|
}
|
|
53
53
|
/** Whether a dead relaunch should resume the saved session strictly, or retry
|
|
54
|
-
* as a fresh cycle instead. A cycling attempt (cycle_pending) that dies
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
* as a fresh cycle instead. A cycling attempt (cycle_pending) that dies before
|
|
55
|
+
* session_start must retry AS a cycle: the fresh branch it was opening is not
|
|
56
|
+
* confirmed, so resuming would silently continue the old leaf the refresh was
|
|
57
|
+
* meant to leave behind.
|
|
58
|
+
*
|
|
59
|
+
* An interrupted turn is NOT a reason to cycle. A strict reopen does restore
|
|
60
|
+
* the transcript at an idle prompt without restarting the dead turn — but the
|
|
61
|
+
* fix for that is the continuation prompt recovery delivers (see `#respawn`),
|
|
62
|
+
* not throwing the conversation away. */
|
|
63
|
+
export function retryResumeMode(meta) {
|
|
64
|
+
return meta?.cycle_pending !== true;
|
|
61
65
|
}
|
|
62
66
|
/** One pure function producing the action for a node known-dead. Used at
|
|
63
67
|
* exactly two call sites — an exit-policy job and the startup recovery sweep
|
|
64
68
|
* (design D-12) — so exit recovery and startup recovery can never diverge.
|
|
65
|
-
* Evaluated in table order; `busy` is the on-disk busy marker.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
|
|
69
|
+
* Evaluated in table order; `busy` is the on-disk busy marker.
|
|
70
|
+
*
|
|
71
|
+
* `busy` no longer selects the ACTION. Whether the engine died mid-turn or
|
|
72
|
+
* parked at an idle prompt, the saved transcript is equally resumable — pi
|
|
73
|
+
* persists whole entries (`appendFileSync` per line) so a killed generation
|
|
74
|
+
* simply leaves the file ending at its last complete entry, and the loader
|
|
75
|
+
* skips a malformed line rather than rejecting the session. A mid-turn death
|
|
76
|
+
* needs the turn re-driven, which is a continuation prompt on top of the
|
|
77
|
+
* resume (see `#respawn`), not a fresh branch. `busy` therefore survives here
|
|
78
|
+
* only to route a node that died before session_start ever recorded a session
|
|
79
|
+
* to a fresh relaunch instead of terminalizing it as a boot failure. */
|
|
80
|
+
export function classifyDeadNode(row, busy) {
|
|
69
81
|
if (row === null)
|
|
70
82
|
return 'forget'; // row 1: row deleted
|
|
71
83
|
if (row.status === 'done' || row.status === 'canceled' || row.status === 'dead')
|
|
@@ -74,15 +86,13 @@ export function classifyDeadNode(row, busy, cleanAbort = false) {
|
|
|
74
86
|
return 'respawn-fresh'; // row 3
|
|
75
87
|
if (row.intent === 'idle-release')
|
|
76
88
|
return 'dormant'; // row 4
|
|
77
|
-
if (cleanAbort && row.pi_session_id != null)
|
|
78
|
-
return 'respawn-resume'; // row 5: broker-certified interrupted turn
|
|
79
|
-
if (busy)
|
|
80
|
-
return 'respawn-cycle'; // row 6: dirty interrupted turn
|
|
81
89
|
if (row.pi_session_id != null)
|
|
82
|
-
return retryResumeMode(row
|
|
83
|
-
if (
|
|
84
|
-
return 'respawn-fresh'; // row
|
|
85
|
-
|
|
90
|
+
return retryResumeMode(row) ? 'respawn-resume' : 'respawn-cycle'; // row 5: any saved session
|
|
91
|
+
if (busy)
|
|
92
|
+
return 'respawn-fresh'; // row 6: died mid-turn before a session was ever recorded
|
|
93
|
+
if (row.fork_from != null)
|
|
94
|
+
return 'respawn-fresh'; // row 7: bounded ordinary fork-birth retry
|
|
95
|
+
return 'boot-failure'; // row 8
|
|
86
96
|
}
|
|
87
97
|
// ---------------------------------------------------------------------------
|
|
88
98
|
// Respawn throttle (replaces the stranded-relaunch counters, event-driven)
|
|
@@ -248,8 +258,12 @@ export class DaemonFleet {
|
|
|
248
258
|
}
|
|
249
259
|
async #applyPolicy(nodeId, origin) {
|
|
250
260
|
const meta = getNode(nodeId);
|
|
251
|
-
|
|
252
|
-
|
|
261
|
+
// Two durable witnesses mean the dead broker had a turn in flight. A dirty
|
|
262
|
+
// death leaves the busy marker behind; a bounded graceful abort clears busy
|
|
263
|
+
// as the run settles, then writes the clean-abort certificate. Both need the
|
|
264
|
+
// same recovery: resume the saved transcript and re-drive the stopped turn.
|
|
265
|
+
const interruptedTurn = isBusy(nodeId) || hasCleanAbort(nodeId);
|
|
266
|
+
const action = classifyDeadNode(meta, interruptedTurn);
|
|
253
267
|
switch (action) {
|
|
254
268
|
case 'forget':
|
|
255
269
|
// Fanouts for terminal exits already happened at the transition site.
|
|
@@ -271,12 +285,12 @@ export class DaemonFleet {
|
|
|
271
285
|
case 'respawn-fresh':
|
|
272
286
|
case 'respawn-cycle':
|
|
273
287
|
case 'respawn-resume':
|
|
274
|
-
await this.#respawn(nodeId, meta, action, origin,
|
|
288
|
+
await this.#respawn(nodeId, meta, action, origin, interruptedTurn);
|
|
275
289
|
break;
|
|
276
290
|
}
|
|
277
291
|
return action;
|
|
278
292
|
}
|
|
279
|
-
async #respawn(nodeId, meta, action, origin,
|
|
293
|
+
async #respawn(nodeId, meta, action, origin, interruptedTurn) {
|
|
280
294
|
const state = this.#throttle.get(nodeId) ?? { failures: 0 };
|
|
281
295
|
if (state.failures >= MAX_RESPAWN_ATTEMPTS) {
|
|
282
296
|
await this.#terminalize(nodeId, meta, {
|
|
@@ -327,7 +341,12 @@ export class DaemonFleet {
|
|
|
327
341
|
try {
|
|
328
342
|
reviveNode(nodeId, {
|
|
329
343
|
resume: action === 'respawn-resume',
|
|
330
|
-
|
|
344
|
+
// A resumed session whose turn was interrupted comes back parked at an
|
|
345
|
+
// idle prompt with the dead turn unfinished. The continuation re-drives
|
|
346
|
+
// it in place, so the conversation continues instead of being rebuilt
|
|
347
|
+
// from disk on a fresh branch. Only a true resume can carry it; a cycle
|
|
348
|
+
// or fresh relaunch gets its own kickoff.
|
|
349
|
+
wakeReason: action === 'respawn-resume' && interruptedTurn ? 'runtime-restart-abort' : undefined,
|
|
331
350
|
// Replacing a dead broker instance is recovery, not a wake: nothing
|
|
332
351
|
// external arrived for this node, so an armed deadline survives the
|
|
333
352
|
// relaunch instead of being consumed by it.
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import type { TicketResult } from '../../core/human/types.js';
|
|
2
|
+
/** Every ticket-ending action retires the ticket's feedback companion — done
|
|
3
|
+
* when active or idle, never left resident. Idempotent and replay-safe; a
|
|
4
|
+
* corrupt feedback file must not block result publication. Exported for the
|
|
5
|
+
* daemon-start sweep, which must settle terminal tickets whether or not a
|
|
6
|
+
* reply bridge exists. */
|
|
7
|
+
export declare function settleFeedbackCompanion(ticketId: string): void;
|
|
2
8
|
/**
|
|
3
9
|
* The one completion path for a page ticket: claim takeover, exclusive result
|
|
4
|
-
* publication, then
|
|
5
|
-
*
|
|
6
|
-
*
|
|
10
|
+
* publication, then optional reply delivery. Every addressing surface routes
|
|
11
|
+
* through this function, so the reply-route descriptor is the sole authority
|
|
12
|
+
* on whether completion reaches a bridge.
|
|
7
13
|
*/
|
|
8
|
-
export declare function resolvePageTicket(
|
|
14
|
+
export declare function resolvePageTicket(ticketId: string, responses: unknown): Promise<TicketResult>;
|
|
9
15
|
/** Cancel a ticket with the store's first-writer-wins publication. */
|
|
10
|
-
export declare function cancelHumanTicket(
|
|
16
|
+
export declare function cancelHumanTicket(ticketId: string, opts: {
|
|
11
17
|
reason?: string;
|
|
12
18
|
actor?: string;
|
|
13
19
|
}): Promise<TicketResult>;
|
|
@@ -16,4 +22,4 @@ export declare function cancelHumanTicket(nodeId: string, opts: {
|
|
|
16
22
|
* receipt: every action below is replay-safe, so lost delivery can be retried
|
|
17
23
|
* by a later resolve/cancel attempt or the daemon-start sweep.
|
|
18
24
|
*/
|
|
19
|
-
export declare function deliverTerminalResult(
|
|
25
|
+
export declare function deliverTerminalResult(ticketId: string): Promise<boolean>;
|
|
@@ -1,36 +1,94 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
1
|
import { ApiError } from '../../api/index.js';
|
|
3
|
-
import {
|
|
2
|
+
import { getRow, subscribersOf } from '../../core/canvas/canvas.js';
|
|
4
3
|
import { ticketDir } from '../../core/human/root.js';
|
|
4
|
+
import { parsePage } from '../../core/human/page.js';
|
|
5
5
|
import { cancelTicket, projectReviewOutput, readTicketResult, takeoverAndCompletePage, } from '../../core/human/tickets.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { describePageAnswer } from '../../core/human/answer.js';
|
|
7
|
+
import { renderAnswerText } from '../../core/human/answer-text.js';
|
|
8
|
+
import { pagePath, readTicketReplyRoute, responsePath } from '../../core/human/convention.js';
|
|
9
|
+
import { readPageFeedback } from '../../core/human/feedback.js';
|
|
10
|
+
import { pageDocumentSha256 } from '../../core/human/feedback-companion.js';
|
|
8
11
|
import { appendInbox } from '../../core/feed/inbox.js';
|
|
9
12
|
import { FinalDeliveryError, pushFinal } from '../../core/feed/feed.js';
|
|
10
13
|
import { transition } from '../../core/runtime/lifecycle.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*/
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
14
|
+
import { emitEvent } from '../../core/events/emit.js';
|
|
15
|
+
import { retireCompanion } from '../companion-retire.js';
|
|
16
|
+
/** Every ticket-ending action retires the ticket's feedback companion — done
|
|
17
|
+
* when active or idle, never left resident. Idempotent and replay-safe; a
|
|
18
|
+
* corrupt feedback file must not block result publication. Exported for the
|
|
19
|
+
* daemon-start sweep, which must settle terminal tickets whether or not a
|
|
20
|
+
* reply bridge exists. */
|
|
21
|
+
export function settleFeedbackCompanion(ticketId) {
|
|
22
|
+
try {
|
|
23
|
+
const companionNodeId = readPageFeedback(ticketDir(ticketId)).companionNodeId;
|
|
24
|
+
if (companionNodeId !== undefined)
|
|
25
|
+
retireCompanion(companionNodeId, 'approved');
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
emitEvent({ level: 'warn', event: 'human.feedback_companion.retire_failed', error });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** The settle-time feedback addendum to a page completion — answer and
|
|
32
|
+
* cancellation alike: a change notice when the final page differs from the
|
|
33
|
+
* version first commented on, and each still-open item's quote and note.
|
|
34
|
+
* Silent when the page is unchanged and no feedback remains open; never a
|
|
35
|
+
* manufactured prose diff. */
|
|
36
|
+
function feedbackAddendum(dir) {
|
|
37
|
+
let sections;
|
|
38
|
+
try {
|
|
39
|
+
const feedback = readPageFeedback(dir);
|
|
40
|
+
if (feedback.companionNodeId === undefined)
|
|
41
|
+
return '';
|
|
42
|
+
sections = [];
|
|
43
|
+
if (feedback.firstCommentedSha256 !== undefined && pageDocumentSha256(dir) !== feedback.firstCommentedSha256) {
|
|
44
|
+
sections.push(`The page changed during feedback: the person commented on it and its companion conversation (node ${feedback.companionNodeId}) revised it in place. ` +
|
|
45
|
+
`Reread the final page at ${pagePath(dir, 'jsx')} before interpreting this outcome.`);
|
|
46
|
+
}
|
|
47
|
+
const open = feedback.comments.filter((comment) => comment.status === 'open');
|
|
48
|
+
if (open.length > 0) {
|
|
49
|
+
const items = open.map((comment) => [
|
|
50
|
+
...comment.quote.split('\n').map((line) => `> ${line}`),
|
|
51
|
+
comment.note,
|
|
52
|
+
].join('\n'));
|
|
53
|
+
sections.push([
|
|
54
|
+
`Feedback left unresolved at settlement — the companion conversation (node ${feedback.companionNodeId}) had not dealt with these items:`,
|
|
55
|
+
...items,
|
|
56
|
+
].join('\n\n'));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// The answer itself is the deliverable; a broken feedback record adds nothing.
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
return sections.length === 0 ? '' : `\n\n${sections.join('\n\n')}`;
|
|
64
|
+
}
|
|
65
|
+
/** A corrupt manifest must not strand the asking node: the raw response remains its escape hatch. */
|
|
66
|
+
function renderCorruptPageAnswer(result, resultPath) {
|
|
67
|
+
return [
|
|
68
|
+
'The page manifest could not be read, so the raw response is provided.',
|
|
69
|
+
'',
|
|
70
|
+
JSON.stringify(result.responses),
|
|
71
|
+
'',
|
|
72
|
+
`Response record: ${resultPath}`,
|
|
73
|
+
].join('\n');
|
|
74
|
+
}
|
|
75
|
+
function pageTitle(dir) {
|
|
76
|
+
try {
|
|
77
|
+
return parsePage(dir).title;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function renderPageAnswer(dir, result, resultPath) {
|
|
84
|
+
let manifest;
|
|
85
|
+
try {
|
|
86
|
+
manifest = parsePage(dir);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return renderCorruptPageAnswer(result, resultPath);
|
|
90
|
+
}
|
|
91
|
+
return renderAnswerText(describePageAnswer(manifest, result.responses, { completedAt: result.completedAt }), resultPath) + feedbackAddendum(dir);
|
|
34
92
|
}
|
|
35
93
|
function deliveryFailed(error) {
|
|
36
94
|
// A canonical final is committed inside its own write boundary and can never
|
|
@@ -42,9 +100,9 @@ function deliveryFailed(error) {
|
|
|
42
100
|
const detail = error instanceof Error ? error.message : String(error);
|
|
43
101
|
return new ApiError(500, 'delivery_failed', `The answer is recorded and delivery will be retried: ${detail}`);
|
|
44
102
|
}
|
|
45
|
-
async function deliverOrConflict(
|
|
103
|
+
async function deliverOrConflict(ticketId) {
|
|
46
104
|
try {
|
|
47
|
-
await deliverTerminalResult(
|
|
105
|
+
await deliverTerminalResult(ticketId);
|
|
48
106
|
}
|
|
49
107
|
catch (error) {
|
|
50
108
|
throw deliveryFailed(error);
|
|
@@ -53,16 +111,16 @@ async function deliverOrConflict(nodeId) {
|
|
|
53
111
|
}
|
|
54
112
|
/**
|
|
55
113
|
* The one completion path for a page ticket: claim takeover, exclusive result
|
|
56
|
-
* publication, then
|
|
57
|
-
*
|
|
58
|
-
*
|
|
114
|
+
* publication, then optional reply delivery. Every addressing surface routes
|
|
115
|
+
* through this function, so the reply-route descriptor is the sole authority
|
|
116
|
+
* on whether completion reaches a bridge.
|
|
59
117
|
*/
|
|
60
|
-
export async function resolvePageTicket(
|
|
61
|
-
const completed = takeoverAndCompletePage(ticketDir(
|
|
118
|
+
export async function resolvePageTicket(ticketId, responses) {
|
|
119
|
+
const completed = takeoverAndCompletePage(ticketDir(ticketId), responses, { host: 'crtrd', pid: process.pid });
|
|
62
120
|
if (!completed.won)
|
|
63
|
-
return deliverOrConflict(
|
|
121
|
+
return deliverOrConflict(ticketId);
|
|
64
122
|
try {
|
|
65
|
-
await deliverTerminalResult(
|
|
123
|
+
await deliverTerminalResult(ticketId);
|
|
66
124
|
}
|
|
67
125
|
catch (error) {
|
|
68
126
|
throw deliveryFailed(error);
|
|
@@ -70,12 +128,12 @@ export async function resolvePageTicket(nodeId, responses, productKinds = []) {
|
|
|
70
128
|
return completed.result;
|
|
71
129
|
}
|
|
72
130
|
/** Cancel a ticket with the store's first-writer-wins publication. */
|
|
73
|
-
export async function cancelHumanTicket(
|
|
74
|
-
const canceled = cancelTicket(ticketDir(
|
|
131
|
+
export async function cancelHumanTicket(ticketId, opts) {
|
|
132
|
+
const canceled = cancelTicket(ticketDir(ticketId), opts);
|
|
75
133
|
if (canceled.status === 'already_resolved')
|
|
76
|
-
return deliverOrConflict(
|
|
134
|
+
return deliverOrConflict(ticketId);
|
|
77
135
|
try {
|
|
78
|
-
await deliverTerminalResult(
|
|
136
|
+
await deliverTerminalResult(ticketId);
|
|
79
137
|
}
|
|
80
138
|
catch (error) {
|
|
81
139
|
throw deliveryFailed(error);
|
|
@@ -87,37 +145,59 @@ export async function cancelHumanTicket(nodeId, opts) {
|
|
|
87
145
|
* receipt: every action below is replay-safe, so lost delivery can be retried
|
|
88
146
|
* by a later resolve/cancel attempt or the daemon-start sweep.
|
|
89
147
|
*/
|
|
90
|
-
export async function deliverTerminalResult(
|
|
91
|
-
const dir = ticketDir(
|
|
92
|
-
const run = readJsonOrNull(join(dir, 'run.json'));
|
|
93
|
-
if (run?.mode === 'notify')
|
|
94
|
-
return false;
|
|
148
|
+
export async function deliverTerminalResult(ticketId) {
|
|
149
|
+
const dir = ticketDir(ticketId);
|
|
95
150
|
const result = readTicketResult(dir);
|
|
96
151
|
if (result === null)
|
|
97
152
|
return false;
|
|
153
|
+
// Companion retirement rides the committed result, BEFORE the reply-route
|
|
154
|
+
// gate: every ticket-ending path (resolve, cancel, conflict replay) funnels
|
|
155
|
+
// through here, and an inbox-only page with no reply bridge still settles
|
|
156
|
+
// its companion. The daemon-start sweep settles the tickets that die before
|
|
157
|
+
// reaching this call.
|
|
158
|
+
settleFeedbackCompanion(ticketId);
|
|
159
|
+
const replyRoute = readTicketReplyRoute(dir);
|
|
160
|
+
if (replyRoute === null)
|
|
161
|
+
return false;
|
|
162
|
+
const bridgeNodeId = replyRoute.bridge_node_id;
|
|
98
163
|
projectReviewOutput(dir);
|
|
99
|
-
|
|
164
|
+
// The ROW, not node metadata: a bridge whose row has been pruned may retain
|
|
165
|
+
// its ticket files, but no lifecycle target remains. No row means nothing is
|
|
166
|
+
// left to deliver to.
|
|
167
|
+
const node = getRow(bridgeNodeId);
|
|
100
168
|
if (node === null || node.status === 'dead' || node.status === 'canceled' || (node.status === 'done' && node.final_report !== null)) {
|
|
101
169
|
return true;
|
|
102
170
|
}
|
|
103
171
|
if (result.kind === 'canceled') {
|
|
104
|
-
const
|
|
172
|
+
const title = pageTitle(dir);
|
|
173
|
+
const reason = result.reason !== undefined && result.reason !== '' ? result.reason : 'no reason was given';
|
|
174
|
+
// A human dismissal is a rejection, not a routing event: say so plainly so
|
|
175
|
+
// the agent acts on the "no" instead of re-raising the same ask.
|
|
176
|
+
const dismissed = result.actor === 'human';
|
|
177
|
+
const subject = dismissed
|
|
178
|
+
? (title === undefined ? `human interaction ${ticketId} dismissed without answering` : `human interaction ${ticketId} — “${title}” dismissed without answering`)
|
|
179
|
+
: (title === undefined ? `human interaction ${ticketId} canceled — no answer is coming` : `human interaction ${ticketId} — “${title}” canceled, no answer is coming`);
|
|
180
|
+
// Dismissal and withdrawal are settlement paths too: the sender still
|
|
181
|
+
// needs any unresolved feedback and the exceptional page-change notice.
|
|
182
|
+
const body = (dismissed
|
|
183
|
+
? `The user dismissed ${title === undefined ? `the human interaction ${ticketId}` : `“${title}”`} without answering — a deliberate “no, I'm not answering this.” Treat it as a rejection: no response will arrive, no partial answer exists, and the same ask must not be raised again.`
|
|
184
|
+
: (title === undefined ? `The human interaction ${ticketId} was canceled — ${reason}. No response will arrive, and any partial answers the user had entered were discarded unsent.` : `The page “${title}” was canceled — ${reason}. No response will arrive, and any partial answers the user had entered were discarded unsent.`)) + feedbackAddendum(dir);
|
|
105
185
|
// One failing subscriber must not silence the rest, and the bridge stays
|
|
106
186
|
// live until every notice lands: a partial fanout leaves the ticket for the
|
|
107
187
|
// next resolve/cancel attempt or the daemon-start sweep to replay. A
|
|
108
188
|
// subscriber notified twice reads the same idempotent "no answer is coming";
|
|
109
189
|
// a subscriber never notified waits forever.
|
|
110
190
|
const failedTo = [];
|
|
111
|
-
for (const subscriber of subscribersOf(
|
|
191
|
+
for (const subscriber of subscribersOf(bridgeNodeId)) {
|
|
112
192
|
if (result.actor !== undefined && subscriber.node_id === result.actor)
|
|
113
193
|
continue;
|
|
114
194
|
try {
|
|
115
195
|
appendInbox(subscriber.node_id, {
|
|
116
|
-
from:
|
|
117
|
-
tier: '
|
|
196
|
+
from: bridgeNodeId,
|
|
197
|
+
tier: 'normal',
|
|
118
198
|
kind: 'message',
|
|
119
|
-
label:
|
|
120
|
-
data: { body
|
|
199
|
+
label: subject,
|
|
200
|
+
data: { body },
|
|
121
201
|
});
|
|
122
202
|
}
|
|
123
203
|
catch {
|
|
@@ -126,11 +206,11 @@ export async function deliverTerminalResult(nodeId) {
|
|
|
126
206
|
}
|
|
127
207
|
if (failedTo.length > 0)
|
|
128
208
|
throw new Error(`cancellation notice failed for ${failedTo.join(', ')}`);
|
|
129
|
-
transition(
|
|
209
|
+
transition(bridgeNodeId, 'finish');
|
|
130
210
|
return true;
|
|
131
211
|
}
|
|
132
212
|
if (result.kind === 'page') {
|
|
133
|
-
await pushFinal(
|
|
213
|
+
await pushFinal(bridgeNodeId, renderPageAnswer(dir, result, responsePath(dir)));
|
|
134
214
|
}
|
|
135
215
|
return true;
|
|
136
216
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { getRow } from '../../core/canvas/canvas.js';
|
|
4
4
|
import { getReviewByBridge } from '../../core/review/store.js';
|
|
5
5
|
import { emitEvent } from '../../core/events/emit.js';
|
|
6
6
|
import { ticketsRoot } from '../../core/human/root.js';
|
|
7
7
|
import { readTicketResult } from '../../core/human/tickets.js';
|
|
8
|
-
import { deliverTerminalResult } from './finish.js';
|
|
8
|
+
import { deliverTerminalResult, settleFeedbackCompanion } from './finish.js';
|
|
9
9
|
function isTerminalBridge(status) {
|
|
10
10
|
return status === 'done' || status === 'dead' || status === 'canceled';
|
|
11
11
|
}
|
|
@@ -27,7 +27,13 @@ export async function reconcileUndeliveredTickets() {
|
|
|
27
27
|
// finisher retries only when its winning operation is explicitly repeated.
|
|
28
28
|
if (getReviewByBridge(entry) !== null)
|
|
29
29
|
continue;
|
|
30
|
-
|
|
30
|
+
// A terminal page ticket retires its feedback companion whether or not a
|
|
31
|
+
// reply bridge exists: inbox-only pages have no bridge row, yet the
|
|
32
|
+
// daemon can die between result publication and companion retirement.
|
|
33
|
+
settleFeedbackCompanion(entry);
|
|
34
|
+
// The ROW is what carries status; `getNode` would answer from a pruned
|
|
35
|
+
// bridge's surviving meta.json with no status at all.
|
|
36
|
+
const bridge = getRow(entry);
|
|
31
37
|
if (bridge === null || isTerminalBridge(bridge.status))
|
|
32
38
|
continue;
|
|
33
39
|
if (await deliverTerminalResult(entry))
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DeleteProfileResultDTO } from '../api/index.js';
|
|
2
|
+
/** Validate and enact one forced profile deletion. A request from a node that
|
|
3
|
+
* belongs to the target profile is acknowledged before its broker is stopped. */
|
|
4
|
+
export declare function requestProfileDeletion(name: string, body: unknown): Promise<{
|
|
5
|
+
status: 200 | 202;
|
|
6
|
+
body: DeleteProfileResultDTO;
|
|
7
|
+
}>;
|