@north-light/crouter 0.3.220 → 0.3.222
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 +29 -0
- package/dist/api/client.js +44 -0
- package/dist/api/dto/chat-inventory.d.ts +56 -0
- package/dist/api/dto/chat-inventory.js +11 -0
- package/dist/api/dto/human-requests.d.ts +88 -0
- package/dist/api/dto/human-requests.js +4 -0
- package/dist/api/dto/human.d.ts +3 -0
- package/dist/api/dto/profiles.d.ts +19 -5
- package/dist/api/dto/profiles.js +2 -1
- package/dist/api/dto/reviews.d.ts +2 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/routes.d.ts +8 -0
- package/dist/api/routes.js +11 -0
- package/dist/build-root.d.ts +2 -6
- package/dist/build-root.js +51 -4
- package/dist/builtin-memory/00-runtime-base/00-authoring.md +31 -0
- package/dist/builtin-memory/00-runtime-base/01-escalation.md +14 -0
- package/dist/builtin-memory/{insights/listen.md → 00-runtime-base/02-insight-capture.md} +1 -0
- package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +27 -0
- package/dist/builtin-memory/{02-lifecycle/01-resident.md → 02-turn-lifecycle/02-resident.md} +5 -0
- package/dist/builtin-memory/04-base-worker.md +4 -8
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +1 -0
- package/dist/builtin-memory/05-kinds/design/00-base.md +2 -1
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +2 -1
- package/dist/builtin-memory/05-kinds/design/design-contract.md +19 -0
- package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/general/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/00-base.md +2 -1
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +2 -1
- package/dist/builtin-memory/05-kinds/plan/plan-contract.md +28 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/lens-contract.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -0
- package/dist/builtin-memory/05-kinds/review/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/review/companion/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/review/security-findings.md +1 -0
- package/dist/builtin-memory/05-kinds/spec/00-base.md +4 -3
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -0
- package/dist/builtin-memory/design/guide.md +35 -0
- package/dist/builtin-memory/design/roadmap.md +21 -0
- package/dist/builtin-memory/insights/capture.md +1 -1
- package/dist/builtin-memory/internal/agent-shaping.md +3 -1
- package/dist/builtin-memory/internal/memory-loading.md +4 -0
- package/dist/builtin-memory/internal/plugins.md +10 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/plan/roadmap.md +6 -22
- package/dist/builtin-memory/spec/guide.md +23 -6
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +34 -16
- package/dist/clients/attach/__tests__/ref-autocomplete.test.js +1 -1
- package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -1
- package/dist/clients/attach/overlays/file-review.js +2 -2
- package/dist/clients/attach/render/markdown-source.js +106 -1
- package/dist/clients/attach/session/file-links.d.ts +13 -4
- package/dist/clients/attach/session/file-links.js +54 -58
- package/dist/clients/attach/session/keys.d.ts +1 -1
- package/dist/clients/attach/session/profile-files.js +1 -1
- package/dist/clients/attach/viewer.js +698 -696
- package/dist/clients/inbox/controller.js +1 -1
- package/dist/clients/inbox/resolve.d.ts +1 -0
- package/dist/clients/inbox/review/launch.d.ts +8 -4
- package/dist/clients/inbox/review/launch.js +55 -5
- package/dist/clients/inbox/review/review-client.d.ts +1 -0
- package/dist/clients/inbox/review/review-client.js +7 -1
- package/dist/clients/inbox/review-adapter.d.ts +1 -8
- package/dist/clients/inbox/review-adapter.js +4 -52
- package/dist/commands/__tests__/human.test.js +2 -2
- package/dist/commands/human/request.d.ts +2 -0
- package/dist/commands/human/request.js +281 -0
- package/dist/commands/human.js +5 -2
- package/dist/commands/memory/lint.js +2 -1
- package/dist/commands/memory/read.js +1 -0
- package/dist/commands/memory.js +1 -1
- package/dist/commands/pkg/market-manage.js +165 -75
- package/dist/commands/pkg/plugin-inspect.js +19 -2
- package/dist/commands/pkg/plugin-manage.d.ts +8 -3
- package/dist/commands/pkg/plugin-manage.js +72 -24
- package/dist/commands/profile/default.js +6 -10
- package/dist/commands/profile/list.js +5 -3
- package/dist/commands/profile/new.js +21 -8
- package/dist/commands/profile/project.js +25 -19
- package/dist/commands/profile/show.js +3 -3
- package/dist/commands/surface-inbox.js +1 -0
- package/dist/commands/sys/__tests__/migrate.test.js +16 -5
- package/dist/commands/sys/config.js +2 -2
- package/dist/commands/sys/doctor.js +87 -5
- package/dist/commands/sys/migrate.js +38 -19
- package/dist/commands/sys/setup-core.js +1 -1
- package/dist/commands/sys/sync-project-guidance.js +1 -1
- package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +7 -4
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +1 -1
- package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +24 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -18
- package/dist/core/__tests__/fixtures/fake-engine.js +8 -1
- package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +71 -0
- package/dist/core/__tests__/human-action-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/human-action-delivery.test.js +140 -0
- package/dist/core/__tests__/human-actions.test.d.ts +1 -0
- package/dist/core/__tests__/human-actions.test.js +116 -0
- package/dist/core/__tests__/inline-memory-refs.test.js +36 -2
- package/dist/core/__tests__/profile-project-memory-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
- package/dist/core/__tests__/prospective-inventory-capability-parity.test.d.ts +1 -0
- package/dist/core/__tests__/prospective-inventory-capability-parity.test.js +91 -0
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.d.ts +1 -0
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +127 -0
- package/dist/core/__tests__/seam/prospective-inventory-stdout.test.d.ts +1 -0
- package/dist/core/__tests__/seam/prospective-inventory-stdout.test.js +31 -0
- package/dist/core/__tests__/serial/broker-sdk-wiring.test.js +102 -2
- package/dist/core/bootstrap.js +6 -0
- package/dist/core/canvas/browse/app.js +5 -2
- package/dist/core/canvas/browse/model.d.ts +25 -15
- package/dist/core/canvas/browse/model.js +86 -65
- package/dist/core/canvas/db.js +23 -0
- package/dist/core/canvas/human-deliveries.d.ts +53 -0
- package/dist/core/canvas/human-deliveries.js +75 -0
- package/dist/core/canvas/render-source.d.ts +6 -0
- package/dist/core/canvas/render-source.js +7 -1
- package/dist/core/canvas/render.js +10 -2
- package/dist/core/command-hooks/artifact.d.ts +10 -0
- package/dist/core/command-hooks/artifact.js +129 -0
- package/dist/core/command-hooks/catalog.d.ts +14 -0
- package/dist/core/command-hooks/catalog.js +38 -0
- package/dist/core/command-hooks/compose.d.ts +15 -0
- package/dist/core/command-hooks/compose.js +99 -0
- package/dist/core/command-hooks/discovery.d.ts +87 -0
- package/dist/core/command-hooks/discovery.js +174 -0
- package/dist/core/command-hooks/help.d.ts +5 -0
- package/dist/core/command-hooks/help.js +18 -0
- package/dist/core/command-hooks/index.d.ts +6 -0
- package/dist/core/command-hooks/index.js +6 -0
- package/dist/core/command-hooks/report.d.ts +23 -0
- package/dist/core/command-hooks/report.js +19 -0
- package/dist/core/command-hooks/schema.d.ts +27 -0
- package/dist/core/command-hooks/schema.js +68 -0
- package/dist/core/command-hooks/transport/exec-invoke.d.ts +22 -0
- package/dist/core/command-hooks/transport/exec-invoke.js +274 -0
- package/dist/core/command-plugins/presence.d.ts +2 -0
- package/dist/core/command-plugins/presence.js +17 -0
- package/dist/core/command-plugins/transport/exec-invoke.d.ts +5 -0
- package/dist/core/command-plugins/transport/exec-invoke.js +58 -5
- package/dist/core/command.d.ts +8 -1
- package/dist/core/command.js +12 -10
- package/dist/core/config.d.ts +13 -1
- package/dist/core/config.js +51 -1
- package/dist/core/feed/inbox.d.ts +6 -0
- package/dist/core/feed/inbox.js +9 -1
- package/dist/core/help.d.ts +7 -1
- package/dist/core/human/action-binding.d.ts +21 -0
- package/dist/core/human/action-binding.js +40 -0
- package/dist/core/human/completion.d.ts +38 -0
- package/dist/core/human/completion.js +27 -0
- package/dist/core/human/convention.d.ts +2 -0
- package/dist/core/human/convention.js +2 -0
- package/dist/core/human/tickets.d.ts +25 -6
- package/dist/core/human/tickets.js +19 -13
- package/dist/core/human/types.d.ts +5 -0
- package/dist/core/human-actions.d.ts +25 -0
- package/dist/core/human-actions.js +101 -0
- package/dist/core/io.d.ts +9 -1
- package/dist/core/io.js +44 -2
- package/dist/core/memory/inline-ref-inventory.d.ts +2 -1
- package/dist/core/memory/inline-ref-inventory.js +15 -8
- package/dist/core/memory-resolver.d.ts +13 -1
- package/dist/core/memory-resolver.js +26 -20
- package/dist/core/profiles/manifest.d.ts +13 -2
- package/dist/core/profiles/manifest.js +84 -18
- package/dist/core/profiles/select.d.ts +2 -0
- package/dist/core/profiles/select.js +29 -12
- package/dist/core/render.js +11 -0
- package/dist/core/runtime/advertised-command-invocation.d.ts +20 -0
- package/dist/core/runtime/advertised-command-invocation.js +233 -0
- package/dist/core/runtime/bearings.js +1 -1
- package/dist/core/runtime/broker/event-projection.js +7 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
- package/dist/core/runtime/broker/frame-dispatch.js +13 -11
- package/dist/core/runtime/broker/read-ops.d.ts +4 -0
- package/dist/core/runtime/broker/read-ops.js +6 -2
- package/dist/core/runtime/broker-extension-render.js +1 -1
- package/dist/core/runtime/broker-inventory.d.ts +4 -0
- package/dist/core/runtime/broker-inventory.js +116 -0
- package/dist/core/runtime/broker-persona-guidance.js +1 -1
- package/dist/core/runtime/broker-protocol.d.ts +9 -2
- package/dist/core/runtime/broker.js +10 -1
- package/dist/core/runtime/chat-inventory-rows.d.ts +8 -0
- package/dist/core/runtime/chat-inventory-rows.js +105 -0
- package/dist/core/runtime/command-surface.d.ts +38 -0
- package/dist/core/runtime/command-surface.js +117 -0
- package/dist/core/runtime/launch-target.d.ts +25 -0
- package/dist/core/runtime/launch-target.js +54 -0
- package/dist/core/runtime/node-read.js +5 -0
- package/dist/core/runtime/persona.js +3 -3
- package/dist/core/runtime/prospective-inventory-cli.d.ts +1 -0
- package/dist/core/runtime/prospective-inventory-cli.js +61 -0
- package/dist/core/runtime/prospective-inventory.d.ts +10 -0
- package/dist/core/runtime/prospective-inventory.js +88 -0
- package/dist/core/runtime/spawn.d.ts +3 -1
- package/dist/core/runtime/spawn.js +5 -3
- package/dist/core/scope.d.ts +26 -1
- package/dist/core/scope.js +52 -12
- package/dist/core/substrate/on-read.d.ts +7 -1
- package/dist/core/substrate/on-read.js +30 -32
- package/dist/core/substrate/render-node.d.ts +3 -2
- package/dist/core/substrate/render-node.js +3 -2
- package/dist/core/substrate/render.js +65 -24
- package/dist/core/substrate/schema.d.ts +16 -2
- package/dist/core/substrate/schema.js +14 -5
- package/dist/core/user-settings.d.ts +4 -0
- package/dist/core/user-settings.js +1 -0
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +56 -7
- package/dist/daemon/api/handlers/chat-inventory.d.ts +2 -0
- package/dist/daemon/api/handlers/chat-inventory.js +25 -0
- package/dist/daemon/api/handlers/human-requests.d.ts +2 -0
- package/dist/daemon/api/handlers/human-requests.js +409 -0
- package/dist/daemon/api/handlers/human.js +3 -0
- package/dist/daemon/api/handlers/inbox.js +3 -0
- package/dist/daemon/api/handlers/nodes.d.ts +1 -3
- package/dist/daemon/api/handlers/nodes.js +11 -46
- package/dist/daemon/api/handlers/profiles.js +7 -1
- package/dist/daemon/api/handlers/prospective-chat-inventory.d.ts +2 -0
- package/dist/daemon/api/handlers/prospective-chat-inventory.js +59 -0
- package/dist/daemon/api/handlers/reviews.js +10 -2
- package/dist/daemon/api/map.d.ts +2 -1
- package/dist/daemon/api/map.js +3 -2
- package/dist/daemon/api/server.js +6 -0
- package/dist/daemon/crtrd.js +6 -0
- package/dist/daemon/human/deliver-action.d.ts +16 -0
- package/dist/daemon/human/deliver-action.js +168 -0
- package/dist/daemon/human/finish.d.ts +8 -5
- package/dist/daemon/human/finish.js +45 -6
- package/dist/daemon/human/sweep.js +4 -1
- package/dist/daemon/reconcilers/human-delivery-lane.d.ts +10 -0
- package/dist/daemon/reconcilers/human-delivery-lane.js +41 -0
- package/dist/daemon/review/finish.d.ts +8 -3
- package/dist/daemon/review/finish.js +19 -1
- package/dist/hook-authoring.d.ts +75 -0
- package/dist/hook-authoring.js +358 -0
- package/dist/hook-process.d.ts +7 -0
- package/dist/hook-process.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/migrations/002-profile-project-memory.d.ts +2 -0
- package/dist/migrations/002-profile-project-memory.js +71 -0
- package/dist/migrations/profile-manifests.d.ts +30 -0
- package/dist/migrations/profile-manifests.js +70 -0
- package/dist/migrations/registry.js +10 -5
- package/dist/migrations/types.d.ts +28 -1
- package/dist/migrations/types.js +15 -9
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +21 -4
- package/dist/pi-extensions/canvas-structured-output.js +85 -2
- package/dist/types.d.ts +23 -6
- package/dist/types.js +1 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/builtin-memory/00-runtime-base.md +0 -55
- package/dist/builtin-memory/design.md +0 -55
- package/dist/clients/attach/__tests__/file-review-focus.test.js +0 -49
- /package/dist/builtin-memory/{02-lifecycle/00-terminal.md → 02-turn-lifecycle/01-terminal.md} +0 -0
- /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/fixtures/memory-slash-live-probe.d.ts} +0 -0
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
//
|
|
8
8
|
// `{id}` is the target node.
|
|
9
9
|
import { readdirSync } from 'node:fs';
|
|
10
|
-
import { basename
|
|
10
|
+
import { basename } from 'node:path';
|
|
11
11
|
import { appendSituationalContext, closeNode, getNode, listNodes, nowIso, reviveNode, } from '../../../index.js';
|
|
12
12
|
import { formatSituationalProse } from '../../../core/runtime/situational-context.js';
|
|
13
|
-
import { assertLaunchModelRegistered, forkNode,
|
|
13
|
+
import { assertLaunchModelRegistered, forkNode, spawnChild } from '../../../core/runtime/spawn.js';
|
|
14
14
|
import { childrenOf, subscribersOf, subscriptionsOf, setMessageWait, updateNode } from '../../../core/canvas/canvas.js';
|
|
15
15
|
import { subtreeIds } from '../../../core/canvas/nav-model.js';
|
|
16
16
|
import { ticketCountsForNodes } from '../../../core/canvas/attention.js';
|
|
@@ -32,7 +32,7 @@ import { BrokerUnreachableError } from '../../../core/runtime/broker-request.js'
|
|
|
32
32
|
import { isBrokerLive, persistDormantModel, setModelLive } from '../../../core/runtime/model-swap.js';
|
|
33
33
|
import { writeOutputSchema } from '../../../core/runtime/structured-output.js';
|
|
34
34
|
import { claimWarmNode, refillWarmPool, warmPoolEnabled } from '../../../core/runtime/warm-pool.js';
|
|
35
|
-
import {
|
|
35
|
+
import { resolveLaunchTarget } from '../../../core/runtime/launch-target.js';
|
|
36
36
|
import { fullName } from '../../../core/canvas/labels.js';
|
|
37
37
|
import { reportsForNode } from './reports.js';
|
|
38
38
|
import { getCron } from '../../../core/canvas/crons.js';
|
|
@@ -99,28 +99,6 @@ function parseCreateBody(body) {
|
|
|
99
99
|
}
|
|
100
100
|
return b;
|
|
101
101
|
}
|
|
102
|
-
/** The directory a new node is pinned to. An explicit `pin_cwd` wins; else a
|
|
103
|
-
* managed child inherits its spawner's cwd (which is what keeps a worktree
|
|
104
|
-
* node's whole subtree inside that worktree); else the launch's own `cwd`, the
|
|
105
|
-
* directory the create came from; else the profile's home.
|
|
106
|
-
*
|
|
107
|
-
* The launch directory beating the profile home is what lets one profile cover
|
|
108
|
-
* many working directories — a root started inside a planted instance runs in
|
|
109
|
-
* that instance while still taking its identity, purview, and memory from the
|
|
110
|
-
* profile covering it. The profile supplies a home only for a create that named
|
|
111
|
-
* no directory at all. */
|
|
112
|
-
function resolveNodeCwd(req, parent, profileId, contextCwd) {
|
|
113
|
-
if (req.pin_cwd !== undefined && req.pin_cwd !== '')
|
|
114
|
-
return resolve(req.pin_cwd);
|
|
115
|
-
if (parent != null) {
|
|
116
|
-
const spawner = getNode(parent);
|
|
117
|
-
if (spawner !== null)
|
|
118
|
-
return spawner.cwd;
|
|
119
|
-
}
|
|
120
|
-
if (req.cwd !== undefined && req.cwd !== '')
|
|
121
|
-
return resolve(req.cwd);
|
|
122
|
-
return profileHome(profileId) ?? contextCwd;
|
|
123
|
-
}
|
|
124
102
|
/** Build the immediate spawn recipe. */
|
|
125
103
|
function buildRecipe(req, parent, cwd) {
|
|
126
104
|
const recipe = {
|
|
@@ -185,9 +163,7 @@ function warmRequestFor(req, cwd, profileId) {
|
|
|
185
163
|
name: req.name ?? null,
|
|
186
164
|
};
|
|
187
165
|
}
|
|
188
|
-
export
|
|
189
|
-
return kind ?? profile?.manifest.default_kind ?? 'general';
|
|
190
|
-
}
|
|
166
|
+
export { resolveCreateKind } from '../../../core/runtime/launch-target.js';
|
|
191
167
|
function requestFromLaunch(launch) {
|
|
192
168
|
const intent = launch.modelIntent === undefined
|
|
193
169
|
? undefined
|
|
@@ -229,20 +205,8 @@ export async function handleCreate(ctx, deps = {}) {
|
|
|
229
205
|
// Resolve and validate the exact launch recipe before the warm pool can claim
|
|
230
206
|
// a spare (or the cold path can create a worktree/node). The registry belongs
|
|
231
207
|
// to this daemon, never to the possibly-remote CLI caller.
|
|
232
|
-
const
|
|
233
|
-
const profileId = await resolveProfileId(req.profile, parent ?? null, contextCwd);
|
|
234
|
-
const profile = assertProfileActive(profileId);
|
|
235
|
-
const kind = resolveCreateKind(req.kind, profile);
|
|
208
|
+
const { profileId, kind, cwd, launch } = await resolveLaunchTarget(req, { validateKind: assertKindServer });
|
|
236
209
|
const resolvedReq = { ...req, kind };
|
|
237
|
-
const cwd = resolveNodeCwd(resolvedReq, parent, profileId, contextCwd);
|
|
238
|
-
await assertKindServer(kind, cwd, profileId);
|
|
239
|
-
const launch = (await buildLaunchSpecAsync(kind, (resolvedReq.mode ?? 'base'), {
|
|
240
|
-
lifecycle: req.root === true ? (req.root_lifecycle ?? 'resident') : 'terminal',
|
|
241
|
-
hasManager: req.root !== true,
|
|
242
|
-
...(req.model !== undefined ? { model: req.model } : {}),
|
|
243
|
-
cwd,
|
|
244
|
-
profileId,
|
|
245
|
-
})).launch;
|
|
246
210
|
await assertLaunchModelRegistered(requestFromLaunch(launch), { cwd, profileId }, deps.modelRegistry);
|
|
247
211
|
// --- Warm path: hand out an already-booted spare instead of paying a full
|
|
248
212
|
// engine boot. A miss is not a failure — it falls through to the cold
|
|
@@ -680,7 +644,7 @@ export async function handleConfig(ctx, deps = {}) {
|
|
|
680
644
|
// --- kind / mode / lifecycle: reshape via canonical primitive (CLI parity). ---
|
|
681
645
|
if (patch.kind !== undefined || patch.mode !== undefined || patch.lifecycle !== undefined) {
|
|
682
646
|
if (patch.kind !== undefined)
|
|
683
|
-
await assertKindServer(patch.kind
|
|
647
|
+
await assertKindServer(patch.kind);
|
|
684
648
|
const newKind = patch.kind ?? meta.kind;
|
|
685
649
|
const newMode = (patch.mode ?? meta.mode);
|
|
686
650
|
const newLifecycle = patch.lifecycle ?? meta.lifecycle;
|
|
@@ -712,10 +676,11 @@ export async function handleConfig(ctx, deps = {}) {
|
|
|
712
676
|
// to receive it, so it is a documented Stage-A no-op here.
|
|
713
677
|
return { status: 200, body: detailOf(id) };
|
|
714
678
|
}
|
|
715
|
-
/**
|
|
716
|
-
*
|
|
717
|
-
|
|
718
|
-
|
|
679
|
+
/** POST /v1/nodes and PATCH /v1/nodes/:id/config validate kinds in the daemon's
|
|
680
|
+
* ambient configuration; prospective reads validate their resolved target. The
|
|
681
|
+
* read is subprocess-isolated — crtrd must never resolve config on its own loop. */
|
|
682
|
+
async function assertKindServer(kind) {
|
|
683
|
+
const kinds = Object.keys((await readMergedLaunchConfigAsync(process.cwd(), process.env['CRTR_PROFILE_ID'] || null)).kinds);
|
|
719
684
|
if (!kinds.includes(kind)) {
|
|
720
685
|
throw usage(`unknown kind: ${kind}`, { received: kind, next: `Valid kinds: ${kinds.sort().join(', ')}.` });
|
|
721
686
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// GET /v1/profiles/:name resolve one profile by name or id
|
|
8
8
|
// DELETE /v1/profiles/:name required-force cascade or detach
|
|
9
9
|
import { assertInstalledKind } from '../../../core/config.js';
|
|
10
|
-
import { createProfile, ensureRootProfile, listProfiles, loadProfileManifest, } from '../../../core/profiles/manifest.js';
|
|
10
|
+
import { assertProfileProjects, createProfile, ensureRootProfile, listProfiles, loadProfileManifest, } from '../../../core/profiles/manifest.js';
|
|
11
11
|
import { requestProfileDeletion } from '../../profile-delete.js';
|
|
12
12
|
import { toProfileDTO } from '../map.js';
|
|
13
13
|
const ROOT_PROFILE_NAME = 'root';
|
|
@@ -19,6 +19,12 @@ const ROOT_PROFILE_NAME = 'root';
|
|
|
19
19
|
function handleEnsure(ctx) {
|
|
20
20
|
const name = ctx.params['name'];
|
|
21
21
|
const req = (ctx.body ?? {});
|
|
22
|
+
// Shape check runs before the idempotent returns, so a legacy string array
|
|
23
|
+
// cannot slip through an ensure of root or an existing profile. Existence
|
|
24
|
+
// and realpath normalization stay create-only, so a valid entry naming a
|
|
25
|
+
// since-deleted directory still returns the existing profile.
|
|
26
|
+
if (req.projects !== undefined)
|
|
27
|
+
assertProfileProjects(req.projects);
|
|
22
28
|
if (name === ROOT_PROFILE_NAME) {
|
|
23
29
|
return { status: 200, body: toProfileDTO(ensureRootProfile()) };
|
|
24
30
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ApiError } from '../../../api/errors.js';
|
|
2
|
+
import { usage } from '../../../core/errors.js';
|
|
3
|
+
import { resolveLaunchTarget } from '../../../core/runtime/launch-target.js';
|
|
4
|
+
import { runProspectiveInventory } from '../../../core/runtime/prospective-inventory.js';
|
|
5
|
+
const inFlight = new Map();
|
|
6
|
+
function singleQueryValue(ctx, name) {
|
|
7
|
+
const values = ctx.query.getAll(name);
|
|
8
|
+
if (values.length > 1)
|
|
9
|
+
throw usage(`${name} must appear at most once`);
|
|
10
|
+
return values[0];
|
|
11
|
+
}
|
|
12
|
+
async function handleGet(ctx) {
|
|
13
|
+
for (const [name] of ctx.query) {
|
|
14
|
+
if (name !== 'profile' && name !== 'cwd' && name !== 'kind')
|
|
15
|
+
throw usage(`unknown prospective inventory query parameter: ${name}`);
|
|
16
|
+
}
|
|
17
|
+
const profile = singleQueryValue(ctx, 'profile');
|
|
18
|
+
const cwd = singleQueryValue(ctx, 'cwd');
|
|
19
|
+
const kind = singleQueryValue(ctx, 'kind');
|
|
20
|
+
if (kind !== undefined && kind.trim() === '')
|
|
21
|
+
throw usage('kind must be a non-empty string when provided');
|
|
22
|
+
const target = await resolveLaunchTarget({
|
|
23
|
+
root: true,
|
|
24
|
+
...(profile !== undefined ? { profile } : {}),
|
|
25
|
+
...(cwd !== undefined ? { cwd } : {}),
|
|
26
|
+
...(kind !== undefined ? { kind } : {}),
|
|
27
|
+
}, { readOnlyProfileSelection: true });
|
|
28
|
+
const key = JSON.stringify([target.profileId, target.cwd, target.kind]);
|
|
29
|
+
let pending = inFlight.get(key);
|
|
30
|
+
if (pending === undefined) {
|
|
31
|
+
pending = runProspectiveInventory({
|
|
32
|
+
cwd: target.cwd,
|
|
33
|
+
profileId: target.profileId,
|
|
34
|
+
extensions: target.launch.extensions,
|
|
35
|
+
});
|
|
36
|
+
inFlight.set(key, pending);
|
|
37
|
+
void pending.finally(() => {
|
|
38
|
+
if (inFlight.get(key) === pending)
|
|
39
|
+
inFlight.delete(key);
|
|
40
|
+
}).catch(() => { });
|
|
41
|
+
}
|
|
42
|
+
let rows;
|
|
43
|
+
try {
|
|
44
|
+
rows = await pending;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new ApiError(503, 'preflight_failed', 'prospective chat inventory preflight failed');
|
|
48
|
+
}
|
|
49
|
+
const body = {
|
|
50
|
+
profile_id: target.profileId,
|
|
51
|
+
cwd: target.cwd,
|
|
52
|
+
commands: rows.commands,
|
|
53
|
+
memory_refs: rows.memory_refs,
|
|
54
|
+
};
|
|
55
|
+
return { status: 200, body };
|
|
56
|
+
}
|
|
57
|
+
export const prospectiveChatInventoryRoutes = [
|
|
58
|
+
{ method: 'GET', pattern: '/v1/prospective-chat-inventory', handler: handleGet },
|
|
59
|
+
];
|
|
@@ -14,7 +14,7 @@ import { toFeedbackResultDTO, toReviewDTO } from '../map.js';
|
|
|
14
14
|
import { invalidRequest as rejectRequest, objectBody, optionalString, requireParam, requiredString, } from './validate.js';
|
|
15
15
|
const REVIEW_STATES = new Set(['open', 'approved', 'canceled']);
|
|
16
16
|
const CREATE_FIELDS = ['origin_kind', 'origin_node_id', 'file', 'idempotency_key', 'subtitle', 'output_path'];
|
|
17
|
-
const CANCEL_FIELDS = ['reason', 'actor'];
|
|
17
|
+
const CANCEL_FIELDS = ['reason', 'actor', 'disposition'];
|
|
18
18
|
function invalidRequest(violations) {
|
|
19
19
|
rejectRequest('review request', violations);
|
|
20
20
|
}
|
|
@@ -131,7 +131,15 @@ function parseCancel(ctx, violations) {
|
|
|
131
131
|
const body = ctx.body === undefined ? {} : objectBody(ctx.body, CANCEL_FIELDS, violations);
|
|
132
132
|
const reason = optionalString(body, 'reason', violations);
|
|
133
133
|
const actor = optionalString(body, 'actor', violations);
|
|
134
|
-
|
|
134
|
+
const disposition = optionalString(body, 'disposition', violations);
|
|
135
|
+
if (disposition !== undefined && disposition !== 'canceled' && disposition !== 'dismissed') {
|
|
136
|
+
violations.push({ field: 'disposition', problem: 'must be "canceled" or "dismissed"', received: disposition });
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
...(reason === undefined ? {} : { reason }),
|
|
140
|
+
...(actor === undefined ? {} : { actor }),
|
|
141
|
+
...(disposition === 'canceled' || disposition === 'dismissed' ? { disposition } : {}),
|
|
142
|
+
};
|
|
135
143
|
}
|
|
136
144
|
function requireEmptyBody(ctx, violations) {
|
|
137
145
|
objectBody(ctx.body, [], violations);
|
package/dist/daemon/api/map.d.ts
CHANGED
|
@@ -60,7 +60,8 @@ export declare function toCronRunDTO(r: CronRunRecord): CronRunDTO;
|
|
|
60
60
|
/** Project a profile-manifest entry into its DTO. `default_dir` (the directory
|
|
61
61
|
* this profile is pinned as default for) is a reverse lookup not carried on the
|
|
62
62
|
* entry, so it is left null here — a caller that needs it resolves it
|
|
63
|
-
* separately.
|
|
63
|
+
* separately. Project entries are cloned: the manifest entry is shared cache
|
|
64
|
+
* state, so handing out its objects would let a DTO consumer mutate it. */
|
|
64
65
|
export declare function toProfileDTO(entry: ProfileEntry): ProfileDTO;
|
|
65
66
|
/** Extra daemon-derived review fields whose source is outside the canonical row. */
|
|
66
67
|
export interface ReviewDTOExtras {
|
package/dist/daemon/api/map.js
CHANGED
|
@@ -258,12 +258,13 @@ export function toCronRunDTO(r) {
|
|
|
258
258
|
/** Project a profile-manifest entry into its DTO. `default_dir` (the directory
|
|
259
259
|
* this profile is pinned as default for) is a reverse lookup not carried on the
|
|
260
260
|
* entry, so it is left null here — a caller that needs it resolves it
|
|
261
|
-
* separately.
|
|
261
|
+
* separately. Project entries are cloned: the manifest entry is shared cache
|
|
262
|
+
* state, so handing out its objects would let a DTO consumer mutate it. */
|
|
262
263
|
export function toProfileDTO(entry) {
|
|
263
264
|
return {
|
|
264
265
|
id: entry.profileId,
|
|
265
266
|
name: entry.manifest.name,
|
|
266
|
-
projects:
|
|
267
|
+
projects: entry.manifest.projects.map((p) => ({ ...p })),
|
|
267
268
|
home: entry.manifest.home,
|
|
268
269
|
paused_at: entry.manifest.paused_at,
|
|
269
270
|
default_kind: entry.manifest.default_kind ?? 'general',
|
|
@@ -22,11 +22,13 @@ import { attachRoutes } from './handlers/attach.js';
|
|
|
22
22
|
import { bashJobRoutes } from './handlers/bash-jobs.js';
|
|
23
23
|
import { brokerOperationRoutes } from './handlers/broker-ops.js';
|
|
24
24
|
import { canvasRoutes } from './handlers/canvas.js';
|
|
25
|
+
import { chatInventoryRoutes } from './handlers/chat-inventory.js';
|
|
25
26
|
import { daemonRoutes } from './handlers/daemon.js';
|
|
26
27
|
import { fileRoutes } from './handlers/files.js';
|
|
27
28
|
import { focusRoutes } from './handlers/focus.js';
|
|
28
29
|
import { healthRoutes } from './handlers/health.js';
|
|
29
30
|
import { humanRoutes } from './handlers/human.js';
|
|
31
|
+
import { humanRequestRoutes } from './handlers/human-requests.js';
|
|
30
32
|
import { inboxRoutes } from './handlers/inbox.js';
|
|
31
33
|
import { feedbackCommentRoutes } from './handlers/feedback-comments.js';
|
|
32
34
|
import { memoryRoutes } from './handlers/memory.js';
|
|
@@ -34,6 +36,7 @@ import { messageRoutes } from './handlers/messages.js';
|
|
|
34
36
|
import { modelAuthRoutes } from './handlers/modelauth.js';
|
|
35
37
|
import { nodeRoutes } from './handlers/nodes.js';
|
|
36
38
|
import { profileRoutes } from './handlers/profiles.js';
|
|
39
|
+
import { prospectiveChatInventoryRoutes } from './handlers/prospective-chat-inventory.js';
|
|
37
40
|
import { reportRoutes } from './handlers/reports.js';
|
|
38
41
|
import { reviewCommentRoutes } from './handlers/review-comments.js';
|
|
39
42
|
import { reviewRoutes } from './handlers/reviews.js';
|
|
@@ -66,6 +69,9 @@ function buildRouter() {
|
|
|
66
69
|
.registerAll(reviewRoutes)
|
|
67
70
|
.registerAll(reviewCommentRoutes)
|
|
68
71
|
.registerAll(inboxRoutes)
|
|
72
|
+
.registerAll(humanRequestRoutes)
|
|
73
|
+
.registerAll(chatInventoryRoutes)
|
|
74
|
+
.registerAll(prospectiveChatInventoryRoutes)
|
|
69
75
|
.registerAll(feedbackCommentRoutes);
|
|
70
76
|
}
|
|
71
77
|
/** Is this request an attach upgrade — `GET /v1/nodes/{id}/attach` — and if so,
|
package/dist/daemon/crtrd.js
CHANGED
|
@@ -50,6 +50,7 @@ import { crtrHome, isSafeNodeId } from '../core/canvas/paths.js';
|
|
|
50
50
|
import { listNodes, migrateLegacyPidIdentities, } from '../core/canvas/index.js';
|
|
51
51
|
import { createApiServer } from './api/server.js';
|
|
52
52
|
import { reconcileUndeliveredTickets } from './human/sweep.js';
|
|
53
|
+
import { recoverRunningActionDeliveries } from '../core/canvas/human-deliveries.js';
|
|
53
54
|
import { sweepReviews } from './review/sweep.js';
|
|
54
55
|
import { pidfilePath } from './pidfile.js';
|
|
55
56
|
import { rendererWarning } from '../core/runtime/package-health.js';
|
|
@@ -57,6 +58,7 @@ import { DEFAULT_INTERVAL_MS } from './supervise-cadence.js';
|
|
|
57
58
|
import { BrokerSupervisionReconciler } from './reconcilers/broker-supervision.js';
|
|
58
59
|
import { ControllerDeathReconciler, DormantInboxReconciler } from './reconcilers/dormant-inbox.js';
|
|
59
60
|
import { CronLaneReconciler } from './reconcilers/cron-lane.js';
|
|
61
|
+
import { HumanDeliveryLaneReconciler } from './reconcilers/human-delivery-lane.js';
|
|
60
62
|
import { PendingReviewSubmitReconciler } from './reconcilers/pending-review-submit.js';
|
|
61
63
|
import { StorageMaintenanceReconciler } from './reconcilers/storage-maintenance.js';
|
|
62
64
|
import { captureLivenessSnapshot, capturePidCommand, captureTeardownSnapshot, isPidAlive, killProcessTreePids, } from '../core/canvas/pid.js';
|
|
@@ -312,6 +314,7 @@ const brokerSupervision = new BrokerSupervisionReconciler();
|
|
|
312
314
|
const controllerDeath = new ControllerDeathReconciler();
|
|
313
315
|
const dormantInbox = new DormantInboxReconciler();
|
|
314
316
|
const cronLane = new CronLaneReconciler();
|
|
317
|
+
const humanDeliveryLane = new HumanDeliveryLaneReconciler();
|
|
315
318
|
const pendingReviewSubmit = new PendingReviewSubmitReconciler();
|
|
316
319
|
const storageMaintenance = new StorageMaintenanceReconciler();
|
|
317
320
|
/** Supervision tick over the daemon's current live set. */
|
|
@@ -336,6 +339,7 @@ export async function superviseTick(now = Date.now(), lifecycle = directTickLife
|
|
|
336
339
|
controllerDeath.run(now, { rows });
|
|
337
340
|
dormantInbox.run(now, { rows, fleet, hasBrokerCapacity });
|
|
338
341
|
cronLane.run(now, { lifecycle, hasBrokerCapacity });
|
|
342
|
+
humanDeliveryLane.run(now, { lifecycle });
|
|
339
343
|
pendingReviewSubmit.run({ lifecycle });
|
|
340
344
|
storageMaintenance.run(now);
|
|
341
345
|
}
|
|
@@ -652,6 +656,8 @@ export async function runDaemon(opts = {}) {
|
|
|
652
656
|
await operationIdContext.fresh(async () => {
|
|
653
657
|
await sweepReviews();
|
|
654
658
|
await reconcileUndeliveredTickets();
|
|
659
|
+
// Unlike a cron one-shot, a completion is at-least-once: destinations deduplicate request IDs, so a dead daemon's running delivery is re-queued immediately.
|
|
660
|
+
recoverRunningActionDeliveries(Date.now());
|
|
655
661
|
});
|
|
656
662
|
// Boot reconciliation clears stale process identities before any tick can
|
|
657
663
|
// inspect them, then migrates legacy identities only with proven same-boot
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ActionDeliveryRecord, type ActionDeliverySettlement } from '../../core/canvas/human-deliveries.js';
|
|
2
|
+
export declare const ACTION_DELIVERY_TIMEOUT_MS = 120000;
|
|
3
|
+
export declare const ACTION_DELIVERY_STDERR_TAIL_BYTES: number;
|
|
4
|
+
/** Durable exponential retry delay for the attempt that just failed. */
|
|
5
|
+
export declare function actionDeliveryBackoffMs(attempt: number): number;
|
|
6
|
+
export interface ActionDeliveryProcessOutcome {
|
|
7
|
+
exitCode: number | null;
|
|
8
|
+
signal: string | null;
|
|
9
|
+
timedOut: boolean;
|
|
10
|
+
spawnError?: Error;
|
|
11
|
+
stderr: string;
|
|
12
|
+
}
|
|
13
|
+
/** Process contract mapping. stdout deliberately does not participate. */
|
|
14
|
+
export declare function actionDeliverySettlementForOutcome(outcome: ActionDeliveryProcessOutcome, attempt: number, now: number): ActionDeliverySettlement;
|
|
15
|
+
/** Spawn and settle one already-claimed action delivery. */
|
|
16
|
+
export declare function deliverAction(delivery: ActionDeliveryRecord, claimOwner: string): Promise<void>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { settleActionDelivery, } from '../../core/canvas/human-deliveries.js';
|
|
3
|
+
import { killProcessGroup } from '../../core/canvas/pid.js';
|
|
4
|
+
import { buildOperationalEnvBase } from '../../core/runtime/spawn-env.js';
|
|
5
|
+
export const ACTION_DELIVERY_TIMEOUT_MS = 120_000;
|
|
6
|
+
const ACTION_DELIVERY_KILL_GRACE_MS = 2_000;
|
|
7
|
+
export const ACTION_DELIVERY_STDERR_TAIL_BYTES = 4 * 1024;
|
|
8
|
+
/** Durable exponential retry delay for the attempt that just failed. */
|
|
9
|
+
export function actionDeliveryBackoffMs(attempt) {
|
|
10
|
+
return Math.min(5_000 * 2 ** (attempt - 1), 3_600_000);
|
|
11
|
+
}
|
|
12
|
+
/** Process contract mapping. stdout deliberately does not participate. */
|
|
13
|
+
export function actionDeliverySettlementForOutcome(outcome, attempt, now) {
|
|
14
|
+
if (outcome.spawnError !== undefined) {
|
|
15
|
+
return {
|
|
16
|
+
state: 'pending',
|
|
17
|
+
nextAttemptAt: now + actionDeliveryBackoffMs(attempt),
|
|
18
|
+
failure: failure('spawn_error', { message: outcome.spawnError.message, stderr: outcome.stderr }),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (outcome.timedOut) {
|
|
22
|
+
return {
|
|
23
|
+
state: 'pending',
|
|
24
|
+
nextAttemptAt: now + actionDeliveryBackoffMs(attempt),
|
|
25
|
+
// The kill escalation's own signal distinguishes the graceful term from the SIGKILL.
|
|
26
|
+
failure: failure('timeout', { ...(outcome.signal === null ? {} : { signal: outcome.signal }), stderr: outcome.stderr }),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (outcome.signal !== null) {
|
|
30
|
+
return {
|
|
31
|
+
state: 'pending',
|
|
32
|
+
nextAttemptAt: now + actionDeliveryBackoffMs(attempt),
|
|
33
|
+
failure: failure('signal', { signal: outcome.signal, stderr: outcome.stderr }),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (outcome.exitCode === 0)
|
|
37
|
+
return { state: 'accepted' };
|
|
38
|
+
if (outcome.exitCode === 78) {
|
|
39
|
+
return { state: 'permanent_failed', failure: failure('exit', { exitCode: 78, stderr: outcome.stderr }) };
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
state: 'pending',
|
|
43
|
+
nextAttemptAt: now + actionDeliveryBackoffMs(attempt),
|
|
44
|
+
failure: failure('exit', { exitCode: outcome.exitCode ?? undefined, stderr: outcome.stderr }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function failure(kind, fields) {
|
|
48
|
+
return { kind, ...fields };
|
|
49
|
+
}
|
|
50
|
+
function stderrTailCapture() {
|
|
51
|
+
let tail = Buffer.alloc(0);
|
|
52
|
+
return {
|
|
53
|
+
take(chunk) {
|
|
54
|
+
tail = chunk.length >= ACTION_DELIVERY_STDERR_TAIL_BYTES
|
|
55
|
+
? chunk.subarray(chunk.length - ACTION_DELIVERY_STDERR_TAIL_BYTES)
|
|
56
|
+
: Buffer.concat([tail, chunk]).subarray(Math.max(0, tail.length + chunk.length - ACTION_DELIVERY_STDERR_TAIL_BYTES));
|
|
57
|
+
},
|
|
58
|
+
decode() {
|
|
59
|
+
return tail.toString('utf8');
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function completionForAttempt(completion, attempt) {
|
|
64
|
+
const document = completion;
|
|
65
|
+
const delivery = document.delivery;
|
|
66
|
+
return JSON.stringify({ ...document, delivery: { ...delivery, attempt } });
|
|
67
|
+
}
|
|
68
|
+
/** Spawn and settle one already-claimed action delivery. */
|
|
69
|
+
export async function deliverAction(delivery, claimOwner) {
|
|
70
|
+
const stderr = stderrTailCapture();
|
|
71
|
+
let completion;
|
|
72
|
+
try {
|
|
73
|
+
completion = completionForAttempt(delivery.completion, delivery.attempt);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
settle(delivery, claimOwner, actionDeliverySettlementForOutcome({
|
|
77
|
+
exitCode: null,
|
|
78
|
+
signal: null,
|
|
79
|
+
timedOut: false,
|
|
80
|
+
spawnError: error,
|
|
81
|
+
stderr: stderr.decode(),
|
|
82
|
+
}, delivery.attempt, Date.now()));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let child;
|
|
86
|
+
try {
|
|
87
|
+
child = spawn(delivery.argv[0], delivery.argv.slice(1), {
|
|
88
|
+
cwd: delivery.cwd,
|
|
89
|
+
env: actionDeliveryEnv(delivery.cwd),
|
|
90
|
+
detached: true,
|
|
91
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
settle(delivery, claimOwner, actionDeliverySettlementForOutcome({
|
|
96
|
+
exitCode: null,
|
|
97
|
+
signal: null,
|
|
98
|
+
timedOut: false,
|
|
99
|
+
spawnError: error,
|
|
100
|
+
stderr: stderr.decode(),
|
|
101
|
+
}, delivery.attempt, Date.now()));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
await new Promise((resolve) => {
|
|
105
|
+
let settled = false;
|
|
106
|
+
let timedOut = false;
|
|
107
|
+
let killTimer;
|
|
108
|
+
let timeout;
|
|
109
|
+
const finish = (outcome) => {
|
|
110
|
+
if (settled)
|
|
111
|
+
return;
|
|
112
|
+
settled = true;
|
|
113
|
+
if (timeout !== undefined)
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
if (killTimer !== undefined)
|
|
116
|
+
clearTimeout(killTimer);
|
|
117
|
+
child.stdin?.destroy();
|
|
118
|
+
child.stdout?.destroy();
|
|
119
|
+
child.stderr?.destroy();
|
|
120
|
+
settle(delivery, claimOwner, actionDeliverySettlementForOutcome(outcome, delivery.attempt, Date.now()));
|
|
121
|
+
resolve();
|
|
122
|
+
};
|
|
123
|
+
child.stdout?.on('data', () => { }); // Drain diagnostics; stdout is never a control channel.
|
|
124
|
+
child.stderr?.on('data', (chunk) => stderr.take(chunk));
|
|
125
|
+
child.stdin?.on('error', () => { }); // A child may exit before draining a large completion; its exit code remains authoritative.
|
|
126
|
+
child.on('error', (error) => {
|
|
127
|
+
finish({ exitCode: null, signal: null, timedOut: false, spawnError: error, stderr: stderr.decode() });
|
|
128
|
+
});
|
|
129
|
+
child.on('exit', (exitCode, signal) => {
|
|
130
|
+
finish({
|
|
131
|
+
exitCode,
|
|
132
|
+
signal,
|
|
133
|
+
timedOut,
|
|
134
|
+
stderr: stderr.decode(),
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
const killWithEscalation = () => {
|
|
138
|
+
if (child.pid === undefined)
|
|
139
|
+
return;
|
|
140
|
+
killProcessGroup(child.pid);
|
|
141
|
+
killTimer = setTimeout(() => killProcessGroup(child.pid, 'SIGKILL'), ACTION_DELIVERY_KILL_GRACE_MS);
|
|
142
|
+
killTimer.unref?.();
|
|
143
|
+
};
|
|
144
|
+
// The deadline starts at spawn, not after stdin write or any output.
|
|
145
|
+
timeout = setTimeout(() => {
|
|
146
|
+
timedOut = true;
|
|
147
|
+
killWithEscalation();
|
|
148
|
+
}, ACTION_DELIVERY_TIMEOUT_MS);
|
|
149
|
+
timeout.unref?.();
|
|
150
|
+
child.stdin?.end(completion, 'utf8');
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/** The delivery child crosses the same consent boundary as every other
|
|
154
|
+
* crouter spawn: nothing of crtrd's own environment reaches it except what
|
|
155
|
+
* `spawnEnv.allow` and the operational base admit for the action's own cwd.
|
|
156
|
+
* No profile participates — an action is declared in scope config alone. */
|
|
157
|
+
function actionDeliveryEnv(cwd) {
|
|
158
|
+
const env = buildOperationalEnvBase({ targetCwd: cwd, targetProfileId: null });
|
|
159
|
+
// Runtime identity, not host state: an action that calls `crtr` must reach
|
|
160
|
+
// the daemon that delivered to it, exactly as a cron body does.
|
|
161
|
+
const canvasHome = process.env['CRTR_HOME'];
|
|
162
|
+
if (canvasHome !== undefined && canvasHome !== '')
|
|
163
|
+
env['CRTR_HOME'] = canvasHome;
|
|
164
|
+
return env;
|
|
165
|
+
}
|
|
166
|
+
function settle(delivery, claimOwner, settlement) {
|
|
167
|
+
settleActionDelivery(delivery.requestId, delivery.attempt, claimOwner, settlement, Date.now());
|
|
168
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CancelTicketOptions, type CompletePageOptions } from '../../core/human/tickets.js';
|
|
1
2
|
import type { TicketResult } from '../../core/human/types.js';
|
|
2
3
|
/** Every ticket-ending action retires the ticket's feedback companion — done
|
|
3
4
|
* with a canonical final when approved, never left resident. Idempotent and replay-safe; a
|
|
@@ -11,12 +12,14 @@ export declare function settleFeedbackCompanion(ticketId: string, outcome: 'appr
|
|
|
11
12
|
* through this function, so the reply-route descriptor is the sole authority
|
|
12
13
|
* on whether completion reaches a bridge.
|
|
13
14
|
*/
|
|
14
|
-
export declare function resolvePageTicket(ticketId: string, responses: unknown): Promise<TicketResult>;
|
|
15
|
+
export declare function resolvePageTicket(ticketId: string, responses: unknown, opts?: CompletePageOptions): Promise<TicketResult>;
|
|
15
16
|
/** Cancel a ticket with the store's first-writer-wins publication. */
|
|
16
|
-
export declare function cancelHumanTicket(ticketId: string, opts:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
export declare function cancelHumanTicket(ticketId: string, opts: CancelTicketOptions): Promise<TicketResult>;
|
|
18
|
+
/** Schedule the settled request's completion action, if it bound one. Only the
|
|
19
|
+
* settlement winner calls this; the daemon-start sweep replays it for a
|
|
20
|
+
* settlement that died before enqueueing, and the insert is idempotent on the
|
|
21
|
+
* request id so neither path can create a second delivery. */
|
|
22
|
+
export declare function releaseActionDelivery(ticketId: string): void;
|
|
20
23
|
/**
|
|
21
24
|
* Deliver a terminal ticket result to its bridge. The canonical response is the
|
|
22
25
|
* receipt: every action below is replay-safe, so lost delivery can be retried
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { ApiError } from '../../api/index.js';
|
|
2
2
|
import { getRow, subscribersOf } from '../../core/canvas/canvas.js';
|
|
3
|
-
import { ticketDir } from '../../core/human/root.js';
|
|
3
|
+
import { opaqueInboxTicketId, ticketDir } from '../../core/human/root.js';
|
|
4
|
+
import { readTicketActionBinding } from '../../core/human/action-binding.js';
|
|
5
|
+
import { completionEventFor, composeHumanCompletion } from '../../core/human/completion.js';
|
|
6
|
+
import { enqueueActionDelivery } from '../../core/canvas/human-deliveries.js';
|
|
4
7
|
import { parsePage } from '../../core/human/page.js';
|
|
5
8
|
import { cancelTicket, projectReviewOutput, readTicketResult, takeoverAndCompletePage, } from '../../core/human/tickets.js';
|
|
6
9
|
import { describePageAnswer } from '../../core/human/answer.js';
|
|
@@ -151,10 +154,11 @@ async function deliverOrConflict(ticketId) {
|
|
|
151
154
|
* through this function, so the reply-route descriptor is the sole authority
|
|
152
155
|
* on whether completion reaches a bridge.
|
|
153
156
|
*/
|
|
154
|
-
export async function resolvePageTicket(ticketId, responses) {
|
|
155
|
-
const completed = takeoverAndCompletePage(ticketDir(ticketId), responses, { host: 'crtrd', pid: process.pid });
|
|
157
|
+
export async function resolvePageTicket(ticketId, responses, opts = {}) {
|
|
158
|
+
const completed = takeoverAndCompletePage(ticketDir(ticketId), responses, { host: 'crtrd', pid: process.pid }, opts);
|
|
156
159
|
if (!completed.won)
|
|
157
160
|
return deliverOrConflict(ticketId);
|
|
161
|
+
releaseActionDelivery(ticketId);
|
|
158
162
|
try {
|
|
159
163
|
await deliverTerminalResult(ticketId);
|
|
160
164
|
}
|
|
@@ -168,6 +172,7 @@ export async function cancelHumanTicket(ticketId, opts) {
|
|
|
168
172
|
const canceled = cancelTicket(ticketDir(ticketId), opts);
|
|
169
173
|
if (canceled.status === 'already_resolved')
|
|
170
174
|
return deliverOrConflict(ticketId);
|
|
175
|
+
releaseActionDelivery(ticketId);
|
|
171
176
|
try {
|
|
172
177
|
await deliverTerminalResult(ticketId);
|
|
173
178
|
}
|
|
@@ -176,6 +181,40 @@ export async function cancelHumanTicket(ticketId, opts) {
|
|
|
176
181
|
}
|
|
177
182
|
return canceled.result;
|
|
178
183
|
}
|
|
184
|
+
/** Schedule the settled request's completion action, if it bound one. Only the
|
|
185
|
+
* settlement winner calls this; the daemon-start sweep replays it for a
|
|
186
|
+
* settlement that died before enqueueing, and the insert is idempotent on the
|
|
187
|
+
* request id so neither path can create a second delivery. */
|
|
188
|
+
export function releaseActionDelivery(ticketId) {
|
|
189
|
+
try {
|
|
190
|
+
const dir = ticketDir(ticketId);
|
|
191
|
+
const binding = readTicketActionBinding(dir);
|
|
192
|
+
if (binding === null)
|
|
193
|
+
return;
|
|
194
|
+
const result = readTicketResult(dir);
|
|
195
|
+
if (result === null || result.kind === 'review')
|
|
196
|
+
return;
|
|
197
|
+
const requestId = opaqueInboxTicketId(ticketId);
|
|
198
|
+
enqueueActionDelivery({
|
|
199
|
+
requestId,
|
|
200
|
+
argv: binding.argv,
|
|
201
|
+
cwd: binding.cwd,
|
|
202
|
+
completion: composeHumanCompletion({ requestId, result, binding, source: pageSource(dir) }),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
// The canonical outcome is already durable; the sweep replays this.
|
|
207
|
+
emitEvent({ level: 'error', event: 'human.action_delivery.enqueue_failed', node_id: ticketId, error });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function pageSource(dir) {
|
|
211
|
+
try {
|
|
212
|
+
return parsePage(dir).source ?? {};
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return {};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
179
218
|
/**
|
|
180
219
|
* Deliver a terminal ticket result to its bridge. The canonical response is the
|
|
181
220
|
* receipt: every action below is replay-safe, so lost delivery can be retried
|
|
@@ -207,9 +246,9 @@ export async function deliverTerminalResult(ticketId) {
|
|
|
207
246
|
if (result.kind === 'canceled') {
|
|
208
247
|
const title = pageTitle(dir);
|
|
209
248
|
const reason = result.reason !== undefined && result.reason !== '' ? result.reason : 'no reason was given';
|
|
210
|
-
// A
|
|
211
|
-
//
|
|
212
|
-
const dismissed = result
|
|
249
|
+
// A dismissal is a rejection, not a routing event: say so plainly so the
|
|
250
|
+
// agent acts on the "no" instead of re-raising the same ask.
|
|
251
|
+
const dismissed = completionEventFor(result) === 'dismissed';
|
|
213
252
|
const subject = dismissed
|
|
214
253
|
? (title === undefined ? `human interaction ${ticketId} dismissed without answering` : `human interaction ${ticketId} — “${title}” dismissed without answering`)
|
|
215
254
|
: (title === undefined ? `human interaction ${ticketId} canceled — no answer is coming` : `human interaction ${ticketId} — “${title}” canceled, no answer is coming`);
|
|
@@ -5,7 +5,7 @@ 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, settleFeedbackCompanion } from './finish.js';
|
|
8
|
+
import { deliverTerminalResult, releaseActionDelivery, settleFeedbackCompanion } from './finish.js';
|
|
9
9
|
function isTerminalBridge(status) {
|
|
10
10
|
return status === 'done' || status === 'dead' || status === 'canceled';
|
|
11
11
|
}
|
|
@@ -32,6 +32,9 @@ export async function reconcileUndeliveredTickets() {
|
|
|
32
32
|
// reply bridge exists: inbox-only pages have no bridge row, yet the
|
|
33
33
|
// daemon can die between result publication and companion retirement.
|
|
34
34
|
await settleFeedbackCompanion(entry, ticketResult.kind === 'canceled' ? 'canceled' : 'approved');
|
|
35
|
+
// A settlement that died before enqueueing its completion action still
|
|
36
|
+
// owes that delivery; the insert is idempotent on the request id.
|
|
37
|
+
releaseActionDelivery(entry);
|
|
35
38
|
// The ROW is what carries status; `getNode` would answer from a pruned
|
|
36
39
|
// bridge's surviving meta.json with no status at all.
|
|
37
40
|
const bridge = getRow(entry);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DetachedWorkLifecycle } from './broker-supervision.js';
|
|
2
|
+
/** One daemon-life identity for guarded action-delivery claims. */
|
|
3
|
+
export declare const humanDeliveryDaemonInstanceId: `${string}-${string}-${string}-${string}-${string}`;
|
|
4
|
+
export interface HumanDeliveryLaneContext {
|
|
5
|
+
lifecycle: DetachedWorkLifecycle;
|
|
6
|
+
}
|
|
7
|
+
/** Admit due action deliveries without making the daemon's serial tick await subprocesses. */
|
|
8
|
+
export declare class HumanDeliveryLaneReconciler {
|
|
9
|
+
run(now: number, ctx: HumanDeliveryLaneContext): void;
|
|
10
|
+
}
|