@north-light/crouter 0.3.191 → 0.3.193
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 +31 -10
- package/dist/api/client.js +83 -24
- package/dist/api/dto/bash-jobs.d.ts +22 -0
- package/dist/api/dto/bash-jobs.js +9 -0
- package/dist/api/dto/human.d.ts +3 -4
- package/dist/api/dto/inbox.d.ts +106 -65
- package/dist/api/dto/inbox.js +2 -8
- 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-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/serial/provider-rotation.test.ts +101 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +6 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +68 -14
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +87 -13
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +2 -2
- package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
- package/dist/clients/attach/chrome/canvas-panels.d.ts +1 -1
- package/dist/clients/attach/chrome/canvas-panels.js +1 -1
- package/dist/clients/attach/input/overlay-owner.js +1 -1
- package/dist/clients/attach/overlays/auth.js +34 -8
- package/dist/clients/attach/overlays/pickers.d.ts +5 -8
- package/dist/clients/attach/overlays/pickers.js +37 -43
- package/dist/clients/attach/viewer.js +643 -620
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +38 -128
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +72 -148
- package/dist/clients/inbox/controller.d.ts +2 -3
- package/dist/clients/inbox/controller.js +41 -42
- package/dist/clients/inbox/page-adapter.d.ts +33 -0
- package/dist/clients/inbox/page-adapter.js +54 -0
- package/dist/clients/inbox/resolve.d.ts +2 -2
- package/dist/clients/inbox/resolve.js +1 -1
- package/dist/clients/inbox/review-visibility.d.ts +1 -1
- package/dist/clients/inbox/review-visibility.js +1 -1
- package/dist/clients/inbox/tui/ansi.d.ts +1 -1
- package/dist/clients/inbox/tui/ansi.js +2 -2
- package/dist/clients/inbox/tui/input.d.ts +1 -3
- package/dist/clients/inbox/tui/input.js +242 -553
- package/dist/clients/inbox/tui/panel.d.ts +3 -1
- package/dist/clients/inbox/tui/panel.js +59 -187
- package/dist/clients/inbox/tui/render.d.ts +0 -25
- package/dist/clients/inbox/tui/render.js +165 -416
- package/dist/clients/inbox/tui/slots.d.ts +32 -0
- package/dist/clients/inbox/tui/slots.js +156 -0
- package/dist/clients/inbox/tui/types.d.ts +25 -13
- package/dist/clients/inbox/tui.js +5 -3
- package/dist/commands/attention.js +2 -2
- package/dist/commands/human/doc.js +66 -20
- package/dist/commands/human/prompts.d.ts +3 -3
- package/dist/commands/human/prompts.js +158 -112
- package/dist/commands/human/queue.d.ts +0 -1
- package/dist/commands/human/queue.js +34 -94
- package/dist/commands/human/shared.d.ts +10 -4
- package/dist/commands/human/shared.js +62 -13
- package/dist/commands/human.js +8 -4
- package/dist/commands/memory/read.js +13 -5
- package/dist/commands/node/bash.js +15 -28
- package/dist/commands/surface/node/focus.js +1 -1
- package/dist/core/__tests__/human-deliver.test.js +40 -46
- package/dist/core/__tests__/seam/broker-attach-stream.test.js +7 -1
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +40 -31
- package/dist/core/__tests__/serial/spawn-root.test.js +3 -1
- package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +3 -1
- package/dist/core/auth-interaction.d.ts +2 -2
- package/dist/core/bash-jobs.d.ts +32 -0
- package/dist/core/bash-jobs.js +72 -1
- package/dist/core/canvas/__tests__/attention.test.js +16 -10
- package/dist/core/canvas/__tests__/render-remote.test.js +9 -3
- package/dist/core/canvas/attention.js +1 -1
- package/dist/core/canvas/browse/app.js +1 -1
- package/dist/core/canvas/nav-model.d.ts +1 -1
- package/dist/core/canvas/nav-model.js +1 -1
- package/dist/core/canvas/remote-canvas-source.js +1 -1
- package/dist/core/config.js +4 -1
- package/dist/core/human/__tests__/page-render.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-render.test.js +49 -0
- package/dist/core/human/__tests__/page-tickets.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-tickets.test.js +112 -0
- package/dist/core/human/__tests__/page.test.d.ts +1 -0
- package/dist/core/human/__tests__/page.test.js +139 -0
- package/dist/core/human/__tests__/serial/inbox-core.test.js +42 -32
- package/dist/core/human/claim.js +2 -2
- package/dist/core/human/convention.d.ts +4 -4
- package/dist/core/human/convention.js +6 -13
- package/dist/core/human/markdown-html.d.ts +8 -0
- package/dist/core/human/markdown-html.js +581 -0
- package/dist/core/human/page-catalog.d.ts +6 -0
- package/dist/core/human/page-catalog.js +43 -0
- package/dist/core/human/page-render.d.ts +3 -0
- package/dist/core/human/page-render.js +131 -0
- package/dist/core/human/page-schema.d.ts +312 -0
- package/dist/core/human/page-schema.js +291 -0
- package/dist/core/human/page-synth.d.ts +9 -0
- package/dist/core/human/page-synth.js +17 -0
- package/dist/core/human/page.d.ts +22 -0
- package/dist/core/human/page.js +427 -0
- package/dist/core/human/review-schema.d.ts +19 -0
- package/dist/core/human/review-schema.js +34 -0
- package/dist/core/human/scan.d.ts +12 -3
- package/dist/core/human/scan.js +69 -26
- package/dist/core/human/summary.d.ts +5 -6
- package/dist/core/human/summary.js +54 -37
- package/dist/core/human/tickets.d.ts +61 -11
- package/dist/core/human/tickets.js +100 -57
- package/dist/core/human/types.d.ts +21 -66
- package/dist/core/human/types.js +1 -5
- package/dist/core/preview-registry.js +0 -1
- package/dist/core/review/ticket-filter.d.ts +1 -1
- package/dist/core/review/ticket-filter.js +2 -8
- package/dist/core/runtime/broker/read-ops.js +3 -0
- package/dist/core/runtime/broker.js +19 -10
- package/dist/core/runtime/package-health.js +1 -1
- package/dist/core/termrender/termrender.d.ts +1 -1
- package/dist/core/termrender/termrender.js +2 -2
- package/dist/core/tui/page-host.d.ts +1 -1
- package/dist/core/tui/page-host.js +2 -2
- package/dist/core/user-settings.d.ts +19 -0
- package/dist/core/user-settings.js +22 -0
- package/dist/daemon/api/handlers/bash-jobs.d.ts +2 -0
- package/dist/daemon/api/handlers/bash-jobs.js +89 -0
- package/dist/daemon/api/handlers/human.js +16 -14
- package/dist/daemon/api/handlers/inbox.js +311 -227
- package/dist/daemon/api/router.d.ts +5 -1
- package/dist/daemon/api/router.js +5 -0
- package/dist/daemon/api/server.js +2 -0
- package/dist/daemon/cron-run.js +23 -10
- package/dist/daemon/human/finish.d.ts +8 -3
- package/dist/daemon/human/finish.js +35 -15
- package/dist/pages/bundle.css +1 -0
- package/dist/pages/bundle.js +1347 -0
- package/dist/pages/comments.d.ts +34 -0
- package/dist/pages/comments.js +72 -0
- package/dist/pages/elements/cards.d.ts +17 -0
- package/dist/pages/elements/cards.js +785 -0
- package/dist/pages/elements/chart.d.ts +1 -0
- package/dist/pages/elements/chart.js +648 -0
- package/dist/pages/elements/options.d.ts +21 -0
- package/dist/pages/elements/options.js +604 -0
- package/dist/pages/elements/pages.d.ts +24 -0
- package/dist/pages/elements/pages.js +355 -0
- package/dist/pages/elements/slot.d.ts +21 -0
- package/dist/pages/elements/slot.js +105 -0
- package/dist/pages/elements/table.d.ts +19 -0
- package/dist/pages/elements/table.js +906 -0
- package/dist/pages/elements/text.d.ts +25 -0
- package/dist/pages/elements/text.js +951 -0
- package/dist/pages/entry.d.ts +8 -0
- package/dist/pages/entry.js +8 -0
- package/dist/pages/host.d.ts +132 -0
- package/dist/pages/host.js +149 -0
- package/dist/pages/register.d.ts +2 -0
- package/dist/pages/register.js +3 -0
- package/dist/pages/slot-config.d.ts +36 -0
- package/dist/pages/slot-config.js +50 -0
- package/dist/pages/types.d.ts +98 -0
- package/dist/pages/types.js +23 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +13 -5
- package/dist/pi-extensions/canvas-structured-output.js +7 -3
- package/dist/types.d.ts +4 -0
- package/dist/types.js +2 -0
- package/package.json +8 -8
- package/runtime.lock.json +156 -101
- package/dist/clients/inbox/deck-adapter.d.ts +0 -29
- package/dist/clients/inbox/deck-adapter.js +0 -63
- package/dist/core/human/deck-factories.d.ts +0 -8
- package/dist/core/human/deck-factories.js +0 -15
- package/dist/core/human/deck-schema.d.ts +0 -70
- package/dist/core/human/deck-schema.js +0 -92
|
@@ -722,14 +722,20 @@ async function resolveLaunchModel(services, cfg) {
|
|
|
722
722
|
/**
|
|
723
723
|
* Wrap a live ModelRuntime so a model-less `createAgentSessionFromServices`
|
|
724
724
|
* call can't have the SDK's own `findInitialModel` silently pick a model
|
|
725
|
-
* behind crouter's launch policy.
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
725
|
+
* behind crouter's launch policy. Every model-list read the SDK performs during
|
|
726
|
+
* construction must be intercepted:
|
|
727
|
+
* - `getModel()` — findInitialModel step 3 (settingsManager's durable
|
|
728
|
+
* `defaultProvider`/`defaultModel`, persisted into a GLOBAL settings.json by
|
|
729
|
+
* every `session.setModel()` call the broker itself already makes, so this
|
|
730
|
+
* is a live bypass on any machine that has ever picked a model, not a
|
|
731
|
+
* theoretical corner case) and the resume path's model restore.
|
|
732
|
+
* - `getAvailableSnapshot()` — findInitialModel step 4 and
|
|
733
|
+
* restoreModelFromSession's fallback both take the registry default off this
|
|
734
|
+
* SYNCHRONOUS snapshot (model-resolver.js:511/556).
|
|
735
|
+
* - `getAvailable()` — the async refresh-and-list sibling, masked alongside it
|
|
736
|
+
* so no construction-time list read can reach the real registry through
|
|
737
|
+
* either accessor.
|
|
738
|
+
* Masking any one of them alone leaves the others wide open. Everything else
|
|
733
739
|
* (`getModels`, `hasConfiguredAuth`, `refresh`, …) passes through untouched via
|
|
734
740
|
* `Reflect.get`. Used ONLY
|
|
735
741
|
* for the one model-less construction call; the `services` returned to every
|
|
@@ -739,7 +745,7 @@ async function resolveLaunchModel(services, cfg) {
|
|
|
739
745
|
* CONSTRUCTION-ONLY MASK: the SDK stores `options.services.modelRuntime` as the
|
|
740
746
|
* session's private `_modelRuntime` for its ENTIRE lifetime and never replaces
|
|
741
747
|
* it (agent-session-services.js:104-119, sdk.js:253). A permanent mask would
|
|
742
|
-
* leave list_models (
|
|
748
|
+
* leave list_models (the available list→[]), cycle_model, and provider-rotation
|
|
743
749
|
* fallback (getModel()→undefined) broken forever for any model-less-booted node,
|
|
744
750
|
* even after a later /login selects a model. Model selection can only happen
|
|
745
751
|
* DURING construction, so this proxy self-heals: it masks while `masked`, then
|
|
@@ -750,7 +756,10 @@ function modelLessRegistry(real) {
|
|
|
750
756
|
let masked = true;
|
|
751
757
|
const runtime = new Proxy(real, {
|
|
752
758
|
get(target, prop, receiver) {
|
|
753
|
-
// `
|
|
759
|
+
// `getAvailableSnapshot` is the sync list findInitialModel reads; its
|
|
760
|
+
// async sibling `getAvailable` refreshes first and returns the same set.
|
|
761
|
+
if (masked && prop === 'getAvailableSnapshot')
|
|
762
|
+
return () => [];
|
|
754
763
|
if (masked && prop === 'getAvailable')
|
|
755
764
|
return async () => [];
|
|
756
765
|
if (masked && prop === 'getModel')
|
|
@@ -23,7 +23,7 @@ import { expandTilde } from '../fs-utils.js';
|
|
|
23
23
|
export function rendererWarning() {
|
|
24
24
|
if (isRendererReady())
|
|
25
25
|
return null;
|
|
26
|
-
return 'crtr: the pinned termrender renderer is not provisioned —
|
|
26
|
+
return 'crtr: the pinned termrender renderer is not provisioned — pages, reviews, and inline diagrams render as plaintext.\n' +
|
|
27
27
|
' Fix: run `crtr sys setup` (requires `uv`: curl -LsSf https://astral.sh/uv/install.sh | sh).';
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
@@ -41,7 +41,7 @@ export declare function renderMarkdownWithMap(md: string, width: number): Render
|
|
|
41
41
|
* The shared block-aware mapped render: prose blocks keep the readability cap
|
|
42
42
|
* (`proseWidth`), while diagram blocks are re-rendered at the pane's full
|
|
43
43
|
* `paneWidth` and spliced back in by block index. Both surfaces (terminal
|
|
44
|
-
* review and the ask
|
|
44
|
+
* review and the ask page) go through this, so "which blocks may be wide" is
|
|
45
45
|
* decided once, from the renderer's own block types, never by heuristics on
|
|
46
46
|
* the emitted rows.
|
|
47
47
|
*
|
|
@@ -611,7 +611,7 @@ export function renderMarkdownWithMap(md, width) {
|
|
|
611
611
|
* The shared block-aware mapped render: prose blocks keep the readability cap
|
|
612
612
|
* (`proseWidth`), while diagram blocks are re-rendered at the pane's full
|
|
613
613
|
* `paneWidth` and spliced back in by block index. Both surfaces (terminal
|
|
614
|
-
* review and the ask
|
|
614
|
+
* review and the ask page) go through this, so "which blocks may be wide" is
|
|
615
615
|
* decided once, from the renderer's own block types, never by heuristics on
|
|
616
616
|
* the emitted rows.
|
|
617
617
|
*
|
|
@@ -664,7 +664,7 @@ export function renderMarkdownBlockAwareLines(md, proseWidth, paneWidth) {
|
|
|
664
664
|
export function checkMarkdown(md) {
|
|
665
665
|
ensureRenderer();
|
|
666
666
|
// Renderer unavailable → don't block validation; the body just renders as
|
|
667
|
-
// plaintext later. Bricking
|
|
667
|
+
// plaintext later. Bricking page validation here would be the wrong default.
|
|
668
668
|
if (rendererState !== 'ready')
|
|
669
669
|
return { ok: true };
|
|
670
670
|
const result = spawnSync(VENV_BIN, ['doc', 'check'], {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProcessTerminal,
|
|
1
|
+
import { ProcessTerminal, type Component, type Focusable, type TUI } from "@earendil-works/pi-tui";
|
|
2
2
|
import { readRawScopeConfig } from "../config.js";
|
|
3
3
|
import { type BindingId, type BindingResolution } from "../keybindings/index.js";
|
|
4
4
|
/** A full-screen page: a focusable TUI component that cleans up after itself. */
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// restore path that survives an exception, a signal, and process exit. Anything
|
|
7
7
|
// a single page needs beyond that — pi's model runtime, a canvas client — the
|
|
8
8
|
// page's own runner builds and closes over before calling in here.
|
|
9
|
-
import { ProcessTerminal,
|
|
9
|
+
import { ProcessTerminal, TuiMainScreen, } from "@earendil-works/pi-tui";
|
|
10
10
|
import { applyTheme } from "../../clients/attach/config.js";
|
|
11
11
|
import { readRawScopeConfig } from "../config.js";
|
|
12
12
|
import { resolveKeybindings, } from "../keybindings/index.js";
|
|
@@ -20,7 +20,7 @@ export async function runTuiPage(createSurface, interceptCancel) {
|
|
|
20
20
|
const rawUserConfig = readRawScopeConfig("user");
|
|
21
21
|
const bindings = resolveKeybindings(rawUserConfig?.keybindings);
|
|
22
22
|
const terminal = new ProcessTerminal();
|
|
23
|
-
const tui = new
|
|
23
|
+
const tui = new TuiMainScreen(terminal, true);
|
|
24
24
|
return new Promise((resolve, reject) => {
|
|
25
25
|
let surface;
|
|
26
26
|
let restored = false;
|
|
@@ -196,6 +196,21 @@ export declare const CONFIG_SETTINGS_COVERAGE: {
|
|
|
196
196
|
setFromString: (config: ScopeConfig, raw: string) => string | null;
|
|
197
197
|
};
|
|
198
198
|
};
|
|
199
|
+
html_pages: {
|
|
200
|
+
surface: "settings";
|
|
201
|
+
setting: {
|
|
202
|
+
key: "html_pages";
|
|
203
|
+
label: string;
|
|
204
|
+
description: string;
|
|
205
|
+
valueConstraint: string;
|
|
206
|
+
applies: string;
|
|
207
|
+
group: "display";
|
|
208
|
+
order: number;
|
|
209
|
+
format: (config: ScopeConfig) => "on" | "off";
|
|
210
|
+
cycle: (config: ScopeConfig) => void;
|
|
211
|
+
setFromString: (config: ScopeConfig, raw: string) => string | null;
|
|
212
|
+
};
|
|
213
|
+
};
|
|
199
214
|
fold_finished_tools: {
|
|
200
215
|
surface: "settings";
|
|
201
216
|
setting: {
|
|
@@ -265,6 +280,10 @@ export declare const CONFIG_SETTINGS_COVERAGE: {
|
|
|
265
280
|
surface: "specialized";
|
|
266
281
|
owner: string;
|
|
267
282
|
};
|
|
283
|
+
page_components: {
|
|
284
|
+
surface: "specialized";
|
|
285
|
+
owner: string;
|
|
286
|
+
};
|
|
268
287
|
remoteCanvas: {
|
|
269
288
|
surface: "specialized";
|
|
270
289
|
owner: string;
|
|
@@ -254,6 +254,27 @@ export const CONFIG_SETTINGS_COVERAGE = {
|
|
|
254
254
|
},
|
|
255
255
|
},
|
|
256
256
|
},
|
|
257
|
+
html_pages: {
|
|
258
|
+
surface: 'settings',
|
|
259
|
+
setting: {
|
|
260
|
+
key: 'html_pages',
|
|
261
|
+
label: 'HTML pages',
|
|
262
|
+
description: 'Teach agent help the HTML page-authoring dialect. Off keeps page authoring Markdown-only.',
|
|
263
|
+
valueConstraint: 'true|false',
|
|
264
|
+
applies: 'Applies on the next help request.',
|
|
265
|
+
group: 'display',
|
|
266
|
+
order: 70,
|
|
267
|
+
format: (config) => config.html_pages ? 'on' : 'off',
|
|
268
|
+
cycle: (config) => { config.html_pages = !config.html_pages; },
|
|
269
|
+
setFromString: (config, raw) => {
|
|
270
|
+
const value = booleanFromString(raw, 'html_pages');
|
|
271
|
+
if (typeof value === 'string')
|
|
272
|
+
return value;
|
|
273
|
+
config.html_pages = value;
|
|
274
|
+
return null;
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
257
278
|
fold_finished_tools: {
|
|
258
279
|
surface: 'settings',
|
|
259
280
|
setting: {
|
|
@@ -323,6 +344,7 @@ export const CONFIG_SETTINGS_COVERAGE = {
|
|
|
323
344
|
modelRoutes: { surface: 'specialized', owner: 'Models route declarations' },
|
|
324
345
|
modelRouting: { surface: 'specialized', owner: 'Models route declarations' },
|
|
325
346
|
kinds: { surface: 'specialized', owner: 'Models tab and kind registry' },
|
|
347
|
+
page_components: { surface: 'specialized', owner: 'Page component catalog: product-registered page slot kinds' },
|
|
326
348
|
remoteCanvas: { surface: 'specialized', owner: 'canvas config commands' },
|
|
327
349
|
spawnEnv: { surface: 'specialized', owner: 'spawn environment allow-list' },
|
|
328
350
|
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Daemon-owned read and stop surfaces for one node's file-backed background
|
|
2
|
+
// bash jobs. The attach roster continues to read the same directory directly;
|
|
3
|
+
// these routes exist for remote presenters that cannot see the node host.
|
|
4
|
+
import { appendInbox } from '../../../core/feed/inbox.js';
|
|
5
|
+
import { contextDir, isSafeNodeId } from '../../../core/canvas/paths.js';
|
|
6
|
+
import { getNode } from '../../../core/canvas/canvas.js';
|
|
7
|
+
import { isBrokerLive } from '../../../core/runtime/model-swap.js';
|
|
8
|
+
import { assertNotFinalized } from '../../../core/runtime/reopen.js';
|
|
9
|
+
import { reviveNode } from '../../../core/runtime/revive.js';
|
|
10
|
+
import { activeBackgroundBashJobs, bashJobPaths, isBashJobProcessGroupAlive, readJobPgid, isSafeBashJobId as isSafeCoreBashJobId, stopBackgroundBashJob, } from '../../../core/bash-jobs.js';
|
|
11
|
+
import { ApiError } from '../../../api/index.js';
|
|
12
|
+
import { notFound } from '../../../core/errors.js';
|
|
13
|
+
function requireTarget(ctx) {
|
|
14
|
+
const nodeId = ctx.params['id'];
|
|
15
|
+
if (nodeId === undefined || !isSafeNodeId(nodeId)) {
|
|
16
|
+
throw new ApiError(400, 'invalid_node_id', `invalid node id: ${JSON.stringify(nodeId)}`);
|
|
17
|
+
}
|
|
18
|
+
const node = getNode(nodeId);
|
|
19
|
+
if (node === null)
|
|
20
|
+
throw notFound(`unknown node: ${nodeId}`, { received: nodeId });
|
|
21
|
+
const rawJobId = ctx.params['jobId'];
|
|
22
|
+
if (rawJobId !== undefined) {
|
|
23
|
+
if (!isSafeCoreBashJobId(rawJobId)) {
|
|
24
|
+
throw new ApiError(400, 'invalid_job_id', `invalid background job id: ${JSON.stringify(rawJobId)}`);
|
|
25
|
+
}
|
|
26
|
+
return { nodeId, jobId: rawJobId, dir: contextDir(nodeId) };
|
|
27
|
+
}
|
|
28
|
+
return { nodeId, dir: contextDir(nodeId) };
|
|
29
|
+
}
|
|
30
|
+
function handleList(ctx) {
|
|
31
|
+
const { nodeId, dir } = requireTarget(ctx);
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const body = activeBackgroundBashJobs(dir).map((job) => {
|
|
34
|
+
const pgid = readJobPgid(dir, job.jobId) ?? null;
|
|
35
|
+
return {
|
|
36
|
+
job_id: job.jobId,
|
|
37
|
+
command: job.command,
|
|
38
|
+
started_at: new Date(job.startedAtMs).toISOString(),
|
|
39
|
+
elapsed_ms: Math.max(0, now - job.startedAtMs),
|
|
40
|
+
pgid,
|
|
41
|
+
pgid_alive: pgid !== null && isBashJobProcessGroupAlive(pgid),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
return { status: 200, body };
|
|
45
|
+
}
|
|
46
|
+
async function handleStop(ctx) {
|
|
47
|
+
const { nodeId, jobId, dir } = requireTarget(ctx);
|
|
48
|
+
if (jobId === undefined)
|
|
49
|
+
throw new ApiError(400, 'invalid_request', 'a job id is required');
|
|
50
|
+
const stopped = await stopBackgroundBashJob(dir, jobId);
|
|
51
|
+
if (stopped.kind === 'missing') {
|
|
52
|
+
throw new ApiError(404, 'job_not_found', `no background job ${jobId} for ${nodeId}`);
|
|
53
|
+
}
|
|
54
|
+
// A presenter's roster can be a snapshot; a row whose job has since finished
|
|
55
|
+
// (or was already stopped) is gone, not stoppable.
|
|
56
|
+
if (stopped.kind === 'retired') {
|
|
57
|
+
throw new ApiError(404, 'job_not_found', `background job ${jobId} for ${nodeId} is no longer running`, { log_path: stopped.logPath });
|
|
58
|
+
}
|
|
59
|
+
if (stopped.kind === 'not-stoppable') {
|
|
60
|
+
throw new ApiError(409, 'job_not_stoppable', `job ${jobId} recorded no process group`, { log_path: stopped.logPath });
|
|
61
|
+
}
|
|
62
|
+
// Match `crtr node bash kill`: retire the job, then send an urgent durable
|
|
63
|
+
// notice to the owning node. The terminal path sends that notice through the
|
|
64
|
+
// message endpoint, whose finalized-node gate runs before append; preserve
|
|
65
|
+
// that same fail-loud behavior here after the job control-plane mutation.
|
|
66
|
+
assertNotFinalized(nodeId);
|
|
67
|
+
const paths = bashJobPaths(dir, jobId);
|
|
68
|
+
const body = `Background bash job ${jobId} was stopped by the human before it finished. Log: ${paths.jobLog} Command: ${paths.cmdSh}`;
|
|
69
|
+
appendInbox(nodeId, {
|
|
70
|
+
from: null,
|
|
71
|
+
tier: 'urgent',
|
|
72
|
+
kind: 'message',
|
|
73
|
+
label: body.split('\n')[0].slice(0, 120),
|
|
74
|
+
data: { body },
|
|
75
|
+
});
|
|
76
|
+
const node = getNode(nodeId);
|
|
77
|
+
if (node !== null && !isBrokerLive(node)) {
|
|
78
|
+
try {
|
|
79
|
+
reviveNode(nodeId, { resume: true });
|
|
80
|
+
}
|
|
81
|
+
catch { /* the notice remains durable */ }
|
|
82
|
+
}
|
|
83
|
+
const result = { node_id: nodeId, job_id: jobId, signaled: stopped.signaled };
|
|
84
|
+
return { status: 200, body: result };
|
|
85
|
+
}
|
|
86
|
+
export const bashJobRoutes = [
|
|
87
|
+
{ method: 'GET', pattern: '/v1/nodes/:id/jobs', handler: handleList },
|
|
88
|
+
{ method: 'DELETE', pattern: '/v1/nodes/:id/jobs/:jobId', handler: handleStop },
|
|
89
|
+
];
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import { ApiError } from '../../../api/index.js';
|
|
6
6
|
import { isSafeNodeId } from '../../../core/canvas/paths.js';
|
|
7
|
-
import {
|
|
7
|
+
import { pageManifestPath, reviewPath } from '../../../core/human/convention.js';
|
|
8
8
|
import { ticketDir } from '../../../core/human/root.js';
|
|
9
|
-
import { readTicketResult,
|
|
9
|
+
import { readTicketResult, validatePageTicketResponses } from '../../../core/human/tickets.js';
|
|
10
|
+
import { readConfig } from '../../../core/config.js';
|
|
10
11
|
import { getReviewByBridge } from '../../../core/review/store.js';
|
|
11
12
|
import { ReviewOperationError } from '../../../core/review/types.js';
|
|
12
13
|
import { spawnNode } from '../../../core/runtime/nodes.js';
|
|
13
|
-
import { cancelHumanTicket,
|
|
14
|
+
import { cancelHumanTicket, resolvePageTicket } from '../../human/finish.js';
|
|
14
15
|
import { cancelReview } from '../../review/finish.js';
|
|
15
16
|
function requireTicketNodeId(ctx) {
|
|
16
17
|
const nodeId = ctx.params['node_id'];
|
|
@@ -52,7 +53,7 @@ function handleCreateBridge(ctx) {
|
|
|
52
53
|
function reviewSubmitGuidance() {
|
|
53
54
|
return new ApiError(400, 'invalid_request', 'review approvals use POST /v1/human/reviews/:review_id/submit');
|
|
54
55
|
}
|
|
55
|
-
/** The ticket resolve route accepts only a
|
|
56
|
+
/** The ticket resolve route accepts only a page response body. */
|
|
56
57
|
function requireResolveRequest(ctx) {
|
|
57
58
|
const request = ctx.body;
|
|
58
59
|
if (request === undefined || request === null || typeof request !== 'object' || Array.isArray(request)) {
|
|
@@ -66,13 +67,13 @@ function requireResolveRequest(ctx) {
|
|
|
66
67
|
throw reviewSubmitGuidance();
|
|
67
68
|
throw new ApiError(400, 'invalid_request', 'ticket resolve body requires exactly `responses`');
|
|
68
69
|
}
|
|
69
|
-
function
|
|
70
|
-
const
|
|
70
|
+
function requirePageTicket(dir) {
|
|
71
|
+
const page = existsSync(pageManifestPath(dir));
|
|
71
72
|
const review = existsSync(reviewPath(dir));
|
|
72
|
-
if (review && !
|
|
73
|
+
if (review && !page)
|
|
73
74
|
throw reviewSubmitGuidance();
|
|
74
|
-
if (!
|
|
75
|
-
throw new ApiError(400, 'invalid_request', 'ticket has no unambiguous
|
|
75
|
+
if (!page || review) {
|
|
76
|
+
throw new ApiError(400, 'invalid_request', 'ticket has no unambiguous page kind');
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
function invalidResolveRequest(error) {
|
|
@@ -91,13 +92,14 @@ async function handleResolve(ctx) {
|
|
|
91
92
|
const nodeId = requireTicketNodeId(ctx);
|
|
92
93
|
const request = requireResolveRequest(ctx);
|
|
93
94
|
const dir = ticketDir(nodeId);
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
requirePageTicket(dir);
|
|
96
|
+
const productKinds = readConfig('user').page_components ?? [];
|
|
97
|
+
const responses = preflightResolve(() => validatePageTicketResponses(dir, request.responses, productKinds));
|
|
98
|
+
await resolvePageTicket(nodeId, responses, productKinds);
|
|
99
|
+
return { status: 200, body: { delivered: true, kind: 'page' } };
|
|
98
100
|
}
|
|
99
101
|
function hasPublishedTicket(dir) {
|
|
100
|
-
return existsSync(
|
|
102
|
+
return existsSync(pageManifestPath(dir)) || existsSync(reviewPath(dir)) || readTicketResult(dir) !== null;
|
|
101
103
|
}
|
|
102
104
|
async function handleCancel(ctx) {
|
|
103
105
|
const nodeId = requireTicketNodeId(ctx);
|