@north-light/crouter 0.3.248 → 0.3.250
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/README.md +1 -1
- package/dist/api/__tests__/integration/client.test.js +28 -7
- package/dist/api/client.d.ts +1 -1
- package/dist/api/client.js +2 -2
- package/dist/api/dto/lifecycle.d.ts +3 -0
- package/dist/api/dto/messages.d.ts +4 -0
- package/dist/api/dto/nodes.d.ts +4 -0
- package/dist/api/dto/reports.d.ts +7 -2
- package/dist/builtin-memory/00-runtime-base/01-escalation.md +1 -1
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +3 -0
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +0 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +4 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +4 -3
- package/dist/clients/attach/session/chrome-refresh.d.ts +3 -0
- package/dist/clients/attach/session/chrome-refresh.js +1 -0
- package/dist/clients/attach/viewer.js +532 -532
- package/dist/clients/inbox/__tests__/integration/stale-row-activation.test.d.ts +1 -0
- package/dist/clients/inbox/__tests__/integration/stale-row-activation.test.js +53 -0
- package/dist/clients/inbox/controller.js +32 -5
- package/dist/clients/inbox/tui/ansi.d.ts +3 -1
- package/dist/clients/inbox/tui/ansi.js +14 -10
- package/dist/commands/__tests__/api-client-cold-start-diagnostic.test.js +7 -7
- package/dist/commands/api-client.d.ts +2 -2
- package/dist/commands/api-client.js +5 -5
- package/dist/commands/canvas-browse.js +1 -1
- package/dist/commands/node/create.js +3 -1
- package/dist/commands/node/inspect.js +1 -0
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node/message.js +6 -4
- package/dist/commands/node/wait.js +1 -1
- package/dist/commands/push.js +36 -37
- package/dist/commands/sys/daemon.js +21 -8
- package/dist/commands/sys/setup-core.js +2 -2
- package/dist/core/__tests__/broker-stream-watchdog-floor.test.d.ts +1 -0
- package/dist/core/__tests__/broker-stream-watchdog-floor.test.js +84 -0
- package/dist/core/__tests__/broker-turn-admission.test.d.ts +1 -0
- package/dist/core/__tests__/broker-turn-admission.test.js +44 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +5 -1
- package/dist/core/__tests__/child-death-wake.test.js +11 -7
- package/dist/core/__tests__/collectible.test.d.ts +1 -0
- package/dist/core/__tests__/collectible.test.js +88 -0
- package/dist/core/__tests__/context-intro.test.js +4 -41
- package/dist/core/__tests__/daemon-boot.test.js +30 -14
- package/dist/core/__tests__/daemon-wedge.test.js +25 -19
- package/dist/core/__tests__/fixtures/fake-engine.js +11 -0
- package/dist/core/__tests__/human-deliver.test.js +5 -5
- package/dist/core/__tests__/integration/deferred-no-wake.test.js +65 -4
- package/dist/core/__tests__/integration/human-deliver-e2e.test.js +3 -3
- package/dist/core/__tests__/integration/revive.test.js +48 -0
- package/dist/core/__tests__/integration/spawn-root.test.js +36 -1
- package/dist/core/__tests__/integration/worktree-land.test.js +82 -3
- package/dist/core/__tests__/integration/worktree-reap.test.js +44 -6
- package/dist/core/__tests__/migration.test.js +30 -0
- package/dist/core/__tests__/passive-subscription.test.js +39 -1
- package/dist/core/__tests__/prune.test.js +12 -3
- package/dist/core/__tests__/relaunch-root.test.js +1 -1
- package/dist/core/__tests__/respawn-throttle.test.js +16 -146
- package/dist/core/__tests__/seam/broker-crash-teardown.test.js +23 -11
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +81 -2
- package/dist/core/__tests__/seam/dormancy-release.test.js +1 -1
- package/dist/core/__tests__/seam/self-close-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/seam/self-close-teardown.test.js +49 -0
- package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +18 -10
- package/dist/core/canvas/canvas.d.ts +9 -15
- package/dist/core/canvas/canvas.js +41 -47
- package/dist/core/canvas/crons.d.ts +5 -8
- package/dist/core/canvas/crons.js +5 -8
- package/dist/core/canvas/history.d.ts +5 -0
- package/dist/core/canvas/history.js +12 -1
- package/dist/core/canvas/migrations.js +58 -1
- package/dist/core/canvas/pid.d.ts +1 -1
- package/dist/core/canvas/pid.js +5 -2
- package/dist/core/canvas/types.d.ts +4 -0
- package/dist/core/feed/feed.d.ts +4 -0
- package/dist/core/feed/feed.js +13 -8
- package/dist/core/feed/inbox.d.ts +2 -1
- package/dist/core/feed/inbox.js +22 -3
- package/dist/core/git.d.ts +3 -0
- package/dist/core/git.js +3 -1
- package/dist/core/human/feedback-companion.js +1 -0
- package/dist/core/preview-registry.js +0 -1
- package/dist/core/review/companion.js +1 -0
- package/dist/core/review/realize.js +1 -0
- package/dist/core/runtime/broker/auth-reload.d.ts +2 -0
- package/dist/core/runtime/broker/auth-reload.js +9 -3
- package/dist/core/runtime/broker/engine-drive.d.ts +2 -0
- package/dist/core/runtime/broker/engine-drive.js +28 -23
- package/dist/core/runtime/broker/event-projection.d.ts +4 -0
- package/dist/core/runtime/broker/event-projection.js +43 -23
- package/dist/core/runtime/broker/fault-retry.d.ts +9 -0
- package/dist/core/runtime/broker/fault-retry.js +94 -8
- package/dist/core/runtime/broker/frame-dispatch.d.ts +2 -0
- package/dist/core/runtime/broker/frame-dispatch.js +4 -1
- package/dist/core/runtime/broker/inbox.js +4 -0
- package/dist/core/runtime/broker/rebind.js +18 -0
- package/dist/core/runtime/broker/turn-admission.d.ts +11 -0
- package/dist/core/runtime/broker/turn-admission.js +31 -0
- package/dist/core/runtime/broker/turn-ignition.d.ts +2 -0
- package/dist/core/runtime/broker/turn-ignition.js +25 -15
- package/dist/core/runtime/broker.js +21 -2
- package/dist/core/runtime/close.d.ts +11 -9
- package/dist/core/runtime/close.js +23 -19
- package/dist/core/runtime/fault.d.ts +21 -0
- package/dist/core/runtime/fault.js +129 -19
- package/dist/core/runtime/fleet.d.ts +0 -6
- package/dist/core/runtime/host.d.ts +7 -1
- package/dist/core/runtime/host.js +20 -2
- package/dist/core/runtime/nodes.d.ts +5 -1
- package/dist/core/runtime/nodes.js +24 -1
- package/dist/core/runtime/placement.d.ts +6 -6
- package/dist/core/runtime/placement.js +15 -8
- package/dist/core/runtime/reset.js +2 -2
- package/dist/core/runtime/revive.js +40 -4
- package/dist/core/runtime/spawn.d.ts +3 -0
- package/dist/core/runtime/spawn.js +1 -0
- package/dist/core/runtime/warm-pool.d.ts +4 -0
- package/dist/core/runtime/warm-pool.js +2 -0
- package/dist/core/worktree.d.ts +4 -3
- package/dist/core/worktree.js +73 -27
- package/dist/daemon/__tests__/integration/api-startup-readiness.test.d.ts +1 -0
- package/dist/daemon/__tests__/integration/api-startup-readiness.test.js +81 -0
- package/dist/daemon/api/__tests__/reopen-delivery.test.js +38 -0
- package/dist/daemon/api/__tests__/seam/api-server.test.js +1 -0
- package/dist/daemon/api/__tests__/seam/leaf-api-parity.test.js +13 -2
- package/dist/daemon/api/handlers/bash-jobs.js +2 -0
- package/dist/daemon/api/handlers/broker-ops.js +23 -2
- package/dist/daemon/api/handlers/human.js +1 -1
- package/dist/daemon/api/handlers/messages.js +11 -2
- package/dist/daemon/api/handlers/nodes.js +13 -1
- package/dist/daemon/api/handlers/reports.js +14 -6
- package/dist/daemon/api/map.js +1 -0
- package/dist/daemon/api/server.d.ts +9 -12
- package/dist/daemon/api/server.js +30 -19
- package/dist/daemon/companion-retire.js +3 -9
- package/dist/daemon/cron/sinks.js +7 -2
- package/dist/daemon/crtrd.js +4 -1
- package/dist/daemon/fleet.d.ts +2 -16
- package/dist/daemon/fleet.js +63 -150
- package/dist/daemon/human/finish.js +2 -0
- package/dist/daemon/manage.d.ts +11 -0
- package/dist/daemon/manage.js +32 -3
- package/dist/daemon/messaging/node-message.js +7 -1
- package/dist/daemon/reconcilers/bash-deadline.js +2 -0
- package/dist/daemon/reconcilers/broker-supervision.d.ts +1 -0
- package/dist/daemon/reconcilers/broker-supervision.js +22 -9
- package/dist/daemon/reconcilers/live-obligation.d.ts +16 -6
- package/dist/daemon/reconcilers/live-obligation.js +18 -6
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.d.ts +15 -4
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.js +34 -3
- package/dist/daemon/reconcilers/node-lifecycle/terminating.d.ts +4 -1
- package/dist/daemon/reconcilers/node-lifecycle/terminating.js +11 -2
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +14 -7
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +2 -2
- package/dist/daemon/reconcilers/storage-maintenance.js +3 -3
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
- package/dist/pi-extensions/canvas-context-intro.d.ts +3 -34
- package/dist/pi-extensions/canvas-context-intro.js +4 -76
- package/dist/pi-extensions/canvas-review-boundary.d.ts +3 -24
- package/dist/pi-extensions/canvas-review-boundary.js +4 -29
- package/dist/pi-extensions/canvas-stophook.js +1 -6
- package/dist/shared/birth-announcement.d.ts +12 -0
- package/dist/shared/birth-announcement.js +25 -0
- package/dist/shared/generated-context.d.ts +8 -4
- package/dist/shared/generated-context.js +15 -9
- package/package.json +4 -4
- package/runtime.lock.json +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +0 -254
- package/dist/clients/inbox/tui/clipboard.d.ts +0 -2
- package/dist/clients/inbox/tui/clipboard.js +0 -26
- package/dist/pi-extensions/truncate.d.ts +0 -5
- package/dist/pi-extensions/truncate.js +0 -14
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Broker-originated canvas operations. The broker sends Pi lifecycle facts;
|
|
2
2
|
// crtrd validates and commits their durable effects while the handler stays
|
|
3
3
|
// strictly canvas-write-only — no broker launch, kill, or view.sock round trip.
|
|
4
|
-
import { canonicalSessionFile, findNodeBySessionFile, getNode, isWarmSpare, listNodes, recordPid, setPresence, subscriptionsOf, updateNode } from '../../../core/canvas/canvas.js';
|
|
4
|
+
import { canonicalSessionFile, findNodeBySessionFile, getNode, isWarmSpare, listNodes, recordPid, recordSettledTurn, setPresence, subscriptionsOf, updateNode } from '../../../core/canvas/canvas.js';
|
|
5
|
+
import { isPidAlive } from '../../../core/canvas/pid.js';
|
|
5
6
|
import { editorLabel } from '../../../core/canvas/labels.js';
|
|
6
7
|
import { assembleNodeSubject } from '../../../core/substrate/subject.js';
|
|
7
8
|
import { withCanvasWrite } from '../../../core/canvas/db.js';
|
|
@@ -13,7 +14,7 @@ import { focusOf, tearDownNode } from '../../../core/runtime/placement.js';
|
|
|
13
14
|
import { hasAttachedBrokerViewers } from '../../../core/runtime/broker/client-registry.js';
|
|
14
15
|
import { handleNewSession } from '../../../core/runtime/reset.js';
|
|
15
16
|
import { evaluateStop } from '../../../core/runtime/stop-guard.js';
|
|
16
|
-
import { clearFault } from '../../../core/runtime/fault.js';
|
|
17
|
+
import { clearFault, recordFault } from '../../../core/runtime/fault.js';
|
|
17
18
|
import { updateModelRecipe } from '../../../core/runtime/model-swap.js';
|
|
18
19
|
import { completeCompanionRequestedSubmit } from '../../review/finish.js';
|
|
19
20
|
import { consumePendingPark } from '../../park-pending.js';
|
|
@@ -187,6 +188,25 @@ function handleSessionBound(ctx) {
|
|
|
187
188
|
const sessionFile = request.sessionFile === null ? null : canonicalSessionFile(request.sessionFile);
|
|
188
189
|
const result = withCanvasWrite(() => {
|
|
189
190
|
const before = requireNode(nodeId);
|
|
191
|
+
// A transcript is one broker's identity. Check ownership before every
|
|
192
|
+
// session-bound mutation: a live owner wins, and the claimant must stop
|
|
193
|
+
// rather than overwrite its own process-scoped identity onto that file.
|
|
194
|
+
const owner = sessionFile === null ? null : findNodeBySessionFile(sessionFile);
|
|
195
|
+
if (owner !== null && owner.node_id !== nodeId) {
|
|
196
|
+
if (isPidAlive(owner.pi_pid)) {
|
|
197
|
+
recordFault(nodeId, {
|
|
198
|
+
link: 'daemon→node',
|
|
199
|
+
op: 'session-file-ownership',
|
|
200
|
+
kind: 'protocol',
|
|
201
|
+
retry: { disposition: 'fatal' },
|
|
202
|
+
message: `refusing to bind ${sessionFile}: it is owned by live node ${owner.node_id}`,
|
|
203
|
+
});
|
|
204
|
+
return { action: 'shutdown' };
|
|
205
|
+
}
|
|
206
|
+
// The prior owner is dead, so transfer the durable coordinate before the
|
|
207
|
+
// claimant writes it; the UNIQUE projection and both meta files agree.
|
|
208
|
+
updateNode(owner.node_id, { pi_session_id: null, pi_session_file: null });
|
|
209
|
+
}
|
|
190
210
|
// The broker reports the markers present in Pi's current branch; crtrd owns
|
|
191
211
|
// the review binding and decides whether that branch belongs to this node.
|
|
192
212
|
const reviewId = before.review_binding?.review_id;
|
|
@@ -247,6 +267,7 @@ function settleBroker(nodeId, request) {
|
|
|
247
267
|
return { action: 'stay_dormant' };
|
|
248
268
|
throw notFound(`unknown node: ${nodeId}`, { received: nodeId });
|
|
249
269
|
}
|
|
270
|
+
recordSettledTurn(nodeId, new Date().toISOString());
|
|
250
271
|
if (request.stopReason === 'quit') {
|
|
251
272
|
if ((node.status === 'active' || node.status === 'idle') && node.intent === null) {
|
|
252
273
|
transition(nodeId, 'finish');
|
|
@@ -37,7 +37,7 @@ function handleCreateBridge(ctx) {
|
|
|
37
37
|
throw new ApiError(400, 'invalid_request', 'bridge `parent` must be a node id or null');
|
|
38
38
|
}
|
|
39
39
|
try {
|
|
40
|
-
const node = spawnNode({ kind: 'human', parent, cwd, name, lifecycle: 'terminal' });
|
|
40
|
+
const node = spawnNode({ kind: 'human', parent, creator: parent, cwd, name, lifecycle: 'terminal' });
|
|
41
41
|
return { status: 200, body: { node_id: node.node_id, name: node.name } };
|
|
42
42
|
}
|
|
43
43
|
catch (error) {
|
|
@@ -8,6 +8,7 @@ import { isBrokerLive } from '../../../core/runtime/model-swap.js';
|
|
|
8
8
|
import { deliverLive, interruptLive } from '../../../core/runtime/interactive-deliver.js';
|
|
9
9
|
import { isProfilePaused } from '../../../core/profiles/manifest.js';
|
|
10
10
|
import { getNode, withFreshTerminalGuard } from '../../../core/canvas/canvas.js';
|
|
11
|
+
import { cancelCronsOnWake } from '../../../core/canvas/crons.js';
|
|
11
12
|
import { assertNotFinalized, assertFinalizedForReopen, commitReopen, commitReopenResident, } from '../../../core/runtime/reopen.js';
|
|
12
13
|
import { writeOutputSchema } from '../../../core/runtime/structured-output.js';
|
|
13
14
|
import { appendSituationalContext, formatSituationalProse } from '../../../core/runtime/situational-context.js';
|
|
@@ -249,6 +250,7 @@ async function handleMessage(ctx) {
|
|
|
249
250
|
if (hasSidecar)
|
|
250
251
|
appendSituationalContext(id, sidecar);
|
|
251
252
|
await deliverLive(id, req.body, [...(hasSidecar ? [sidecar] : []), ...oneShotCards]);
|
|
253
|
+
cancelCronsOnWake(id);
|
|
252
254
|
const body = {
|
|
253
255
|
node_id: id,
|
|
254
256
|
delivered: true,
|
|
@@ -323,11 +325,17 @@ async function handleMessage(ctx) {
|
|
|
323
325
|
return deliverImmediate();
|
|
324
326
|
};
|
|
325
327
|
const entry = deliver();
|
|
328
|
+
// Deferred mail normally waits for a natural cycle, but a frozen row already
|
|
329
|
+
// owes a thaw; consume its deadline when that durable future wake arrives.
|
|
330
|
+
if (tier !== 'deferred' || meta.frozen_at !== null)
|
|
331
|
+
cancelCronsOnWake(id);
|
|
326
332
|
// A wake-capable tier revives a dormant target so its inbox-watcher delivers
|
|
327
|
-
// this; deferred never wakes — it rides the target's next natural cycle.
|
|
333
|
+
// this; deferred never wakes — it rides the target's next natural cycle. The
|
|
334
|
+
// APPENDED entry's tier decides, because a terminal target's deferred was
|
|
335
|
+
// already raised to a wake-capable tier by the append.
|
|
328
336
|
let revived = false;
|
|
329
337
|
let frozen = false;
|
|
330
|
-
if (tier !== 'deferred' && !paused && !isBrokerLive(meta)) {
|
|
338
|
+
if (entry.tier !== 'deferred' && !paused && !isBrokerLive(meta)) {
|
|
331
339
|
try {
|
|
332
340
|
// The entry is already durable, so a wake denied for capacity is only
|
|
333
341
|
// delayed: the row freezes and the tick relaunches it — at which point
|
|
@@ -346,6 +354,7 @@ async function handleMessage(ctx) {
|
|
|
346
354
|
revived,
|
|
347
355
|
...(frozen ? { not_revived_reason: 'capacity_frozen' } : {}),
|
|
348
356
|
delivered_at: entry.ts,
|
|
357
|
+
...(entry.tier === tier ? {} : { delivered_tier: entry.tier }),
|
|
349
358
|
...(req.delivery === 'interactive' ? { delivered_via: 'inbox' } : {}),
|
|
350
359
|
};
|
|
351
360
|
return { status: 201, body };
|
|
@@ -102,6 +102,9 @@ function parseCreateBody(body) {
|
|
|
102
102
|
if (b['description'] !== undefined && (typeof b['description'] !== 'string' || b['description'].trim() === '')) {
|
|
103
103
|
throw usage('description must be a non-empty string when provided');
|
|
104
104
|
}
|
|
105
|
+
if (b['creator'] !== undefined && (typeof b['creator'] !== 'string' || b['creator'].trim() === '')) {
|
|
106
|
+
throw usage('creator must be a non-empty node id when provided');
|
|
107
|
+
}
|
|
105
108
|
const rootLifecycle = b['root_lifecycle'];
|
|
106
109
|
if (rootLifecycle !== undefined &&
|
|
107
110
|
rootLifecycle !== 'resident' &&
|
|
@@ -125,6 +128,8 @@ function buildRecipe(req, parent, cwd) {
|
|
|
125
128
|
recipe.description = req.description;
|
|
126
129
|
if (parent !== undefined)
|
|
127
130
|
recipe.parent = parent;
|
|
131
|
+
if (req.creator !== undefined)
|
|
132
|
+
recipe.creator = req.creator;
|
|
128
133
|
if (req.root === true)
|
|
129
134
|
recipe.root = true;
|
|
130
135
|
if (req.root_lifecycle !== undefined) {
|
|
@@ -174,6 +179,7 @@ function warmRequestFor(req, cwd, profileId) {
|
|
|
174
179
|
model: req.model ?? null,
|
|
175
180
|
situationalContext: req.situational_context ?? null,
|
|
176
181
|
name: req.name ?? null,
|
|
182
|
+
creator: req.creator ?? null,
|
|
177
183
|
};
|
|
178
184
|
}
|
|
179
185
|
export { resolveCreateKind } from '../../../core/runtime/launch-target.js';
|
|
@@ -508,7 +514,13 @@ function handleClose(ctx) {
|
|
|
508
514
|
// `finish` selects the root disposition: finalize to `done` (browse `x`) vs the
|
|
509
515
|
// default cancel.
|
|
510
516
|
const body = ctx.body;
|
|
511
|
-
|
|
517
|
+
if (body?.caller_pid !== undefined && (!Number.isInteger(body.caller_pid) || body.caller_pid <= 0)) {
|
|
518
|
+
throw usage('caller_pid must be a positive integer when provided');
|
|
519
|
+
}
|
|
520
|
+
const result = closeNode(id, {
|
|
521
|
+
rootEvent: body?.finish === true ? 'finish' : 'cancel',
|
|
522
|
+
callerPid: body?.caller_pid,
|
|
523
|
+
});
|
|
512
524
|
return { status: 200, body: toCloseResultDTO(result) };
|
|
513
525
|
}
|
|
514
526
|
async function handleRecycle(ctx) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Report handlers (spec §6.4, §7.4). `POST /nodes/{id}/reports` is the single
|
|
2
|
-
// push route —
|
|
3
|
-
//
|
|
2
|
+
// push route — `tier` is the report kind and update-only `delivery_tier` sets
|
|
3
|
+
// subscriber delivery urgency; `final` drives the server-side lifecycle
|
|
4
|
+
// transition inside feed `push()` (the sanctioned path). `GET
|
|
4
5
|
// /nodes/{id}/reports` lists the node's stored report files.
|
|
5
6
|
//
|
|
6
7
|
// `{id}` is the REPORTING node (client-side = CRTR_NODE_ID).
|
|
@@ -24,14 +25,21 @@ function parsePushBody(body) {
|
|
|
24
25
|
throw usage('a push report requires a JSON body { tier, body }');
|
|
25
26
|
}
|
|
26
27
|
const tier = body.tier;
|
|
28
|
+
const deliveryTier = body.delivery_tier;
|
|
27
29
|
const text = body.body;
|
|
28
30
|
if (tier !== 'update' && tier !== 'urgent' && tier !== 'final') {
|
|
29
31
|
throw usage(`invalid report tier: ${String(tier)} (expected update|urgent|final)`);
|
|
30
32
|
}
|
|
33
|
+
if (deliveryTier !== undefined && deliveryTier !== 'deferred' && deliveryTier !== 'normal' && deliveryTier !== 'urgent') {
|
|
34
|
+
throw usage(`invalid delivery_tier: ${String(deliveryTier)} (expected deferred|normal|urgent)`);
|
|
35
|
+
}
|
|
36
|
+
if (tier === 'final' && deliveryTier !== undefined) {
|
|
37
|
+
throw usage('delivery_tier is only valid for update reports');
|
|
38
|
+
}
|
|
31
39
|
if (typeof text !== 'string' || text.trim() === '') {
|
|
32
40
|
throw usage('a push report requires a non-empty body');
|
|
33
41
|
}
|
|
34
|
-
return { tier, body: text };
|
|
42
|
+
return { tier, ...(deliveryTier === undefined ? {} : { delivery_tier: deliveryTier }), body: text };
|
|
35
43
|
}
|
|
36
44
|
async function handlePush(ctx) {
|
|
37
45
|
const id = ctx.params['id'];
|
|
@@ -68,7 +76,7 @@ async function handlePush(ctx) {
|
|
|
68
76
|
}
|
|
69
77
|
autoDroppedPath = dropped.worktree_path;
|
|
70
78
|
}
|
|
71
|
-
const result = await pushOrRethrow(id, req
|
|
79
|
+
const result = await pushOrRethrow(id, req);
|
|
72
80
|
const transitioned = req.tier === 'final' && before !== undefined
|
|
73
81
|
? { from: before, to: getNode(id)?.status ?? 'done' }
|
|
74
82
|
: undefined;
|
|
@@ -83,9 +91,9 @@ async function handlePush(ctx) {
|
|
|
83
91
|
* survives (a bare 409 would otherwise collapse to the generic `conflict`
|
|
84
92
|
* code). `unknown_node` is unreachable here (`requireNode` guards above), so
|
|
85
93
|
* it falls through as the generic 404. */
|
|
86
|
-
async function pushOrRethrow(id,
|
|
94
|
+
async function pushOrRethrow(id, req) {
|
|
87
95
|
try {
|
|
88
|
-
return await push(id, { kind: tier, body });
|
|
96
|
+
return await push(id, { kind: req.tier, body: req.body, ...(req.delivery_tier === undefined ? {} : { deliveryTier: req.delivery_tier }) });
|
|
89
97
|
}
|
|
90
98
|
catch (err) {
|
|
91
99
|
if (err instanceof FinalizationError && err.code === 'already_finalized') {
|
package/dist/daemon/api/map.js
CHANGED
|
@@ -119,6 +119,7 @@ export function toNodeDetailDTO(meta, edges) {
|
|
|
119
119
|
pane: meta.pane ?? null,
|
|
120
120
|
final_report: meta.final_report ?? null,
|
|
121
121
|
finalized_at: meta.finalized_at ?? null,
|
|
122
|
+
creator: meta.creator ?? null,
|
|
122
123
|
edges: toNodeEdgesDTO(meta, edges),
|
|
123
124
|
paths: toNodePathsDTO(meta.node_id),
|
|
124
125
|
};
|
|
@@ -4,20 +4,17 @@ export interface CreateApiServerOptions {
|
|
|
4
4
|
tcp?: string;
|
|
5
5
|
}
|
|
6
6
|
export interface ApiServer {
|
|
7
|
+
/** Settles only after the mandatory Unix socket binds; rejects on its bind failure. */
|
|
8
|
+
ready: Promise<void>;
|
|
7
9
|
/** Tear down both listeners and unlink the socket file. Double-close safe. */
|
|
8
10
|
close(): Promise<void>;
|
|
9
11
|
}
|
|
10
|
-
/** Create and start the crtrd API server
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
* bind failure or a post-listen accept error is emitted and swallowed — it must
|
|
14
|
-
* NEVER become an unhandled 'error' that crashes the daemon.
|
|
12
|
+
/** Create and start the crtrd API server. Returns immediately with a `close()`
|
|
13
|
+
* handle and a `ready` promise for the mandatory Unix socket bind. TCP remains
|
|
14
|
+
* opt-in, and its bind or post-listen accept errors are emitted but non-fatal.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* functions (the standard way to bind one logical API to two transports). The
|
|
21
|
-
* Router and attach bridge are process-wide singletons, so both servers route
|
|
22
|
-
* into identical state. */
|
|
16
|
+
* A Node `net.Server`/`http.Server` binds exactly ONE handle, so the two
|
|
17
|
+
* listeners are separate `http.Server` instances sharing request and upgrade
|
|
18
|
+
* handlers. The Router and attach bridge are process-wide singletons, so both
|
|
19
|
+
* servers route into identical state. */
|
|
23
20
|
export declare function createApiServer(opts?: CreateApiServerOptions): ApiServer;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// server.ts — the crtrd HTTP+WS API server (spec §4, esp. §4.1/§5). A-9.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// Two `http.Server` listeners share one logical API (a Node server can bind one
|
|
4
|
+
// handle only):
|
|
4
5
|
// - the unix socket at `apiSocketPath()` — ALWAYS bound, owner-only (mode
|
|
5
6
|
// 0600 = the fs-perms-are-auth model);
|
|
6
7
|
// - an opt-in TCP listener, bound only when `--tcp <host:port>` / `CRTRD_TCP`
|
|
@@ -121,19 +122,14 @@ function closeServer(server) {
|
|
|
121
122
|
server.close(() => resolve());
|
|
122
123
|
});
|
|
123
124
|
}
|
|
124
|
-
/** Create and start the crtrd API server
|
|
125
|
-
* `
|
|
126
|
-
*
|
|
127
|
-
* bind failure or a post-listen accept error is emitted and swallowed — it must
|
|
128
|
-
* NEVER become an unhandled 'error' that crashes the daemon.
|
|
125
|
+
/** Create and start the crtrd API server. Returns immediately with a `close()`
|
|
126
|
+
* handle and a `ready` promise for the mandatory Unix socket bind. TCP remains
|
|
127
|
+
* opt-in, and its bind or post-listen accept errors are emitted but non-fatal.
|
|
129
128
|
*
|
|
130
|
-
*
|
|
131
|
-
* `
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* functions (the standard way to bind one logical API to two transports). The
|
|
135
|
-
* Router and attach bridge are process-wide singletons, so both servers route
|
|
136
|
-
* into identical state. */
|
|
129
|
+
* A Node `net.Server`/`http.Server` binds exactly ONE handle, so the two
|
|
130
|
+
* listeners are separate `http.Server` instances sharing request and upgrade
|
|
131
|
+
* handlers. The Router and attach bridge are process-wide singletons, so both
|
|
132
|
+
* servers route into identical state. */
|
|
137
133
|
export function createApiServer(opts = {}) {
|
|
138
134
|
const router = buildRouter();
|
|
139
135
|
const sockPath = apiSocketPath();
|
|
@@ -199,17 +195,32 @@ export function createApiServer(opts = {}) {
|
|
|
199
195
|
}
|
|
200
196
|
};
|
|
201
197
|
const bindErrorHandler = (server) => {
|
|
202
|
-
//
|
|
203
|
-
//
|
|
198
|
+
// TCP is opt-in, so neither its bind failure nor a later accept error can
|
|
199
|
+
// make the mandatory Unix API unavailable.
|
|
204
200
|
server.on('error', (err) => {
|
|
205
201
|
emitEvent({ event: 'api.server.failed', level: 'error', error: err });
|
|
206
202
|
});
|
|
207
203
|
};
|
|
208
|
-
// Unix socket — ALWAYS bound.
|
|
209
|
-
//
|
|
204
|
+
// Unix socket — ALWAYS bound. Its bind result is startup readiness; after
|
|
205
|
+
// that point, accept errors remain observable but do not tear the daemon down.
|
|
210
206
|
const socketServer = createServer(onRequest);
|
|
211
207
|
socketServer.on('upgrade', onUpgrade);
|
|
212
|
-
|
|
208
|
+
let socketBound = false;
|
|
209
|
+
let resolveReady;
|
|
210
|
+
let rejectReady;
|
|
211
|
+
const ready = new Promise((resolve, reject) => {
|
|
212
|
+
resolveReady = resolve;
|
|
213
|
+
rejectReady = reject;
|
|
214
|
+
});
|
|
215
|
+
socketServer.once('listening', () => {
|
|
216
|
+
socketBound = true;
|
|
217
|
+
resolveReady();
|
|
218
|
+
});
|
|
219
|
+
socketServer.on('error', (err) => {
|
|
220
|
+
emitEvent({ event: 'api.server.failed', level: 'error', error: err });
|
|
221
|
+
if (!socketBound)
|
|
222
|
+
rejectReady(err);
|
|
223
|
+
});
|
|
213
224
|
try {
|
|
214
225
|
if (existsSync(sockPath))
|
|
215
226
|
unlinkSync(sockPath);
|
|
@@ -265,5 +276,5 @@ export function createApiServer(opts = {}) {
|
|
|
265
276
|
/* best-effort cleanup */
|
|
266
277
|
}
|
|
267
278
|
};
|
|
268
|
-
return { close };
|
|
279
|
+
return { ready, close };
|
|
269
280
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { getNode } from '../core/canvas/canvas.js';
|
|
6
6
|
import { FinalizationError, pushFinal } from '../core/feed/feed.js';
|
|
7
7
|
import { transition } from '../core/runtime/lifecycle.js';
|
|
8
|
-
import {
|
|
8
|
+
import { requestBrokerTeardown } from '../core/runtime/host.js';
|
|
9
9
|
/** Retire a companion only through the terminal transition legal for its current status. */
|
|
10
10
|
export async function retireCompanion(nodeId, outcome) {
|
|
11
11
|
const node = getNode(nodeId);
|
|
@@ -23,18 +23,12 @@ export async function retireCompanion(nodeId, outcome) {
|
|
|
23
23
|
throw error;
|
|
24
24
|
result = 'already';
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
headlessBrokerHost.teardown(nodeId);
|
|
28
|
-
}
|
|
29
|
-
catch { /* the canonical final is authoritative */ }
|
|
26
|
+
requestBrokerTeardown(nodeId);
|
|
30
27
|
return result;
|
|
31
28
|
}
|
|
32
29
|
transition(nodeId, 'cancel', { reason: 'retired' });
|
|
33
30
|
if (node.status === 'active' || node.status === 'idle') {
|
|
34
|
-
|
|
35
|
-
headlessBrokerHost.teardown(nodeId);
|
|
36
|
-
}
|
|
37
|
-
catch { /* the terminal transition is authoritative */ }
|
|
31
|
+
requestBrokerTeardown(nodeId);
|
|
38
32
|
}
|
|
39
33
|
return 'retired';
|
|
40
34
|
}
|
|
@@ -5,6 +5,7 @@ import { ticketDir } from '../../core/human/root.js';
|
|
|
5
5
|
import { preparePage, publishPage } from '../../core/human/tickets.js';
|
|
6
6
|
import { resolvePageComponents } from '../../core/config.js';
|
|
7
7
|
import { getNode } from '../../core/canvas/canvas.js';
|
|
8
|
+
import { cancelCronsOnWake } from '../../core/canvas/crons.js';
|
|
8
9
|
import { appendInbox } from '../../core/feed/inbox.js';
|
|
9
10
|
import { hasNoNaturalCycle } from '../../core/runtime/revive-all.js';
|
|
10
11
|
import { isParked } from '../../core/runtime/lifecycle.js';
|
|
@@ -23,15 +24,19 @@ function deliverNodeSink(c, target, stdout) {
|
|
|
23
24
|
throw new Error(`node sink ${target} has no natural cycle ahead (done or finalized)`);
|
|
24
25
|
}
|
|
25
26
|
const from = c.created_by != null && getNode(c.created_by) !== null ? c.created_by : null;
|
|
26
|
-
appendInbox(target, {
|
|
27
|
+
const entry = appendInbox(target, {
|
|
27
28
|
from,
|
|
28
29
|
tier: c.tier,
|
|
29
30
|
kind: 'message',
|
|
30
31
|
label: `⏰ cron ${c.name}`,
|
|
31
32
|
data: { body: stdout },
|
|
32
33
|
});
|
|
34
|
+
if (c.tier !== 'deferred' || meta.frozen_at !== null)
|
|
35
|
+
cancelCronsOnWake(target);
|
|
36
|
+
// The APPENDED tier decides the wake, not the cron's requested one: a deferred
|
|
37
|
+
// cron aimed at a terminal node was raised out of `deferred` by the append.
|
|
33
38
|
// The durable append succeeds even if its best-effort wake fails.
|
|
34
|
-
if (
|
|
39
|
+
if (entry.tier !== 'deferred' && !isBrokerLive(meta)) {
|
|
35
40
|
try {
|
|
36
41
|
reviveNode(target, { resume: true, capacity: 'freeze' });
|
|
37
42
|
}
|
package/dist/daemon/crtrd.js
CHANGED
|
@@ -668,8 +668,11 @@ export async function runDaemon(opts = {}) {
|
|
|
668
668
|
// no adoption across epochs, ever.
|
|
669
669
|
await operationIdContext.fresh(() => reapPriorEpochBrokers(epoch));
|
|
670
670
|
writePidfile();
|
|
671
|
-
//
|
|
671
|
+
// The Unix API socket is the daemon's readiness boundary. A rejected bind
|
|
672
|
+
// enters the existing startup catch, which closes resources and releases
|
|
673
|
+
// this invocation's pidfile mirror and ownership claim before propagating.
|
|
672
674
|
apiServer = createApiServer({ tcp: opts.tcp });
|
|
675
|
+
await apiServer.ready;
|
|
673
676
|
// Pre-warm the node pool for the most recent project recipes, so the first
|
|
674
677
|
// create after a boot is warm too. Fire-and-forget (the mints run off the
|
|
675
678
|
// work lane), so it never delays supervision. Boot-time dead rows are the
|
package/dist/daemon/fleet.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type NodeMeta } from '../core/canvas/index.js';
|
|
2
2
|
import { type FleetEntry, type FleetExitStatus, type FleetRegistry } from '../core/runtime/fleet.js';
|
|
3
3
|
import type { HostHandle } from '../core/runtime/host.js';
|
|
4
|
-
/** Surface a vehicle that never booted
|
|
4
|
+
/** Surface a vehicle that never booted.
|
|
5
5
|
*
|
|
6
6
|
* A pi that dies before its first session_start (so `pi_session_id` was never
|
|
7
7
|
* recorded) is invisible to its parent — the spawn returned optimistically.
|
|
@@ -43,16 +43,6 @@ export type DeadNodeRow = Pick<NodeMeta, 'status' | 'intent' | 'pi_session_id' |
|
|
|
43
43
|
* would drive a turn if the engine came back. It selects row 4b and defaults
|
|
44
44
|
* to `true`, so an uninformed caller keeps the conservative respawn. */
|
|
45
45
|
export declare function classifyDeadNode(row: DeadNodeRow | null, busy: boolean, pendingWake?: boolean): DeadNodeAction;
|
|
46
|
-
/** An exit with uptime at or above this resets the consecutive-failure
|
|
47
|
-
* counter before classification; a shorter-lived exit increments it. */
|
|
48
|
-
export declare const HEALTHY_UPTIME_MS = 60000;
|
|
49
|
-
/** At this many consecutive short-lived exits the respawn loop terminalizes:
|
|
50
|
-
* transition('crash') + surfaceBootFailure + doctrine wake, counter cleared. */
|
|
51
|
-
export declare const MAX_RESPAWN_ATTEMPTS = 6;
|
|
52
|
-
/** Backoff before the next respawn given the consecutive-failure count.
|
|
53
|
-
* Zero failures → immediate (the sub-second yield respawn D-3 buys); the
|
|
54
|
-
* n-th delayed attempt waits min(5s·2^n, 10min). */
|
|
55
|
-
export declare function respawnBackoffMs(consecutiveFailures: number): number;
|
|
56
46
|
export interface DaemonFleetOpts {
|
|
57
47
|
/** This daemon's epoch id (randomBytes(8).toString('hex'), minted by
|
|
58
48
|
* runDaemon after the ownership claim). */
|
|
@@ -78,16 +68,12 @@ export declare class DaemonFleet implements FleetRegistry {
|
|
|
78
68
|
forget(nodeId: string): void;
|
|
79
69
|
onChildExit(nodeId: string, status: FleetExitStatus): void;
|
|
80
70
|
deliverExit(nodeId: string, status: FleetExitStatus): void;
|
|
81
|
-
/** The node-lifecycle tick's enactment entry
|
|
82
|
-
* enact immediately through the SAME policy table exit events use, so tick
|
|
83
|
-
* recovery and exit recovery can never diverge. */
|
|
71
|
+
/** The node-lifecycle tick's enactment entry. */
|
|
84
72
|
applyDeadRowPolicy(nodeId: string): Promise<void>;
|
|
85
73
|
/** A revive refusal before its launch try/catch leaves the row active but
|
|
86
74
|
* with no fleet entry. Convert that otherwise-silent strand into the same
|
|
87
75
|
* terminal outcome as a boot failure. The fresh read avoids duplicating
|
|
88
76
|
* reviveNode's launch-failure crash path. */
|
|
89
77
|
terminalizeIfStillNonTerminal(nodeId: string, event: string, error: unknown): Promise<boolean>;
|
|
90
|
-
/** Daemon shutdown: cancel every pending backoff respawn. Live processes
|
|
91
|
-
* are torn down by the daemon's fleet-teardown stage, not here. */
|
|
92
78
|
dispose(): void;
|
|
93
79
|
}
|