@north-light/crouter 0.3.282 → 0.3.283
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 -3
- package/dist/api/client.js +34 -0
- package/dist/api/dto/broker-ops.d.ts +11 -0
- package/dist/api/dto/broker.d.ts +3 -8
- package/dist/api/dto/common.d.ts +1 -1
- package/dist/api/dto/messages.d.ts +2 -2
- package/dist/api/dto/node-outcomes.d.ts +80 -0
- package/dist/api/dto/node-outcomes.js +2 -0
- package/dist/api/dto/nodes.d.ts +9 -0
- package/dist/api/dto/profiles.d.ts +5 -0
- package/dist/api/dto/reports.d.ts +45 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +7 -0
- package/dist/api/routes.js +7 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +24 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +31 -4
- package/dist/clients/attach/overlays/graph.d.ts +5 -0
- package/dist/clients/attach/overlays/graph.js +65 -47
- package/dist/clients/attach/overlays/pickers.js +8 -9
- package/dist/clients/attach/render/card-presentation.js +1 -1
- package/dist/clients/attach/viewer.js +1286 -1260
- package/dist/commands/api-client.js +22 -23
- package/dist/commands/node/create.js +38 -3
- package/dist/commands/node/inspect.js +3 -7
- package/dist/commands/node/message.js +3 -3
- package/dist/commands/node/outcome.d.ts +2 -0
- package/dist/commands/node/outcome.js +134 -0
- package/dist/commands/node.js +2 -1
- package/dist/commands/profile/pause.js +17 -14
- package/dist/commands/push.js +102 -1
- package/dist/commands/sys/__tests__/setup-core.test.js +4 -1
- package/dist/commands/sys/panels/broker-limits-panel.js +7 -1
- package/dist/commands/sys/panels/models-panel.js +2 -2
- package/dist/commands/sys/settings-shell.d.ts +1 -1
- package/dist/commands/sys/settings-shell.js +6 -0
- package/dist/commands/sys/settings.js +3 -3
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +2 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +42 -4
- package/dist/core/__tests__/helpers/harness.d.ts +1 -0
- package/dist/core/__tests__/helpers/harness.js +9 -0
- package/dist/core/__tests__/integration/profile-pause.test.d.ts +1 -0
- package/dist/core/__tests__/integration/profile-pause.test.js +115 -0
- package/dist/core/__tests__/node-outcome.test.d.ts +1 -0
- package/dist/core/__tests__/node-outcome.test.js +184 -0
- package/dist/core/__tests__/revive-parked-fresh.test.js +13 -20
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +6 -2
- package/dist/core/__tests__/seam/dormancy-release.test.js +3 -3
- package/dist/core/__tests__/seam/node-outcome-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/seam/node-outcome-delivery.test.js +153 -0
- package/dist/core/bash-jobs.d.ts +6 -0
- package/dist/core/bash-jobs.js +22 -0
- package/dist/core/canvas/canvas.d.ts +41 -3
- package/dist/core/canvas/canvas.js +153 -39
- package/dist/core/canvas/crons.js +2 -1
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/migrations.js +47 -0
- package/dist/core/canvas/node-outcome-deliveries.d.ts +80 -0
- package/dist/core/canvas/node-outcome-deliveries.js +163 -0
- package/dist/core/canvas/pid.d.ts +7 -0
- package/dist/core/canvas/pid.js +17 -0
- package/dist/core/canvas/render-source.js +2 -2
- package/dist/core/canvas/types.d.ts +42 -1
- package/dist/core/canvas/types.js +2 -1
- package/dist/core/config.d.ts +4 -1
- package/dist/core/config.js +25 -1
- package/dist/core/feed/feed.d.ts +8 -0
- package/dist/core/feed/feed.js +25 -4
- package/dist/core/feed/inbox.d.ts +5 -0
- package/dist/core/feed/inbox.js +13 -0
- package/dist/core/keybindings/catalog.d.ts +1 -1
- package/dist/core/keybindings/catalog.js +3 -0
- package/dist/core/profiles/manifest.d.ts +3 -1
- package/dist/core/profiles/manifest.js +6 -1
- package/dist/core/runtime/broker/daemon-ops.d.ts +3 -1
- package/dist/core/runtime/broker/daemon-ops.js +6 -0
- package/dist/core/runtime/broker/engine-drive.d.ts +2 -3
- package/dist/core/runtime/broker/engine-drive.js +74 -27
- package/dist/core/runtime/broker/frame-dispatch.js +1 -0
- package/dist/core/runtime/broker/read-ops.js +14 -0
- package/dist/core/runtime/broker/rebind.js +0 -7
- package/dist/core/runtime/broker-protocol.d.ts +1 -6
- package/dist/core/runtime/fleet.d.ts +7 -0
- package/dist/core/runtime/headless-pi.d.ts +13 -0
- package/dist/core/runtime/headless-pi.js +10 -2
- package/dist/core/runtime/lifecycle.d.ts +6 -1
- package/dist/core/runtime/lifecycle.js +50 -1
- package/dist/core/runtime/nodes.d.ts +5 -1
- package/dist/core/runtime/nodes.js +2 -1
- package/dist/core/runtime/outcome-document.d.ts +70 -0
- package/dist/core/runtime/outcome-document.js +97 -0
- package/dist/core/runtime/revive.js +16 -21
- package/dist/core/runtime/spawn.d.ts +5 -1
- package/dist/core/runtime/spawn.js +18 -7
- package/dist/core/runtime/stamp/channel.d.ts +3 -5
- package/dist/core/runtime/stamp/channel.js +3 -10
- package/dist/core/runtime/stop-guard.js +2 -2
- package/dist/core/runtime/structured-output.d.ts +24 -3
- package/dist/core/runtime/structured-output.js +42 -3
- package/dist/core/runtime/tmux-bindings.js +4 -0
- package/dist/core/runtime/warm-pool.d.ts +5 -2
- package/dist/core/runtime/warm-pool.js +5 -5
- package/dist/core/subscription-state.js +7 -5
- package/dist/core/substrate/render.js +10 -10
- package/dist/core/user-settings.d.ts +18 -1
- package/dist/core/user-settings.js +37 -0
- package/dist/daemon/__tests__/node-deadline-lane.test.d.ts +1 -0
- package/dist/daemon/__tests__/node-deadline-lane.test.js +76 -0
- package/dist/daemon/__tests__/node-outcome-birth-invariants.test.d.ts +1 -0
- package/dist/daemon/__tests__/node-outcome-birth-invariants.test.js +122 -0
- package/dist/daemon/__tests__/node-outcome-delivery.test.d.ts +1 -0
- package/dist/daemon/__tests__/node-outcome-delivery.test.js +86 -0
- package/dist/daemon/api/__tests__/broker-settle-park.test.js +86 -18
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +10 -1
- package/dist/daemon/api/__tests__/seam/node-outcome-api.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/seam/node-outcome-api.test.js +159 -0
- package/dist/daemon/api/__tests__/submit-result.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/submit-result.test.js +214 -0
- package/dist/daemon/api/handlers/broker-ops.js +53 -24
- package/dist/daemon/api/handlers/canvas.js +2 -2
- package/dist/daemon/api/handlers/messages.js +12 -2
- package/dist/daemon/api/handlers/node-outcomes.d.ts +2 -0
- package/dist/daemon/api/handlers/node-outcomes.js +136 -0
- package/dist/daemon/api/handlers/nodes.js +67 -1
- package/dist/daemon/api/handlers/profiles.js +53 -1
- package/dist/daemon/api/handlers/reports.js +197 -33
- package/dist/daemon/api/map.d.ts +7 -2
- package/dist/daemon/api/map.js +78 -4
- package/dist/daemon/api/server.js +2 -0
- package/dist/daemon/crtrd.js +12 -0
- package/dist/daemon/fleet.d.ts +4 -8
- package/dist/daemon/fleet.js +48 -9
- package/dist/daemon/messaging/node-message.js +2 -1
- package/dist/daemon/node-outcome/deliver-outcome.d.ts +14 -0
- package/dist/daemon/node-outcome/deliver-outcome.js +155 -0
- package/dist/daemon/park-activity.d.ts +9 -0
- package/dist/daemon/park-activity.js +30 -0
- package/dist/daemon/park-pending.d.ts +10 -5
- package/dist/daemon/park-pending.js +18 -8
- package/dist/daemon/reconcilers/bash-deadline.js +6 -0
- package/dist/daemon/reconcilers/broker-supervision.d.ts +3 -7
- package/dist/daemon/reconcilers/broker-supervision.js +29 -33
- package/dist/daemon/reconcilers/node-deadline.d.ts +5 -0
- package/dist/daemon/reconcilers/node-deadline.js +82 -0
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +9 -1
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +12 -3
- package/dist/daemon/reconcilers/node-outcome-delivery-lane.d.ts +10 -0
- package/dist/daemon/reconcilers/node-outcome-delivery-lane.js +41 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +66 -66
- package/dist/pi-extensions/canvas-structured-output.d.ts +9 -5
- package/dist/pi-extensions/canvas-structured-output.js +23 -297
- package/dist/shared/env.d.ts +3 -0
- package/dist/shared/env.js +6 -0
- package/dist/shared/generated-context.d.ts +3 -9
- package/dist/shared/generated-context.js +8 -16
- package/dist/types.d.ts +12 -0
- package/dist/types.js +1 -0
- package/package.json +8 -8
- package/runtime.lock.json +227 -383
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type NodeOutcomeKind, type TerminalReason } from './types.js';
|
|
2
|
+
export type OutcomeDeliveryState = 'armed' | 'pending' | 'running' | 'accepted' | 'permanent_failed';
|
|
3
|
+
export interface OutcomeDeliveryFailure {
|
|
4
|
+
kind: 'exit' | 'signal' | 'timeout' | 'spawn_error';
|
|
5
|
+
exitCode?: number;
|
|
6
|
+
signal?: string;
|
|
7
|
+
message?: string;
|
|
8
|
+
stderr?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Durable exponential retry delay for the attempt that just failed. */
|
|
11
|
+
export declare function outcomeDeliveryBackoffMs(attempt: number): number;
|
|
12
|
+
export interface OutcomeDeliveryRecord {
|
|
13
|
+
nodeId: string;
|
|
14
|
+
state: OutcomeDeliveryState;
|
|
15
|
+
attempt: number;
|
|
16
|
+
actionName: string;
|
|
17
|
+
argv: string[];
|
|
18
|
+
cwd: string;
|
|
19
|
+
configOrigin: string;
|
|
20
|
+
payload: unknown | null;
|
|
21
|
+
document: unknown | null;
|
|
22
|
+
nextAttemptAt: number;
|
|
23
|
+
claimedAt: number | null;
|
|
24
|
+
claimOwner: string | null;
|
|
25
|
+
acceptedAt: number | null;
|
|
26
|
+
permanentFailedAt: number | null;
|
|
27
|
+
lastFailure: OutcomeDeliveryFailure | null;
|
|
28
|
+
createdAt: number;
|
|
29
|
+
updatedAt: number;
|
|
30
|
+
}
|
|
31
|
+
export interface OutcomeDocumentSnapshot {
|
|
32
|
+
kind: NodeOutcomeKind;
|
|
33
|
+
reason: TerminalReason;
|
|
34
|
+
at: string;
|
|
35
|
+
revision: number;
|
|
36
|
+
detailJson: string | null;
|
|
37
|
+
}
|
|
38
|
+
/** Compose every delivery document from the hydrated identity and the immutable outcome latch. */
|
|
39
|
+
export declare function composeOutcomeDeliveryDocument(args: {
|
|
40
|
+
nodeId: string;
|
|
41
|
+
outcome: OutcomeDocumentSnapshot;
|
|
42
|
+
action: {
|
|
43
|
+
name: string;
|
|
44
|
+
payload: unknown;
|
|
45
|
+
};
|
|
46
|
+
finalReport?: string | null;
|
|
47
|
+
structuredResult?: unknown;
|
|
48
|
+
}): string;
|
|
49
|
+
/** Register a target atomically against the outcome latch. A still-armed target
|
|
50
|
+
* is replaceable; a settled registration is not. When the outcome already
|
|
51
|
+
* exists, the row is born pending with its complete immutable document. */
|
|
52
|
+
export declare function armOutcomeDelivery(row: {
|
|
53
|
+
nodeId: string;
|
|
54
|
+
actionName: string;
|
|
55
|
+
argv: string[];
|
|
56
|
+
cwd: string;
|
|
57
|
+
configOrigin: string;
|
|
58
|
+
payload?: unknown;
|
|
59
|
+
now?: number;
|
|
60
|
+
}): OutcomeDeliveryRecord | null;
|
|
61
|
+
export declare function readOutcomeDelivery(nodeId: string): OutcomeDeliveryRecord | null;
|
|
62
|
+
export declare function dueOutcomeDeliveries(now: number): OutcomeDeliveryRecord[];
|
|
63
|
+
export declare function claimOutcomeDelivery(nodeId: string, now: number, claimOwner: string): OutcomeDeliveryRecord | null;
|
|
64
|
+
export type OutcomeDeliverySettlement = {
|
|
65
|
+
state: 'accepted';
|
|
66
|
+
} | {
|
|
67
|
+
state: 'permanent_failed';
|
|
68
|
+
failure: OutcomeDeliveryFailure;
|
|
69
|
+
} | {
|
|
70
|
+
state: 'pending';
|
|
71
|
+
nextAttemptAt: number;
|
|
72
|
+
failure: OutcomeDeliveryFailure;
|
|
73
|
+
};
|
|
74
|
+
/** Settle only the still-owned running attempt, fencing stale delivery processes. */
|
|
75
|
+
export declare function settleOutcomeDelivery(nodeId: string, attempt: number, claimOwner: string, settlement: OutcomeDeliverySettlement, now: number): boolean;
|
|
76
|
+
export declare function recoverRunningOutcomeDeliveries(now: number): number;
|
|
77
|
+
/** Disarming is deliberately limited to registrations that have not started delivery. */
|
|
78
|
+
export declare function disarmOutcomeDelivery(nodeId: string): boolean;
|
|
79
|
+
/** A canonical final is the one allowed outcome supersede: discard any old claim and retry state. */
|
|
80
|
+
export declare function resetOutcomeDeliveryForSupersede(nodeId: string, documentJson: string | null, now: number): boolean;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Durable delivery registration and attempt state for a node's terminal outcome.
|
|
2
|
+
import { openDb, withCanvasWrite } from './db.js';
|
|
3
|
+
import { getNode } from './canvas.js';
|
|
4
|
+
import { OUTCOME_PAYLOAD_MAX_BYTES } from './types.js';
|
|
5
|
+
import { composeNodeOutcomeDocument } from '../runtime/outcome-document.js';
|
|
6
|
+
/** Durable exponential retry delay for the attempt that just failed. */
|
|
7
|
+
export function outcomeDeliveryBackoffMs(attempt) {
|
|
8
|
+
return Math.min(5_000 * 2 ** (attempt - 1), 3_600_000);
|
|
9
|
+
}
|
|
10
|
+
function serializedPayload(payload) {
|
|
11
|
+
if (payload === undefined)
|
|
12
|
+
return null;
|
|
13
|
+
const json = JSON.stringify(payload);
|
|
14
|
+
if (json === undefined)
|
|
15
|
+
throw new TypeError('outcome delivery payload must be JSON-serializable');
|
|
16
|
+
if (Buffer.byteLength(json, 'utf8') > OUTCOME_PAYLOAD_MAX_BYTES) {
|
|
17
|
+
throw new RangeError(`outcome delivery payload exceeds ${OUTCOME_PAYLOAD_MAX_BYTES} bytes`);
|
|
18
|
+
}
|
|
19
|
+
return json;
|
|
20
|
+
}
|
|
21
|
+
function outcomeDeliveryFrom(row) {
|
|
22
|
+
return {
|
|
23
|
+
nodeId: row.node_id,
|
|
24
|
+
state: row.state,
|
|
25
|
+
attempt: row.attempt,
|
|
26
|
+
actionName: row.action_name,
|
|
27
|
+
argv: JSON.parse(row.argv_json),
|
|
28
|
+
cwd: row.cwd,
|
|
29
|
+
configOrigin: row.config_origin,
|
|
30
|
+
payload: row.payload_json === null ? null : JSON.parse(row.payload_json),
|
|
31
|
+
document: row.document_json === null ? null : JSON.parse(row.document_json),
|
|
32
|
+
nextAttemptAt: row.next_attempt_at,
|
|
33
|
+
claimedAt: row.claimed_at,
|
|
34
|
+
claimOwner: row.claim_owner,
|
|
35
|
+
acceptedAt: row.accepted_at,
|
|
36
|
+
permanentFailedAt: row.permanent_failed_at,
|
|
37
|
+
lastFailure: row.last_failure_json === null ? null : JSON.parse(row.last_failure_json),
|
|
38
|
+
createdAt: row.created_at,
|
|
39
|
+
updatedAt: row.updated_at,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** The latch, rather than a later mutable row, defines the terminal view in a delivery document. */
|
|
43
|
+
function nodeAtOutcome(node, outcome) {
|
|
44
|
+
let status;
|
|
45
|
+
let intent;
|
|
46
|
+
if (outcome.kind === 'result' || outcome.reason === 'finished' || outcome.reason === 'declined') {
|
|
47
|
+
status = 'done';
|
|
48
|
+
intent = 'done';
|
|
49
|
+
}
|
|
50
|
+
else if (outcome.reason === 'parked') {
|
|
51
|
+
status = 'done';
|
|
52
|
+
intent = 'parked';
|
|
53
|
+
}
|
|
54
|
+
else if (outcome.reason === 'closed' || outcome.reason === 'retired' || outcome.reason === 'stranded' || outcome.reason === 'deadline_exceeded') {
|
|
55
|
+
status = 'canceled';
|
|
56
|
+
intent = null;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
status = 'dead';
|
|
60
|
+
intent = null;
|
|
61
|
+
}
|
|
62
|
+
return { ...node, status, intent, terminal_reason: outcome.reason };
|
|
63
|
+
}
|
|
64
|
+
/** Compose every delivery document from the hydrated identity and the immutable outcome latch. */
|
|
65
|
+
export function composeOutcomeDeliveryDocument(args) {
|
|
66
|
+
const node = getNode(args.nodeId);
|
|
67
|
+
if (node === null)
|
|
68
|
+
throw new Error(`unknown node ${args.nodeId}`);
|
|
69
|
+
return JSON.stringify(composeNodeOutcomeDocument({
|
|
70
|
+
node: nodeAtOutcome(node, args.outcome),
|
|
71
|
+
outcome: args.outcome,
|
|
72
|
+
action: args.action,
|
|
73
|
+
...(args.finalReport === undefined ? {} : { finalReport: args.finalReport }),
|
|
74
|
+
...(args.structuredResult === undefined ? {} : { structuredResult: args.structuredResult }),
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
/** Register a target atomically against the outcome latch. A still-armed target
|
|
78
|
+
* is replaceable; a settled registration is not. When the outcome already
|
|
79
|
+
* exists, the row is born pending with its complete immutable document. */
|
|
80
|
+
export function armOutcomeDelivery(row) {
|
|
81
|
+
const now = row.now ?? Date.now();
|
|
82
|
+
const payloadJson = serializedPayload(row.payload);
|
|
83
|
+
return withCanvasWrite((db) => {
|
|
84
|
+
const node = db.prepare('SELECT * FROM nodes WHERE node_id = ?').get(row.nodeId);
|
|
85
|
+
if (node === undefined)
|
|
86
|
+
throw new Error(`unknown node ${row.nodeId}`);
|
|
87
|
+
const settled = node.outcome_at !== null;
|
|
88
|
+
if (settled && (node.outcome_kind === null || node.outcome_reason === null)) {
|
|
89
|
+
throw new Error(`outcome latch is incomplete for node ${row.nodeId}`);
|
|
90
|
+
}
|
|
91
|
+
const documentJson = !settled
|
|
92
|
+
? null
|
|
93
|
+
: composeOutcomeDeliveryDocument({
|
|
94
|
+
nodeId: row.nodeId,
|
|
95
|
+
outcome: {
|
|
96
|
+
kind: node.outcome_kind,
|
|
97
|
+
reason: node.outcome_reason,
|
|
98
|
+
at: node.outcome_at,
|
|
99
|
+
revision: node.outcome_revision,
|
|
100
|
+
detailJson: node.outcome_detail_json,
|
|
101
|
+
},
|
|
102
|
+
action: { name: row.actionName, payload: row.payload ?? null },
|
|
103
|
+
finalReport: node.final_report,
|
|
104
|
+
});
|
|
105
|
+
const state = settled ? 'pending' : 'armed';
|
|
106
|
+
const inserted = db.prepare(`INSERT INTO node_outcome_deliveries
|
|
107
|
+
(node_id, state, action_name, argv_json, cwd, config_origin, payload_json, document_json, next_attempt_at, created_at, updated_at)
|
|
108
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
109
|
+
ON CONFLICT(node_id) DO UPDATE SET
|
|
110
|
+
state = excluded.state, action_name = excluded.action_name, argv_json = excluded.argv_json,
|
|
111
|
+
cwd = excluded.cwd, config_origin = excluded.config_origin, payload_json = excluded.payload_json,
|
|
112
|
+
document_json = excluded.document_json, next_attempt_at = excluded.next_attempt_at,
|
|
113
|
+
updated_at = excluded.updated_at
|
|
114
|
+
WHERE node_outcome_deliveries.state = 'armed'`).run(row.nodeId, state, row.actionName, JSON.stringify(row.argv), row.cwd, row.configOrigin, payloadJson, documentJson, settled ? now : 0, now, now);
|
|
115
|
+
if (Number(inserted.changes) === 0)
|
|
116
|
+
return null;
|
|
117
|
+
return outcomeDeliveryFrom(db.prepare('SELECT * FROM node_outcome_deliveries WHERE node_id = ?').get(row.nodeId));
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export function readOutcomeDelivery(nodeId) {
|
|
121
|
+
const row = openDb().prepare('SELECT * FROM node_outcome_deliveries WHERE node_id = ?').get(nodeId);
|
|
122
|
+
return row === undefined ? null : outcomeDeliveryFrom(row);
|
|
123
|
+
}
|
|
124
|
+
export function dueOutcomeDeliveries(now) {
|
|
125
|
+
return openDb().prepare(`SELECT * FROM node_outcome_deliveries
|
|
126
|
+
WHERE state = 'pending' AND next_attempt_at <= ?
|
|
127
|
+
ORDER BY next_attempt_at, created_at, node_id`).all(now).map(outcomeDeliveryFrom);
|
|
128
|
+
}
|
|
129
|
+
export function claimOutcomeDelivery(nodeId, now, claimOwner) {
|
|
130
|
+
return withCanvasWrite((db) => {
|
|
131
|
+
const claimed = db.prepare(`UPDATE node_outcome_deliveries
|
|
132
|
+
SET state = 'running', attempt = attempt + 1, claimed_at = ?, claim_owner = ?, updated_at = ?
|
|
133
|
+
WHERE node_id = ? AND state = 'pending' AND next_attempt_at <= ?`).run(now, claimOwner, now, nodeId, now);
|
|
134
|
+
if (Number(claimed.changes) === 0)
|
|
135
|
+
return null;
|
|
136
|
+
return outcomeDeliveryFrom(db.prepare('SELECT * FROM node_outcome_deliveries WHERE node_id = ?').get(nodeId));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/** Settle only the still-owned running attempt, fencing stale delivery processes. */
|
|
140
|
+
export function settleOutcomeDelivery(nodeId, attempt, claimOwner, settlement, now) {
|
|
141
|
+
const failure = settlement.state === 'accepted' ? null : JSON.stringify(settlement.failure);
|
|
142
|
+
return withCanvasWrite((db) => Number(db.prepare(`UPDATE node_outcome_deliveries
|
|
143
|
+
SET state = ?, next_attempt_at = ?, claimed_at = NULL, claim_owner = NULL,
|
|
144
|
+
accepted_at = ?, permanent_failed_at = ?, last_failure_json = ?, updated_at = ?
|
|
145
|
+
WHERE node_id = ? AND state = 'running' AND attempt = ? AND claim_owner = ?`).run(settlement.state, settlement.state === 'pending' ? settlement.nextAttemptAt : now, settlement.state === 'accepted' ? now : null, settlement.state === 'permanent_failed' ? now : null, failure, now, nodeId, attempt, claimOwner).changes) === 1);
|
|
146
|
+
}
|
|
147
|
+
export function recoverRunningOutcomeDeliveries(now) {
|
|
148
|
+
return withCanvasWrite((db) => Number(db.prepare(`UPDATE node_outcome_deliveries
|
|
149
|
+
SET state = 'pending', next_attempt_at = ?, claimed_at = NULL, claim_owner = NULL, updated_at = ?
|
|
150
|
+
WHERE state = 'running'`).run(now, now).changes));
|
|
151
|
+
}
|
|
152
|
+
/** Disarming is deliberately limited to registrations that have not started delivery. */
|
|
153
|
+
export function disarmOutcomeDelivery(nodeId) {
|
|
154
|
+
return withCanvasWrite((db) => Number(db.prepare("DELETE FROM node_outcome_deliveries WHERE node_id = ? AND state = 'armed'").run(nodeId).changes) === 1);
|
|
155
|
+
}
|
|
156
|
+
/** A canonical final is the one allowed outcome supersede: discard any old claim and retry state. */
|
|
157
|
+
export function resetOutcomeDeliveryForSupersede(nodeId, documentJson, now) {
|
|
158
|
+
return withCanvasWrite((db) => Number(db.prepare(`UPDATE node_outcome_deliveries
|
|
159
|
+
SET state = 'pending', attempt = 0, document_json = ?, next_attempt_at = ?,
|
|
160
|
+
claimed_at = NULL, claim_owner = NULL, accepted_at = NULL, permanent_failed_at = NULL,
|
|
161
|
+
last_failure_json = NULL, updated_at = ?
|
|
162
|
+
WHERE node_id = ?`).run(documentJson, now, now, nodeId).changes) === 1);
|
|
163
|
+
}
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
* zombies and processes reaped between the two probes. Only an unavailable
|
|
5
5
|
* `ps` probe fails open to alive. A null/undefined pid reads dead. */
|
|
6
6
|
export declare function isPidAlive(pid: number | null | undefined): boolean;
|
|
7
|
+
/** Existence-only liveness for READ-ONLY display projections: the signal-0
|
|
8
|
+
* probe alone, no `ps`. A zombie reads alive here, which a dashboard column
|
|
9
|
+
* tolerates and which no lifecycle decision may rely on — revive, teardown,
|
|
10
|
+
* and every other authority stays on `isPidAlive`. Exists because the
|
|
11
|
+
* dashboard snapshot runs this per live row per request; a `ps` spawn each
|
|
12
|
+
* time was a third of daemon CPU on a large canvas. */
|
|
13
|
+
export declare function isPidPresent(pid: number | null | undefined): boolean;
|
|
7
14
|
/** THE single precision-aware identity compare, used EVERYWHERE two
|
|
8
15
|
* `composeIdentity` fingerprints are compared (this module's own
|
|
9
16
|
* `recordedPidLiveness`, `captureTeardownSnapshot`'s reuse check, and
|
package/dist/core/canvas/pid.js
CHANGED
|
@@ -46,6 +46,23 @@ export function isPidAlive(pid) {
|
|
|
46
46
|
const state = psProcessState(pid);
|
|
47
47
|
return state !== 'zombie' && state !== 'gone';
|
|
48
48
|
}
|
|
49
|
+
/** Existence-only liveness for READ-ONLY display projections: the signal-0
|
|
50
|
+
* probe alone, no `ps`. A zombie reads alive here, which a dashboard column
|
|
51
|
+
* tolerates and which no lifecycle decision may rely on — revive, teardown,
|
|
52
|
+
* and every other authority stays on `isPidAlive`. Exists because the
|
|
53
|
+
* dashboard snapshot runs this per live row per request; a `ps` spawn each
|
|
54
|
+
* time was a third of daemon CPU on a large canvas. */
|
|
55
|
+
export function isPidPresent(pid) {
|
|
56
|
+
if (pid == null)
|
|
57
|
+
return false;
|
|
58
|
+
try {
|
|
59
|
+
process.kill(pid, 0);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
return e.code === 'EPERM';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
49
66
|
/** Matches the kernel `boot_id` UUID shape (`/proc/sys/kernel/random/boot_id`),
|
|
50
67
|
* used to tell a NEW-format identity base (a per-boot UUID) apart from a
|
|
51
68
|
* LEGACY one (a `ps lstart` wall-clock timestamp). A boot_id is a lowercase
|
|
@@ -19,7 +19,7 @@ import { fullName } from './labels.js';
|
|
|
19
19
|
import { jobDir, contextDir, reportsDir } from './paths.js';
|
|
20
20
|
import { readNodeRecap } from './node-recap.js';
|
|
21
21
|
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
22
|
-
import { isPidAlive } from './pid.js';
|
|
22
|
+
import { isPidAlive, isPidPresent } from './pid.js';
|
|
23
23
|
import { isFrozenRow, resolveNodeVisual, faultSummary } from './status-glyph.js';
|
|
24
24
|
import { readFault } from '../runtime/fault.js';
|
|
25
25
|
import { parseCard } from '../../shared/generated-context.js';
|
|
@@ -434,7 +434,7 @@ function assistantContentIsSubstantive(content) {
|
|
|
434
434
|
* (an active node is usually dormant between turns). Read directly off disk
|
|
435
435
|
* (mirrors telemetry) to avoid inverting the canvas→runtime dependency. */
|
|
436
436
|
export function isStreaming(nodeId, piPid) {
|
|
437
|
-
if (!
|
|
437
|
+
if (!isPidPresent(piPid))
|
|
438
438
|
return false;
|
|
439
439
|
try {
|
|
440
440
|
return existsSync(join(jobDir(nodeId), 'busy'));
|
|
@@ -18,7 +18,34 @@ export type Lifecycle = 'terminal' | 'resident';
|
|
|
18
18
|
export type Mode = 'base' | 'orchestrator';
|
|
19
19
|
/** Why a node last stopped — drives the daemon's reap-vs-revive decision. */
|
|
20
20
|
export type ExitIntent = 'done' | 'refresh' | 'idle-release' | 'parked' | null;
|
|
21
|
-
export type TerminalReason = 'finalized' | 'finished' | 'parked' | 'closed' | 'retired' | 'crashed' | 'stranded';
|
|
21
|
+
export type TerminalReason = 'finalized' | 'finished' | 'parked' | 'closed' | 'retired' | 'crashed' | 'stranded' | 'boot_failed' | 'crash_looped' | 'launch_failed' | 'context_overflow' | 'deadline_exceeded' | 'provider_fatal' | 'declined';
|
|
22
|
+
export type NodeOutcomeKind = 'result' | 'failure';
|
|
23
|
+
/** Maximum serialized opaque action payload; enforced at registration in Phase 5. */
|
|
24
|
+
export declare const OUTCOME_PAYLOAD_MAX_BYTES: number;
|
|
25
|
+
export interface NodeOutcomeDetailV1 {
|
|
26
|
+
schema: 'crtr.node-outcome-detail/v1';
|
|
27
|
+
message?: string;
|
|
28
|
+
fault_kind?: import('../fault-classifier.js').FaultKind;
|
|
29
|
+
error_class?: import('../events/types.js').ErrorClassCode;
|
|
30
|
+
retry?: {
|
|
31
|
+
attempt: number;
|
|
32
|
+
max: number;
|
|
33
|
+
};
|
|
34
|
+
respawn_failures?: number;
|
|
35
|
+
deadline?: {
|
|
36
|
+
deadline_at: string;
|
|
37
|
+
elapsed_ms: number;
|
|
38
|
+
};
|
|
39
|
+
/** Present only for reason `declined`: what the node gave `crtr push result --decline`. */
|
|
40
|
+
declined?: DeclinedResult;
|
|
41
|
+
truncated?: true;
|
|
42
|
+
}
|
|
43
|
+
/** A declined structured result. `code` is opaque here — the requester validates it. */
|
|
44
|
+
export interface DeclinedResult {
|
|
45
|
+
reason: string;
|
|
46
|
+
code: string;
|
|
47
|
+
retryable: boolean;
|
|
48
|
+
}
|
|
22
49
|
/** The two structural edges. `subscribes_to` is the load-bearing spine (flow,
|
|
23
50
|
* org chart, views, completion routing). `spawned_by` is audit only. */
|
|
24
51
|
export type EdgeType = 'subscribes_to' | 'spawned_by';
|
|
@@ -266,6 +293,14 @@ export interface NodeRuntime {
|
|
|
266
293
|
final_report?: string | null;
|
|
267
294
|
/** ISO timestamp carried by the committed canonical final report, or null. */
|
|
268
295
|
finalized_at?: string | null;
|
|
296
|
+
/** Durable terminal outcome. `outcome_at` is its write-once latch. */
|
|
297
|
+
outcome_kind?: NodeOutcomeKind | null;
|
|
298
|
+
outcome_reason?: TerminalReason | null;
|
|
299
|
+
outcome_at?: string | null;
|
|
300
|
+
outcome_revision?: number;
|
|
301
|
+
outcome_detail_json?: string | null;
|
|
302
|
+
/** Absolute wall-clock spawn deadline, or null when unbounded. */
|
|
303
|
+
deadline_at?: string | null;
|
|
269
304
|
/** OS pid of the live broker process, recorded on boot (stophook session_start).
|
|
270
305
|
* The daemon's authoritative liveness signal — every node is a detached broker,
|
|
271
306
|
* so `isPidAlive(pi_pid)` is the sole liveness check. Cleared to null by
|
|
@@ -342,6 +377,12 @@ export interface NodeRow {
|
|
|
342
377
|
terminal_reason?: TerminalReason | null;
|
|
343
378
|
final_report?: string | null;
|
|
344
379
|
finalized_at?: string | null;
|
|
380
|
+
outcome_kind?: NodeOutcomeKind | null;
|
|
381
|
+
outcome_reason?: TerminalReason | null;
|
|
382
|
+
outcome_at?: string | null;
|
|
383
|
+
outcome_revision?: number;
|
|
384
|
+
outcome_detail_json?: string | null;
|
|
385
|
+
deadline_at?: string | null;
|
|
345
386
|
pi_pid: number | null;
|
|
346
387
|
/** See `NodeRuntime.pi_pid_identity`. */
|
|
347
388
|
pi_pid_identity: string | null;
|
|
@@ -5,4 +5,5 @@
|
|
|
5
5
|
// `meta.json` is the source of truth for its own row; the db is a queryable
|
|
6
6
|
// index over those metas, plus the authoritative store for the mutable
|
|
7
7
|
// `subscribes_to` edges (which no single meta owns).
|
|
8
|
-
|
|
8
|
+
/** Maximum serialized opaque action payload; enforced at registration in Phase 5. */
|
|
9
|
+
export const OUTCOME_PAYLOAD_MAX_BYTES = 64 * 1024;
|
package/dist/core/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PageComponentRegistration, Scope, ScopeConfig, ScopeState, HumanActionConfig, KindConfig, ModelLaddersConfig, ModelProvider, ModelStrength, ModelRouteConfig, ModelRoutingConfig } from '../types.js';
|
|
1
|
+
import type { PageComponentRegistration, Scope, ScopeConfig, ScopeState, HumanActionConfig, KindConfig, ModelLaddersConfig, ModelProvider, ModelStrength, ModelRouteConfig, ModelRoutingConfig, ProviderOptionsConfig } from '../types.js';
|
|
2
2
|
import { type BindingId } from './keybindings/catalog.js';
|
|
3
3
|
export declare function configPath(scope: Scope): string | null;
|
|
4
4
|
export declare function statePath(scope: Scope): string | null;
|
|
@@ -111,6 +111,9 @@ export declare function readRawScopeConfig(scope: Scope): Partial<ScopeConfig> |
|
|
|
111
111
|
export interface MergedLaunchConfig {
|
|
112
112
|
kinds: Record<string, KindConfig>;
|
|
113
113
|
modelLadders: ModelLaddersConfig;
|
|
114
|
+
/** Per-provider behavior options keyed by runtime provider id, merged
|
|
115
|
+
* per provider across scopes (project stack > profile > user). */
|
|
116
|
+
providerOptions: Record<string, ProviderOptionsConfig>;
|
|
114
117
|
/** Complete declared routes. Every route here has all four strengths. */
|
|
115
118
|
modelRoutes?: Record<string, ModelRouteConfig>;
|
|
116
119
|
/** Read-only compatibility view for sparse routes written before complete
|
package/dist/core/config.js
CHANGED
|
@@ -343,6 +343,25 @@ function mergeSpawnEnv(raw, base = []) {
|
|
|
343
343
|
}
|
|
344
344
|
return { allow: [...new Set([...base, ...extra])] };
|
|
345
345
|
}
|
|
346
|
+
/** Per-provider options, merged SHALLOWLY per provider id so a nearer scope
|
|
347
|
+
* that sets one option does not erase a sibling option from a farther scope. */
|
|
348
|
+
function mergeProviderOptions(raw, base = {}) {
|
|
349
|
+
const merged = {};
|
|
350
|
+
for (const [providerId, options] of Object.entries(base))
|
|
351
|
+
merged[providerId] = { ...options };
|
|
352
|
+
if (raw !== null && typeof raw === 'object') {
|
|
353
|
+
for (const [providerId, options] of Object.entries(raw)) {
|
|
354
|
+
if (options === null || typeof options !== 'object')
|
|
355
|
+
continue;
|
|
356
|
+
const next = { ...merged[providerId] };
|
|
357
|
+
const fastMode = options.fastMode;
|
|
358
|
+
if (typeof fastMode === 'boolean')
|
|
359
|
+
next.fastMode = fastMode;
|
|
360
|
+
merged[providerId] = next;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return merged;
|
|
364
|
+
}
|
|
346
365
|
/** The one bare-command-name rule, shared by the read path (`normalizeBin`)
|
|
347
366
|
* and the strict install gate (`invalidPluginBinReasons`): a name a shell can
|
|
348
367
|
* invoke verbatim and a filesystem can hold as a single symlink entry — no
|
|
@@ -761,6 +780,7 @@ function mergeConfig(partial) {
|
|
|
761
780
|
const keybindings = normalizeKeybindings(partial.keybindings);
|
|
762
781
|
const tmux_passthrough = normalizeTmuxPassthrough(partial.tmux_passthrough);
|
|
763
782
|
const modelLadders = mergeModelLadders(partial.modelLadders);
|
|
783
|
+
const providerOptions = mergeProviderOptions(partial.providerOptions, defaults.providerOptions);
|
|
764
784
|
const routes = resolveModelRoutes(mergeModelRoutePatches(partial.modelRoutes));
|
|
765
785
|
const modelRoutes = routes.complete;
|
|
766
786
|
const modelRouting = mergeModelRouting(partial.modelRouting, undefined);
|
|
@@ -769,7 +789,7 @@ function mergeConfig(partial) {
|
|
|
769
789
|
const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
|
|
770
790
|
const bin = normalizeBin(partial.bin);
|
|
771
791
|
const humanActions = normalizeHumanActions(partial.humanActions);
|
|
772
|
-
return { bin, humanActions, schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, lifecycle, completion_bell, working_gerunds, whip_mode, page_components, page_surface, whip_messages, whip_message_mode, mouse_mode_default, live_cycles, condensed_history, bash_tool_purpose, fold_finished_tools, summarize_tool_calls, detailed_tool_recaps, keybindings, tmux_passthrough, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(Object.keys(routes.legacy).length > 0 ? { legacyModelRoutes: routes.legacy } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
|
|
792
|
+
return { bin, humanActions, schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, lifecycle, completion_bell, working_gerunds, whip_mode, page_components, page_surface, whip_messages, whip_message_mode, mouse_mode_default, live_cycles, condensed_history, bash_tool_purpose, fold_finished_tools, summarize_tool_calls, detailed_tool_recaps, keybindings, tmux_passthrough, modelLadders, providerOptions, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(Object.keys(routes.legacy).length > 0 ? { legacyModelRoutes: routes.legacy } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
|
|
773
793
|
}
|
|
774
794
|
/** Raw (un-defaulted) partial config for one scope, or null if the scope has
|
|
775
795
|
* no root or no config.json. Used by `readMergedLaunchConfig` to layer
|
|
@@ -858,12 +878,14 @@ export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileI
|
|
|
858
878
|
// (ladders/routes/spawnEnv) have no plugin channel.
|
|
859
879
|
let kinds = mergeKinds(userRaw?.kinds, layerPluginKinds('user', scopeRoot('user'), defaults.kinds));
|
|
860
880
|
let modelLadders = mergeModelLadders(userRaw?.modelLadders, defaults.modelLadders);
|
|
881
|
+
let providerOptions = mergeProviderOptions(userRaw?.providerOptions, defaults.providerOptions);
|
|
861
882
|
let modelRoutePatches = mergeModelRoutePatches(userRaw?.modelRoutes);
|
|
862
883
|
let modelRouting = mergeModelRouting(userRaw?.modelRouting, undefined);
|
|
863
884
|
let spawnEnv = mergeSpawnEnv(userRaw?.spawnEnv, defaults.spawnEnv?.allow);
|
|
864
885
|
if (profileRaw !== null) {
|
|
865
886
|
kinds = mergeKinds(profileRaw.kinds, kinds);
|
|
866
887
|
modelLadders = mergeModelLadders(profileRaw.modelLadders, modelLadders);
|
|
888
|
+
providerOptions = mergeProviderOptions(profileRaw.providerOptions, providerOptions);
|
|
867
889
|
modelRoutePatches = mergeModelRoutePatches(profileRaw.modelRoutes, modelRoutePatches);
|
|
868
890
|
modelRouting = mergeModelRouting(profileRaw.modelRouting, modelRouting);
|
|
869
891
|
spawnEnv = mergeSpawnEnv(profileRaw.spawnEnv, spawnEnv.allow);
|
|
@@ -873,6 +895,7 @@ export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileI
|
|
|
873
895
|
if (raw === null)
|
|
874
896
|
continue;
|
|
875
897
|
modelLadders = mergeModelLadders(raw.modelLadders, modelLadders);
|
|
898
|
+
providerOptions = mergeProviderOptions(raw.providerOptions, providerOptions);
|
|
876
899
|
modelRoutePatches = mergeModelRoutePatches(raw.modelRoutes, modelRoutePatches);
|
|
877
900
|
modelRouting = mergeModelRouting(raw.modelRouting, modelRouting);
|
|
878
901
|
spawnEnv = mergeSpawnEnv(raw.spawnEnv, spawnEnv.allow);
|
|
@@ -881,6 +904,7 @@ export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileI
|
|
|
881
904
|
return {
|
|
882
905
|
kinds,
|
|
883
906
|
modelLadders,
|
|
907
|
+
providerOptions,
|
|
884
908
|
...(Object.keys(routes.complete).length > 0 ? { modelRoutes: routes.complete } : {}),
|
|
885
909
|
...(Object.keys(routes.legacy).length > 0 ? { legacyModelRoutes: routes.legacy } : {}),
|
|
886
910
|
...(modelRouting !== undefined ? { modelRouting } : {}),
|
package/dist/core/feed/feed.d.ts
CHANGED
|
@@ -9,6 +9,14 @@ export interface PushOpts {
|
|
|
9
9
|
from?: string;
|
|
10
10
|
/** Attribution snapshotted by the human-ticket settlement paths; absent means an ordinary report. */
|
|
11
11
|
disposition?: InboxDisposition;
|
|
12
|
+
/** Final only. The outcome the canonical final records; absent means `result`/`finalized`. */
|
|
13
|
+
outcome?: FinalOutcome;
|
|
14
|
+
}
|
|
15
|
+
/** A declined structured result: the node finishes, but its latch records a failure. */
|
|
16
|
+
export interface FinalOutcome {
|
|
17
|
+
kind: 'failure';
|
|
18
|
+
reason: 'declined';
|
|
19
|
+
detailJson: string | null;
|
|
12
20
|
}
|
|
13
21
|
export interface PushResult {
|
|
14
22
|
reportPath: string;
|
package/dist/core/feed/feed.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync, linkSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { openDb, reportsDir, subscribersOf, cancelCronsOnWake, getNode } from '../canvas/index.js';
|
|
5
|
+
import { openDb, reportsDir, subscribersOf, cancelCronsOnWake, composeOutcomeDeliveryDocument, getNode, readOutcomeDelivery, resetOutcomeDeliveryForSupersede } from '../canvas/index.js';
|
|
6
6
|
import { appendInbox } from './inbox.js';
|
|
7
7
|
import { appendPassive } from './passive.js';
|
|
8
8
|
import { operationIdContext } from '../events/operation-id.js';
|
|
@@ -77,7 +77,7 @@ function deliver(subscribers, from, fromName, kind, requestedTier, reportRef, bo
|
|
|
77
77
|
return { deliveredTo, failedTo };
|
|
78
78
|
}
|
|
79
79
|
/** Claim the one canonical final while holding SQLite's write lock. */
|
|
80
|
-
function commitCanonicalFinal(nodeId, staged) {
|
|
80
|
+
function commitCanonicalFinal(nodeId, staged, outcome) {
|
|
81
81
|
const db = openDb();
|
|
82
82
|
let installed = false;
|
|
83
83
|
db.exec('BEGIN IMMEDIATE');
|
|
@@ -92,9 +92,28 @@ function commitCanonicalFinal(nodeId, staged) {
|
|
|
92
92
|
if (row.status !== 'active' && row.status !== 'idle' && row.status !== 'done')
|
|
93
93
|
throw new FinalizationError('finalization_forbidden');
|
|
94
94
|
const subscribers = db.prepare(`SELECT from_id AS node_id, active, created FROM edges WHERE type = 'subscribes_to' AND to_id = ? ORDER BY created`).all(nodeId).map((row) => ({ node_id: row['node_id'], active: row['active'] === 1, created: row['created'] }));
|
|
95
|
+
const delivery = readOutcomeDelivery(nodeId);
|
|
96
|
+
const node = getNode(nodeId);
|
|
97
|
+
const recordsOutcome = node?.kind !== 'human';
|
|
98
|
+
const revision = (node?.outcome_revision ?? 0) + 1;
|
|
99
|
+
const outcomeKind = outcome?.kind ?? 'result';
|
|
100
|
+
const outcomeReason = outcome?.reason ?? 'finalized';
|
|
101
|
+
const outcomeDetailJson = outcome?.detailJson ?? null;
|
|
102
|
+
const documentJson = !recordsOutcome || delivery === null || node === null ? null : composeOutcomeDeliveryDocument({
|
|
103
|
+
nodeId,
|
|
104
|
+
outcome: { kind: outcomeKind, reason: outcomeReason, at: staged.ts, revision, detailJson: outcomeDetailJson },
|
|
105
|
+
action: { name: delivery.actionName, payload: delivery.payload },
|
|
106
|
+
finalReport: staged.basename,
|
|
107
|
+
});
|
|
95
108
|
install(staged);
|
|
96
109
|
installed = true;
|
|
97
|
-
|
|
110
|
+
if (recordsOutcome) {
|
|
111
|
+
db.prepare("UPDATE nodes SET status = 'done', intent = 'done', terminal_reason = ?, final_report = ?, finalized_at = ?, outcome_kind = ?, outcome_reason = ?, outcome_at = ?, outcome_revision = outcome_revision + 1, outcome_detail_json = ? WHERE node_id = ? AND final_report IS NULL").run(outcomeReason, staged.basename, staged.ts, outcomeKind, outcomeReason, staged.ts, outcomeDetailJson, nodeId);
|
|
112
|
+
resetOutcomeDeliveryForSupersede(nodeId, documentJson, Date.now());
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
db.prepare("UPDATE nodes SET status = 'done', intent = 'done', terminal_reason = 'finalized', final_report = ?, finalized_at = ? WHERE node_id = ? AND final_report IS NULL").run(staged.basename, staged.ts, nodeId);
|
|
116
|
+
}
|
|
98
117
|
db.exec('COMMIT');
|
|
99
118
|
return { subscribers };
|
|
100
119
|
}
|
|
@@ -113,11 +132,13 @@ async function pushWithContext(nodeId, opts) {
|
|
|
113
132
|
throw new Error(`push: empty nodeId (kind=${opts.kind}) — a report must belong to a node`);
|
|
114
133
|
if (opts.kind === 'final' && opts.deliveryTier !== undefined)
|
|
115
134
|
throw new Error('push final does not accept a delivery tier');
|
|
135
|
+
if (opts.kind !== 'final' && opts.outcome !== undefined)
|
|
136
|
+
throw new Error('push outcome is final-only');
|
|
116
137
|
const staged = stageReport(nodeId, opts.kind, opts.body);
|
|
117
138
|
const from = opts.from ?? nodeId;
|
|
118
139
|
const fromName = getNode(from)?.name;
|
|
119
140
|
if (opts.kind === 'final') {
|
|
120
|
-
const { subscribers } = commitCanonicalFinal(nodeId, staged);
|
|
141
|
+
const { subscribers } = commitCanonicalFinal(nodeId, staged, opts.outcome);
|
|
121
142
|
// The finalization latch ends every wait this node was holding. It flips the
|
|
122
143
|
// row to done DIRECTLY (not through lifecycle's `finish` event), so the
|
|
123
144
|
// deadline cleanup must ride here too: an armed deadline outliving the final
|
|
@@ -87,6 +87,11 @@ export declare function writeCursor(nodeId: string, entryId: string): void;
|
|
|
87
87
|
* (do not park, do not shut down, do not resume quietly), so report it and
|
|
88
88
|
* return that. */
|
|
89
89
|
export declare function hasUnseenWakeMail(nodeId: string): boolean;
|
|
90
|
+
/** Whether wake-capable mail arrived at or after a parking turn began. Unlike
|
|
91
|
+
* `hasUnseenWakeMail`, this deliberately includes mail the main engine already
|
|
92
|
+
* consumed: a shadow completion must not undo activity that occurred while it
|
|
93
|
+
* ran. A failed scan conservatively keeps the node live. */
|
|
94
|
+
export declare function hasWakeMailSince(nodeId: string, sinceMs: number): boolean;
|
|
90
95
|
/** Clip a body to a bounded preview, reporting whether anything was dropped. */
|
|
91
96
|
export declare function clipBody(body: string): {
|
|
92
97
|
text: string;
|
package/dist/core/feed/inbox.js
CHANGED
|
@@ -315,6 +315,19 @@ export function hasUnseenWakeMail(nodeId) {
|
|
|
315
315
|
return true;
|
|
316
316
|
}
|
|
317
317
|
}
|
|
318
|
+
/** Whether wake-capable mail arrived at or after a parking turn began. Unlike
|
|
319
|
+
* `hasUnseenWakeMail`, this deliberately includes mail the main engine already
|
|
320
|
+
* consumed: a shadow completion must not undo activity that occurred while it
|
|
321
|
+
* ran. A failed scan conservatively keeps the node live. */
|
|
322
|
+
export function hasWakeMailSince(nodeId, sinceMs) {
|
|
323
|
+
try {
|
|
324
|
+
return readPhysicalInbox(nodeId).some((entry) => entry.tier !== 'deferred' && Date.parse(entry.ts) >= sinceMs);
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
emitEvent({ level: 'warn', event: 'inbox.wake_scan.failed', node_id: nodeId, error });
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
318
331
|
// Coalesce
|
|
319
332
|
/** Bounds for inlining a message body's preview in the digest. */
|
|
320
333
|
const BODY_MAX_LINES = 12;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BindingDefinition, SafetyRequirement } from './types.js';
|
|
2
2
|
import type { AttachBindingId, AttachMenuBindingId } from './attach-control.js';
|
|
3
|
-
type TmuxMenuAction = 'open' | 'attach' | 'show' | 'view-settings' | 'fold-tools' | 'metadata' | 'promote' | 'demote-mode' | 'resume' | 'fork' | 'demote' | 'make-resident' | 'detach' | 'close-subtree' | 'background-bash' | 'file-review' | 'pin' | 'reload-viewer' | 'recap' | 'context' | 'context-admin' | 'providers' | 'plugins' | 'graph' | 'focus-manager' | 'copy-id' | 'issues';
|
|
3
|
+
type TmuxMenuAction = 'open' | 'attach' | 'show' | 'view-settings' | 'provider-options' | 'fold-tools' | 'metadata' | 'promote' | 'demote-mode' | 'resume' | 'fork' | 'demote' | 'make-resident' | 'detach' | 'close-subtree' | 'background-bash' | 'file-review' | 'pin' | 'reload-viewer' | 'recap' | 'context' | 'context-admin' | 'providers' | 'plugins' | 'graph' | 'focus-manager' | 'copy-id' | 'issues';
|
|
4
4
|
type TmuxMenuEntryAction = Exclude<TmuxMenuAction, 'open' | 'attach'> | `issue.${'idea' | 'bug'}`;
|
|
5
5
|
type GraphAction = 'down' | 'up' | 'first' | 'last' | 'collapse-or-manager' | 'expand-or-child' | 'focus' | 'revive' | 'focus-manager' | 'close-subtree' | 'dismiss';
|
|
6
6
|
type BrowseAction = 'quit' | 'down' | 'up' | 'first' | 'last' | 'expand-or-child' | 'collapse-or-parent' | 'toggle-fold' | 'tab-next' | 'tab-previous' | `tab.${1 | 2 | 3 | 4 | 5}` | 'close-subtree' | 'confirm-close' | 'sort' | 'scope' | 'profile' | 'residents' | 'preview' | 'report' | 'pin' | 'search' | 'revive-now' | 'focus' | 'focus-background' | 'yank-id';
|
|
@@ -55,6 +55,9 @@ export const BINDING_CATALOG = Object.freeze([
|
|
|
55
55
|
...menuEntry('metadata', 'Show metadata', 'Show the focused node’s metadata dossier.', ['m'], ['tmux.menu.show']),
|
|
56
56
|
...menuEntry('context', 'Browse context', 'Browse the focused node context.', ['c'], ['tmux.menu.show']),
|
|
57
57
|
...menuEntry('providers', 'Open settings', 'Open the crouter settings panel.', [','], ['tmux.menu']),
|
|
58
|
+
// Next to the settings comma: the same page, opened on its provider-options
|
|
59
|
+
// destination.
|
|
60
|
+
...menuEntry('provider-options', 'Provider options', 'Open provider options — how an authenticated provider serves your turns, including Codex fast mode.', ['.'], ['tmux.menu']),
|
|
58
61
|
// j and k steer the menu cursor, so no selector may claim either.
|
|
59
62
|
...menuEntry('plugins', 'Browse plugins', 'Open the crouter plugin browser.', ['l'], ['tmux.menu']),
|
|
60
63
|
// Lowercase c opens the create submenu, so the context surface takes the
|
|
@@ -114,7 +114,9 @@ export declare function isProfilePaused(profileId: string | null | undefined): b
|
|
|
114
114
|
export declare function assertProfileEntryActive(entry: ProfileEntry): void;
|
|
115
115
|
export declare function assertProfileActive(profileId: string | null | undefined): ProfileEntry | null;
|
|
116
116
|
export declare function pauseProfile(profileId: string): ProfileEntry;
|
|
117
|
-
export declare function resumeProfile(profileId: string): ProfileEntry
|
|
117
|
+
export declare function resumeProfile(profileId: string): ProfileEntry & {
|
|
118
|
+
pausedAt: string | null;
|
|
119
|
+
};
|
|
118
120
|
export declare function setProfileDefaultKind(profileId: string, defaultKind: string): ProfileEntry;
|
|
119
121
|
/** Merge `set` entries over the stored map and drop `unset` keys; the
|
|
120
122
|
* `metadata` field is omitted entirely when the result is empty. */
|
|
@@ -592,7 +592,12 @@ export function pauseProfile(profileId) {
|
|
|
592
592
|
return mutateManifest(profileId, (m) => ({ ...m, paused_at: m.paused_at ?? nowIso() }));
|
|
593
593
|
}
|
|
594
594
|
export function resumeProfile(profileId) {
|
|
595
|
-
|
|
595
|
+
let pausedAt = null;
|
|
596
|
+
const entry = mutateManifest(profileId, (m) => {
|
|
597
|
+
pausedAt = m.paused_at;
|
|
598
|
+
return { ...m, paused_at: null };
|
|
599
|
+
});
|
|
600
|
+
return { ...entry, pausedAt };
|
|
596
601
|
}
|
|
597
602
|
export function setProfileDefaultKind(profileId, defaultKind) {
|
|
598
603
|
return mutateManifest(profileId, (m) => ({ ...m, default_kind: defaultKind }));
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { BrokerExtensionStateDTO, BrokerGeneratedNameRequest, BrokerGeneratedNameResultDTO, BrokerInboxCursorDirective, BrokerInboxCursorRequest, BrokerModelCommitRequest, BrokerModelCommitResultDTO, BrokerPersonaAckRequest, BrokerPersonaAckResultDTO, BrokerSessionBoundRequest, BrokerSessionBoundResultDTO, BrokerSettleDirective, BrokerSettleRequest } from '../../../api/dto/broker-ops.js';
|
|
1
|
+
import type { BrokerExtensionStateDTO, BrokerGeneratedNameRequest, BrokerGeneratedNameResultDTO, BrokerInboxCursorDirective, BrokerInboxCursorRequest, BrokerModelCommitRequest, BrokerModelCommitResultDTO, BrokerParkActivityResultDTO, BrokerParkCompleteRequest, BrokerPersonaAckRequest, BrokerPersonaAckResultDTO, BrokerSessionBoundRequest, BrokerSessionBoundResultDTO, BrokerSettleDirective, BrokerSettleRequest } from '../../../api/dto/broker-ops.js';
|
|
2
2
|
import type { RelaunchRootResultDTO } from '../../../api/dto/lifecycle.js';
|
|
3
3
|
import type { PushReportRequest } from '../../../api/dto/reports.js';
|
|
4
4
|
export declare function bindBrokerSession(nodeId: string, request: BrokerSessionBoundRequest): Promise<BrokerSessionBoundResultDTO>;
|
|
5
5
|
export declare function settleBroker(nodeId: string, request: BrokerSettleRequest): Promise<BrokerSettleDirective>;
|
|
6
|
+
export declare function completeBrokerPark(nodeId: string, request: BrokerParkCompleteRequest): Promise<BrokerSettleDirective>;
|
|
7
|
+
export declare function recordBrokerParkActivity(nodeId: string): Promise<BrokerParkActivityResultDTO>;
|
|
6
8
|
export declare function advanceBrokerInboxCursor(nodeId: string, request: BrokerInboxCursorRequest): Promise<BrokerInboxCursorDirective>;
|
|
7
9
|
/** Fixed canvas projection for broker extension rendering. It deliberately
|
|
8
10
|
* exposes no generic node/database facade. */
|