@north-light/crouter 0.3.281 → 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/plugin-manifest-schema.d.ts +208 -0
- package/dist/api/plugin-manifest-schema.js +23 -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/cli.js +8 -2
- 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/pkg/browse/catalog.js +1 -1
- package/dist/commands/pkg/plugin-manage.d.ts +46 -1
- package/dist/commands/pkg/plugin-manage.js +170 -20
- 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/plugin-revalidate.test.d.ts +1 -0
- package/dist/core/__tests__/integration/plugin-revalidate.test.js +355 -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/command-manifests/registry.d.ts +3 -1
- package/dist/core/command-manifests/schema.d.ts +2 -75
- package/dist/core/command-manifests/schema.js +5 -0
- package/dist/core/command-plugins/compose.js +1 -1
- package/dist/core/command-plugins/revalidate.d.ts +18 -0
- package/dist/core/command-plugins/revalidate.js +152 -0
- package/dist/core/command-plugins/transport/http-fetch.d.ts +29 -4
- package/dist/core/command-plugins/transport/http-fetch.js +18 -8
- package/dist/core/command-plugins/transport/http-invoke.js +7 -0
- package/dist/core/command.d.ts +8 -0
- package/dist/core/command.js +19 -3
- package/dist/core/config.d.ts +4 -1
- package/dist/core/config.js +25 -1
- package/dist/core/exclusive-lock.d.ts +12 -0
- package/dist/core/exclusive-lock.js +29 -0
- 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/installed-plugins.js +31 -0
- package/dist/core/keybindings/catalog.d.ts +1 -1
- package/dist/core/keybindings/catalog.js +3 -0
- package/dist/core/manifest-recovery.d.ts +15 -0
- package/dist/core/manifest-recovery.js +72 -0
- package/dist/core/manifest-stale.d.ts +18 -0
- package/dist/core/manifest-stale.js +39 -0
- package/dist/core/plugin-swap-lock.d.ts +9 -0
- package/dist/core/plugin-swap-lock.js +31 -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 +19 -0
- package/dist/types.js +1 -0
- package/package.json +9 -8
- package/runtime.lock.json +227 -383
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 } : {}),
|
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
* work" apart from "my child is queued behind someone else's work" without
|
|
5
5
|
* any new bookkeeping. */
|
|
6
6
|
export declare function exclusiveLockOwnerPid(path: string): number | null;
|
|
7
|
+
/**
|
|
8
|
+
* Block until `path` is no longer held by a LIVE other process, or the wait
|
|
9
|
+
* runs out. Unlike `withExclusiveDirectoryLock*` this never CLAIMS the lock —
|
|
10
|
+
* it is for a reader that only needs the holder's transition to be over before
|
|
11
|
+
* it looks at the directory the holder is rewriting.
|
|
12
|
+
*
|
|
13
|
+
* Returns immediately when the lock is free, when its marker names this very
|
|
14
|
+
* process (a caller waiting on its own lock would otherwise deadlock), or when
|
|
15
|
+
* the named owner is gone — a dead holder's transition is already over, however
|
|
16
|
+
* it ended.
|
|
17
|
+
*/
|
|
18
|
+
export declare function awaitExclusiveLockRelease(path: string, timeoutMs: number): void;
|
|
7
19
|
export interface ExclusiveLockOptions {
|
|
8
20
|
/** Grace before a lock that names no owner at all is cleared. */
|
|
9
21
|
staleMs?: number;
|
|
@@ -103,6 +103,35 @@ export function exclusiveLockOwnerPid(path) {
|
|
|
103
103
|
const pid = Number(token.split('.', 1)[0]);
|
|
104
104
|
return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Block until `path` is no longer held by a LIVE other process, or the wait
|
|
108
|
+
* runs out. Unlike `withExclusiveDirectoryLock*` this never CLAIMS the lock —
|
|
109
|
+
* it is for a reader that only needs the holder's transition to be over before
|
|
110
|
+
* it looks at the directory the holder is rewriting.
|
|
111
|
+
*
|
|
112
|
+
* Returns immediately when the lock is free, when its marker names this very
|
|
113
|
+
* process (a caller waiting on its own lock would otherwise deadlock), or when
|
|
114
|
+
* the named owner is gone — a dead holder's transition is already over, however
|
|
115
|
+
* it ended.
|
|
116
|
+
*/
|
|
117
|
+
export function awaitExclusiveLockRelease(path, timeoutMs) {
|
|
118
|
+
const deadline = Date.now() + timeoutMs;
|
|
119
|
+
let pollMs = POLL_MS;
|
|
120
|
+
for (;;) {
|
|
121
|
+
const token = observedMarkerToken(path);
|
|
122
|
+
if (token === null)
|
|
123
|
+
return;
|
|
124
|
+
if (Number(token.split('.', 1)[0]) === process.pid)
|
|
125
|
+
return;
|
|
126
|
+
if (!ownerIsAlive(token))
|
|
127
|
+
return;
|
|
128
|
+
const remaining = deadline - Date.now();
|
|
129
|
+
if (remaining <= 0)
|
|
130
|
+
return;
|
|
131
|
+
pause(Math.min(pollMs, remaining));
|
|
132
|
+
pollMs = nextPollMs(pollMs);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
106
135
|
function tryAcquire(path, staleMs) {
|
|
107
136
|
const token = `${process.pid}.${randomUUID()}`;
|
|
108
137
|
try {
|
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;
|
|
@@ -11,15 +11,46 @@ import { join } from 'node:path';
|
|
|
11
11
|
import { CONFIG_FILE } from '../types.js';
|
|
12
12
|
import { listDirs, pathExists, readJsonIfExists } from './fs-utils.js';
|
|
13
13
|
import { readPluginManifest } from './manifest.js';
|
|
14
|
+
import { awaitBundleSwap, bundleSwapInFlightElsewhere } from './plugin-swap-lock.js';
|
|
14
15
|
function pluginConfigForRoot(root) {
|
|
15
16
|
const cfg = readJsonIfExists(join(root, CONFIG_FILE));
|
|
16
17
|
return cfg && cfg.plugins && typeof cfg.plugins === 'object' ? cfg.plugins : {};
|
|
17
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* A bundle-plugin swap replaces `<plugins>/<name>` with two renames, so there
|
|
21
|
+
* is a window in which the config names a plugin whose directory is not there.
|
|
22
|
+
* A list taken inside that window would drop a plugin that is installed and
|
|
23
|
+
* about to be present again — the caller would build a command tree missing it.
|
|
24
|
+
*
|
|
25
|
+
* That exact signal — named in config, absent on disk — is what we re-check,
|
|
26
|
+
* and only when another process's swap lock says a swap is actually running: a
|
|
27
|
+
* genuinely deleted directory holds no lock and is reported as gone at once.
|
|
28
|
+
* Every normal invocation, where each config-named plugin has its directory,
|
|
29
|
+
* pays one Set lookup per name and nothing else.
|
|
30
|
+
*/
|
|
31
|
+
function swapsToWaitOut(scopeRootPath, cfg, present) {
|
|
32
|
+
const names = Object.keys(cfg);
|
|
33
|
+
if (names.every((name) => present.has(name)))
|
|
34
|
+
return [];
|
|
35
|
+
return names.filter((name) => !present.has(name) && bundleSwapInFlightElsewhere(scopeRootPath, name));
|
|
36
|
+
}
|
|
18
37
|
export function listInstalledPluginsInRoot(scope, scopeRootPath) {
|
|
19
38
|
const dir = join(scopeRootPath, 'plugins');
|
|
20
39
|
if (!pathExists(dir))
|
|
21
40
|
return [];
|
|
22
41
|
const cfg = pluginConfigForRoot(scopeRootPath);
|
|
42
|
+
const first = collectInstalledPlugins(scope, dir, cfg);
|
|
43
|
+
const waiting = swapsToWaitOut(scopeRootPath, cfg, new Set(first.map((plugin) => plugin.name)));
|
|
44
|
+
if (waiting.length === 0)
|
|
45
|
+
return first;
|
|
46
|
+
for (const name of waiting)
|
|
47
|
+
awaitBundleSwap(scopeRootPath, name);
|
|
48
|
+
// One re-list, never a loop: the second read is taken after every swap we
|
|
49
|
+
// observed has finished. A swap that starts after it is a package this
|
|
50
|
+
// invocation was never going to see anyway.
|
|
51
|
+
return collectInstalledPlugins(scope, dir, cfg);
|
|
52
|
+
}
|
|
53
|
+
function collectInstalledPlugins(scope, dir, cfg) {
|
|
23
54
|
const out = [];
|
|
24
55
|
for (const name of listDirs(dir)) {
|
|
25
56
|
const root = join(dir, name);
|
|
@@ -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
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RootDef } from './command.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run `argv` against `root`, and if the backend answers `manifest_stale`,
|
|
4
|
+
* refetch that plugin's package and run the ORIGINAL argv again against a
|
|
5
|
+
* freshly resolved tree — exactly once.
|
|
6
|
+
*
|
|
7
|
+
* Re-parsing the original argv, rather than re-running the leaf that failed, is
|
|
8
|
+
* what makes a RENAMED verb recoverable: the old leaf no longer exists in the
|
|
9
|
+
* refreshed tree, and only the dispatcher can answer that against the fresh
|
|
10
|
+
* tree's own siblings.
|
|
11
|
+
*
|
|
12
|
+
* Never throws: every outcome is rendered through the dispatcher's own
|
|
13
|
+
* `handle`, so a caller can mark the end of dispatch unconditionally.
|
|
14
|
+
*/
|
|
15
|
+
export declare function dispatchWithStaleRecovery(root: RootDef, argv: string[]): Promise<void>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Dispatch with recovery from an out-of-date command description.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from the cli entry so the whole behaviour — rethrow, refetch,
|
|
4
|
+
// re-parse, exactly-once — can be driven by a test through `runCli` and a real
|
|
5
|
+
// error off the wire, rather than only by launching the binary.
|
|
6
|
+
import { resolveRoot } from '../build-root.js';
|
|
7
|
+
import { runCli } from './command.js';
|
|
8
|
+
import { forceRevalidateBundlePlugin } from './command-plugins/revalidate.js';
|
|
9
|
+
import { handle } from './io.js';
|
|
10
|
+
import { staleManifestPlugin } from './manifest-stale.js';
|
|
11
|
+
/**
|
|
12
|
+
* Run `argv` against `root`, and if the backend answers `manifest_stale`,
|
|
13
|
+
* refetch that plugin's package and run the ORIGINAL argv again against a
|
|
14
|
+
* freshly resolved tree — exactly once.
|
|
15
|
+
*
|
|
16
|
+
* Re-parsing the original argv, rather than re-running the leaf that failed, is
|
|
17
|
+
* what makes a RENAMED verb recoverable: the old leaf no longer exists in the
|
|
18
|
+
* refreshed tree, and only the dispatcher can answer that against the fresh
|
|
19
|
+
* tree's own siblings.
|
|
20
|
+
*
|
|
21
|
+
* Never throws: every outcome is rendered through the dispatcher's own
|
|
22
|
+
* `handle`, so a caller can mark the end of dispatch unconditionally.
|
|
23
|
+
*/
|
|
24
|
+
export async function dispatchWithStaleRecovery(root, argv) {
|
|
25
|
+
try {
|
|
26
|
+
// The dispatcher renders every other failure itself; a `manifest_stale`
|
|
27
|
+
// error is the one it rethrows, because recovery needs this frame.
|
|
28
|
+
await runCli(root, argv);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (!(await refetchStalePlugin(error))) {
|
|
33
|
+
// Nothing newer to run against: report the backend's own complaint
|
|
34
|
+
// exactly as the dispatcher would have.
|
|
35
|
+
handle(error);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
// The refreshed tree, re-resolved: the package this invocation dispatches
|
|
40
|
+
// against is not the one it was built from. Replaying the argv is only
|
|
41
|
+
// safe because a server that sets the flag has promised it — the two
|
|
42
|
+
// preconditions are recorded on `ManifestStaleEnvelope` in
|
|
43
|
+
// `src/api/plugin-manifest-schema.ts`, and crtr cannot check either one.
|
|
44
|
+
await runCli(await resolveRoot(argv[2]), argv);
|
|
45
|
+
}
|
|
46
|
+
catch (retryError) {
|
|
47
|
+
// Exactly once. A second `manifest_stale` against a package just
|
|
48
|
+
// refetched is the server's answer, not another refresh cycle.
|
|
49
|
+
handle(retryError);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A backend that answers `manifest_stale` is saying this client invoked from an
|
|
55
|
+
* out-of-date description of its commands. Refetch that plugin's package and
|
|
56
|
+
* report whether it actually changed. An unchanged package means the operation
|
|
57
|
+
* is genuinely gone, so the server's own error, which already names what it
|
|
58
|
+
* does serve, is the honest answer and stands.
|
|
59
|
+
*/
|
|
60
|
+
async function refetchStalePlugin(error) {
|
|
61
|
+
const plugin = staleManifestPlugin(error);
|
|
62
|
+
if (plugin === undefined)
|
|
63
|
+
return false;
|
|
64
|
+
try {
|
|
65
|
+
return await forceRevalidateBundlePlugin(plugin);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// The refetch is the recovery. If it fails there is nothing newer to retry
|
|
69
|
+
// against, and the server's original complaint is what the caller needs.
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ManifestStaleEnvelope } from '../api/plugin-manifest-schema.js';
|
|
2
|
+
/** What crtr attaches to the `CrtrError` it raises for a stale answer. */
|
|
3
|
+
type StaleErrorDetails = Required<ManifestStaleEnvelope> & {
|
|
4
|
+
plugin: string;
|
|
5
|
+
};
|
|
6
|
+
/** Did the backend's own error envelope claim staleness? Reads the parsed JSON
|
|
7
|
+
* body, before it becomes a `CrtrError`. */
|
|
8
|
+
export declare function envelopeClaimsStale(envelope: Record<string, unknown>): boolean;
|
|
9
|
+
/** The details a transport adds when it converts a stale envelope into the
|
|
10
|
+
* error the dispatcher rethrows. */
|
|
11
|
+
export declare function staleErrorDetails(plugin: string): StaleErrorDetails;
|
|
12
|
+
/** Is this the one error class the dispatcher rethrows instead of rendering? */
|
|
13
|
+
export declare function isManifestStaleError(error: unknown): boolean;
|
|
14
|
+
/** The plugin to refetch, or `undefined` when this error is not a recoverable
|
|
15
|
+
* stale answer. A stale flag with no plugin name is not actionable: nothing
|
|
16
|
+
* identifies the package whose description went out of date. */
|
|
17
|
+
export declare function staleManifestPlugin(error: unknown): string | undefined;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// The one place in crtr that names the `manifest_stale` wire field.
|
|
2
|
+
//
|
|
3
|
+
// The field itself is declared in the published format subpath
|
|
4
|
+
// (`src/api/plugin-manifest-schema.ts`), which is what a plugin backend compiles
|
|
5
|
+
// against. This module binds crtr's readers and writer to that declaration, so
|
|
6
|
+
// renaming the field there fails the build here rather than silently splitting
|
|
7
|
+
// the two sides: a backend that keeps setting a field nobody reads, and a client
|
|
8
|
+
// that never recovers.
|
|
9
|
+
import { CrtrError } from './errors.js';
|
|
10
|
+
/** Bound to the published declaration: `keyof` is the field's literal type, so
|
|
11
|
+
* a rename over there makes this assignment a type error. */
|
|
12
|
+
const STALE_FIELD = 'manifest_stale';
|
|
13
|
+
/** The plugin whose package produced the out-of-date description, carried
|
|
14
|
+
* alongside the flag so the recovery frame knows what to refetch. The backend
|
|
15
|
+
* does not send this — the transport that made the call names it. */
|
|
16
|
+
const PLUGIN_FIELD = 'plugin';
|
|
17
|
+
/** Did the backend's own error envelope claim staleness? Reads the parsed JSON
|
|
18
|
+
* body, before it becomes a `CrtrError`. */
|
|
19
|
+
export function envelopeClaimsStale(envelope) {
|
|
20
|
+
return envelope[STALE_FIELD] === true;
|
|
21
|
+
}
|
|
22
|
+
/** The details a transport adds when it converts a stale envelope into the
|
|
23
|
+
* error the dispatcher rethrows. */
|
|
24
|
+
export function staleErrorDetails(plugin) {
|
|
25
|
+
return { [STALE_FIELD]: true, [PLUGIN_FIELD]: plugin };
|
|
26
|
+
}
|
|
27
|
+
/** Is this the one error class the dispatcher rethrows instead of rendering? */
|
|
28
|
+
export function isManifestStaleError(error) {
|
|
29
|
+
return error instanceof CrtrError && error.details?.[STALE_FIELD] === true;
|
|
30
|
+
}
|
|
31
|
+
/** The plugin to refetch, or `undefined` when this error is not a recoverable
|
|
32
|
+
* stale answer. A stale flag with no plugin name is not actionable: nothing
|
|
33
|
+
* identifies the package whose description went out of date. */
|
|
34
|
+
export function staleManifestPlugin(error) {
|
|
35
|
+
if (!isManifestStaleError(error))
|
|
36
|
+
return undefined;
|
|
37
|
+
const plugin = error.details?.[PLUGIN_FIELD];
|
|
38
|
+
return typeof plugin === 'string' ? plugin : undefined;
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Under the scope root's `tmp/`, alongside the staging and `.prev` dirs the
|
|
2
|
+
* swap itself uses, so one directory holds the whole transition. */
|
|
3
|
+
export declare function bundleSwapLockPath(scopeRootPath: string, name: string): string;
|
|
4
|
+
/** Whether ANOTHER live process currently holds this plugin's swap lock. Our
|
|
5
|
+
* own lock does not count: the swapper reads the plugin list from inside its
|
|
6
|
+
* own critical section. */
|
|
7
|
+
export declare function bundleSwapInFlightElsewhere(scopeRootPath: string, name: string): boolean;
|
|
8
|
+
/** Wait out another process's swap of this plugin, bounded. */
|
|
9
|
+
export declare function awaitBundleSwap(scopeRootPath: string, name: string): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The one name for the lock a bundle-plugin package swap is held under, and the
|
|
2
|
+
// two questions a reader asks about it.
|
|
3
|
+
//
|
|
4
|
+
// Two modules need it and neither may own it: the swapper lives in
|
|
5
|
+
// `commands/pkg/plugin-manage.ts` (the whole command graph), the reader in
|
|
6
|
+
// `core/installed-plugins.ts` (deliberately leaf-safe: types, fs-utils,
|
|
7
|
+
// manifest). This module keeps both dependency-light — `node:path` plus the
|
|
8
|
+
// generic lock primitive, which itself imports only `node:fs` and
|
|
9
|
+
// `node:crypto`.
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { awaitExclusiveLockRelease, exclusiveLockOwnerPid } from './exclusive-lock.js';
|
|
12
|
+
/** How long a reader waits for another process's swap to finish before it
|
|
13
|
+
* reports what it can see. A swap is one rename of an already-staged, already-
|
|
14
|
+
* validated tree, so a wait this long means the holder is wedged, not slow. */
|
|
15
|
+
const READER_WAIT_MS = 2_000;
|
|
16
|
+
/** Under the scope root's `tmp/`, alongside the staging and `.prev` dirs the
|
|
17
|
+
* swap itself uses, so one directory holds the whole transition. */
|
|
18
|
+
export function bundleSwapLockPath(scopeRootPath, name) {
|
|
19
|
+
return join(scopeRootPath, 'tmp', `${name}.swap.lock`);
|
|
20
|
+
}
|
|
21
|
+
/** Whether ANOTHER live process currently holds this plugin's swap lock. Our
|
|
22
|
+
* own lock does not count: the swapper reads the plugin list from inside its
|
|
23
|
+
* own critical section. */
|
|
24
|
+
export function bundleSwapInFlightElsewhere(scopeRootPath, name) {
|
|
25
|
+
const pid = exclusiveLockOwnerPid(bundleSwapLockPath(scopeRootPath, name));
|
|
26
|
+
return pid !== null && pid !== process.pid;
|
|
27
|
+
}
|
|
28
|
+
/** Wait out another process's swap of this plugin, bounded. */
|
|
29
|
+
export function awaitBundleSwap(scopeRootPath, name) {
|
|
30
|
+
awaitExclusiveLockRelease(bundleSwapLockPath(scopeRootPath, name), READER_WAIT_MS);
|
|
31
|
+
}
|
|
@@ -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. */
|
|
@@ -12,6 +12,12 @@ export function bindBrokerSession(nodeId, request) {
|
|
|
12
12
|
export function settleBroker(nodeId, request) {
|
|
13
13
|
return daemonClient().settleBroker(nodeId, request);
|
|
14
14
|
}
|
|
15
|
+
export function completeBrokerPark(nodeId, request) {
|
|
16
|
+
return daemonClient().completeBrokerPark(nodeId, request);
|
|
17
|
+
}
|
|
18
|
+
export function recordBrokerParkActivity(nodeId) {
|
|
19
|
+
return daemonClient().recordBrokerParkActivity(nodeId);
|
|
20
|
+
}
|
|
15
21
|
export function advanceBrokerInboxCursor(nodeId, request) {
|
|
16
22
|
return daemonClient().advanceBrokerInboxCursor(nodeId, request);
|
|
17
23
|
}
|
|
@@ -10,9 +10,7 @@ import type { RebindDriver } from './rebind.js';
|
|
|
10
10
|
import type { ToolGroupTracker } from './tool-groups.js';
|
|
11
11
|
import type { PromptDispatchTracker } from './turn-ignition.js';
|
|
12
12
|
import type { TurnAdmissionGate } from './turn-admission.js';
|
|
13
|
-
/** The frames that drive the live engine.
|
|
14
|
-
* provenance through admission because only the resolved route can decide
|
|
15
|
-
* whether the resulting run is internal. */
|
|
13
|
+
/** The frames that drive the live engine. */
|
|
16
14
|
type DeliveredInput = {
|
|
17
15
|
delivery?: {
|
|
18
16
|
origin?: MessageOrigin;
|
|
@@ -25,6 +23,7 @@ export type DriveFrame = (Extract<ClientToBroker, {
|
|
|
25
23
|
}>;
|
|
26
24
|
export type EngineDriver = (client: BrokerClient, frame: DriveFrame) => Promise<unknown> | undefined;
|
|
27
25
|
export declare function createEngineDriver(deps: {
|
|
26
|
+
nodeId: string;
|
|
28
27
|
registry: BrokerClientRegistry;
|
|
29
28
|
replies: FrameReplies;
|
|
30
29
|
session: () => BrokerSession;
|