@north-light/crouter 0.3.258 → 0.3.260
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/dto/attach.d.ts +6 -0
- package/dist/api/dto/nodes.d.ts +15 -0
- package/dist/clients/attach/command.js +3 -1
- package/dist/clients/attach/input/capabilities.d.ts +3 -0
- package/dist/clients/attach/input/capabilities.js +7 -3
- package/dist/clients/attach/input/controller.d.ts +1 -1
- package/dist/clients/attach/input/controller.js +31 -17
- package/dist/clients/attach/session/connection.d.ts +12 -3
- package/dist/clients/attach/session/connection.js +40 -25
- package/dist/clients/attach/session/input-wiring.d.ts +1 -1
- package/dist/clients/attach/session/input-wiring.js +6 -2
- package/dist/clients/attach/session/reconnect.d.ts +7 -3
- package/dist/clients/attach/session/reconnect.js +13 -2
- package/dist/clients/attach/slash/dispatch.js +1 -1
- package/dist/clients/attach/viewer.d.ts +1 -1
- package/dist/clients/attach/viewer.js +742 -742
- package/dist/commands/pkg/plugin-inspect.js +2 -1
- package/dist/commands/surface/node/cycle.js +2 -2
- package/dist/commands/surface/node/focus.js +11 -11
- package/dist/commands/surface/node/placement.js +14 -5
- package/dist/commands/sys/doctor.d.ts +35 -0
- package/dist/commands/sys/doctor.js +22 -2
- package/dist/core/__tests__/integration/command-plugins.test.js +317 -2
- package/dist/core/broker-client/client.d.ts +4 -5
- package/dist/core/broker-client/client.js +9 -5
- package/dist/core/canvas/browse/model.d.ts +3 -3
- package/dist/core/canvas/browse/model.js +3 -3
- package/dist/core/canvas/browse/render.js +3 -3
- package/dist/core/command-manifests/manifest.d.ts +5 -3
- package/dist/core/command-manifests/manifest.js +20 -31
- package/dist/core/command-manifests/registry.d.ts +3 -0
- package/dist/core/command-manifests/schema.d.ts +12 -2
- package/dist/core/command-manifests/schema.js +24 -11
- package/dist/core/command-plugins/compose.js +1 -1
- package/dist/core/command-plugins/discovery.d.ts +1 -1
- package/dist/core/command-plugins/discovery.js +10 -4
- package/dist/core/command-plugins/extensions.d.ts +20 -0
- package/dist/core/command-plugins/extensions.js +166 -0
- package/dist/core/command-plugins/transport/exec-invoke.d.ts +15 -2
- package/dist/core/command-plugins/transport/exec-invoke.js +11 -7
- package/dist/core/help.d.ts +5 -0
- package/dist/core/help.js +3 -0
- package/dist/core/runtime/revive.d.ts +19 -6
- package/dist/core/runtime/revive.js +34 -9
- package/dist/daemon/api/handlers/attach.js +24 -20
- package/dist/daemon/api/handlers/nodes.js +7 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
package/dist/core/help.d.ts
CHANGED
|
@@ -198,6 +198,11 @@ export interface BranchHelp {
|
|
|
198
198
|
/** Parent-level listing assembled by defineBranch from the actual child defs.
|
|
199
199
|
* renderBranch reads this; never author it by hand. */
|
|
200
200
|
listing?: ListingChild[];
|
|
201
|
+
/** A rejected repository fragment is visible only on its owning extensible branch. */
|
|
202
|
+
extensionIssue?: {
|
|
203
|
+
fragment: string;
|
|
204
|
+
message: string;
|
|
205
|
+
};
|
|
201
206
|
}
|
|
202
207
|
/** Viewer-only hint about how a leaf's result should preview in the attach
|
|
203
208
|
* viewer's chat — never part of `-h` output and never reaches agent-facing
|
package/dist/core/help.js
CHANGED
|
@@ -161,6 +161,9 @@ export function renderBranch(h) {
|
|
|
161
161
|
lines.push('');
|
|
162
162
|
lines.push(`<subcommand name="${c.name}" description="${attr(c.description)}" whenToUse="${attr(c.whenToUse)}"${subs}/>`);
|
|
163
163
|
}
|
|
164
|
+
if (h.extensionIssue !== undefined) {
|
|
165
|
+
lines.push(`<extension-issue fragment="${attr(h.extensionIssue.fragment)}">This repository's contributed commands were rejected: ${h.extensionIssue.message}. Run \`crtr sys doctor\` for the full report.</extension-issue>`);
|
|
166
|
+
}
|
|
164
167
|
lines.push('</command>');
|
|
165
168
|
return lines.join('\n');
|
|
166
169
|
}
|
|
@@ -57,6 +57,18 @@ export interface ReviveResult {
|
|
|
57
57
|
outcome: ReviveOutcome;
|
|
58
58
|
}
|
|
59
59
|
export type ReviveOutcome = 'launched' | 'already-live' | 'frozen';
|
|
60
|
+
export type AttachReviveDecision = {
|
|
61
|
+
parked: true;
|
|
62
|
+
} | {
|
|
63
|
+
parked: false;
|
|
64
|
+
result: ReviveResult;
|
|
65
|
+
};
|
|
66
|
+
interface ReviveOptions {
|
|
67
|
+
resume: boolean;
|
|
68
|
+
capacity: ReviveCapacityPolicy;
|
|
69
|
+
wakeReason?: ReviveWakeReason;
|
|
70
|
+
recovery?: boolean;
|
|
71
|
+
}
|
|
60
72
|
/** What a caller wants when the broker cap is full. `freeze` suits a caller
|
|
61
73
|
* whose reason to run survives waiting (durable mail, a cron sink, a recovery
|
|
62
74
|
* respawn): the row is marked frozen and the tick relaunches it when a slot
|
|
@@ -75,9 +87,10 @@ export type ReviveCapacityPolicy = 'freeze' | 'refuse';
|
|
|
75
87
|
* Throws if the node does not exist. All other failures propagate as-is —
|
|
76
88
|
* callers (daemon, command) decide how to handle.
|
|
77
89
|
*/
|
|
78
|
-
export declare function reviveNode(nodeId: string, opts:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
export declare function reviveNode(nodeId: string, opts: ReviveOptions): ReviveResult;
|
|
91
|
+
/** The attach-specific entry to the revive boundary. A focus viewer may decline
|
|
92
|
+
* to wake an exact parked resident while preserving the ordinary activating
|
|
93
|
+
* attach default. The fleet check and parked decision are synchronous with the
|
|
94
|
+
* launch below, so an already-running broker wins before dormancy is observed. */
|
|
95
|
+
export declare function decideAttachRevive(nodeId: string, opts: ReviveOptions, wakeParked: boolean): AttachReviveDecision;
|
|
96
|
+
export {};
|
|
@@ -140,6 +140,19 @@ export function hasFreshGroundState(nodeId) {
|
|
|
140
140
|
* callers (daemon, command) decide how to handle.
|
|
141
141
|
*/
|
|
142
142
|
export function reviveNode(nodeId, opts) {
|
|
143
|
+
const decision = decideRevive(nodeId, opts, true);
|
|
144
|
+
if (decision.parked)
|
|
145
|
+
throw new Error(`reviveNode: internal parked decision for activating revive ${nodeId}`);
|
|
146
|
+
return decision.result;
|
|
147
|
+
}
|
|
148
|
+
/** The attach-specific entry to the revive boundary. A focus viewer may decline
|
|
149
|
+
* to wake an exact parked resident while preserving the ordinary activating
|
|
150
|
+
* attach default. The fleet check and parked decision are synchronous with the
|
|
151
|
+
* launch below, so an already-running broker wins before dormancy is observed. */
|
|
152
|
+
export function decideAttachRevive(nodeId, opts, wakeParked) {
|
|
153
|
+
return decideRevive(nodeId, opts, wakeParked);
|
|
154
|
+
}
|
|
155
|
+
function decideRevive(nodeId, opts, wakeParked) {
|
|
143
156
|
let meta = getNode(nodeId);
|
|
144
157
|
if (meta === null) {
|
|
145
158
|
throw new Error(`reviveNode: unknown node ${nodeId}`);
|
|
@@ -170,12 +183,21 @@ export function reviveNode(nodeId, opts) {
|
|
|
170
183
|
// so this is a no-op for a row that is already active.
|
|
171
184
|
transition(nodeId, meta.intent === 'refresh' ? 'refresh-launch' : 'revive');
|
|
172
185
|
return {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
186
|
+
parked: false,
|
|
187
|
+
result: {
|
|
188
|
+
window: null,
|
|
189
|
+
session: meta.tmux_session ?? null,
|
|
190
|
+
resumed: false,
|
|
191
|
+
outcome: 'already-live',
|
|
192
|
+
},
|
|
177
193
|
};
|
|
178
194
|
}
|
|
195
|
+
// Focus may establish a viewport without establishing an engine. This exact
|
|
196
|
+
// row has no final latch and no fleet owner; returning before reservation is
|
|
197
|
+
// what keeps lifecycle, cycle count, pid, inbox cursor, and session untouched.
|
|
198
|
+
if (!wakeParked && meta.lifecycle === 'resident' && isParked(meta)) {
|
|
199
|
+
return { parked: true };
|
|
200
|
+
}
|
|
179
201
|
// The cap, consulted BEFORE any mutation below: a node that cannot get a
|
|
180
202
|
// broker must not have its cycle counter bumped, its session identity
|
|
181
203
|
// cleared, or its bearings drained on the way to being turned away.
|
|
@@ -203,14 +225,17 @@ export function reviveNode(nodeId, opts) {
|
|
|
203
225
|
});
|
|
204
226
|
}
|
|
205
227
|
return {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
228
|
+
parked: false,
|
|
229
|
+
result: {
|
|
230
|
+
window: null,
|
|
231
|
+
session: meta.tmux_session ?? null,
|
|
232
|
+
resumed: false,
|
|
233
|
+
outcome: 'frozen',
|
|
234
|
+
},
|
|
210
235
|
};
|
|
211
236
|
}
|
|
212
237
|
try {
|
|
213
|
-
return launchRevive(nodeId, meta, opts);
|
|
238
|
+
return { parked: false, result: launchRevive(nodeId, meta, opts) };
|
|
214
239
|
}
|
|
215
240
|
finally {
|
|
216
241
|
// The slot is either consumed by the fleet registration inside `launch`
|
|
@@ -6,53 +6,57 @@
|
|
|
6
6
|
//
|
|
7
7
|
// `{id}` is the target node being attached to.
|
|
8
8
|
import { getNode } from '../../../core/canvas/canvas.js';
|
|
9
|
-
import {
|
|
9
|
+
import { decideAttachRevive } from '../../../core/runtime/revive.js';
|
|
10
10
|
import { viewSocketPath } from '../../../core/canvas/paths.js';
|
|
11
11
|
import { waitForBrokerViewSocket } from '../../../core/runtime/placement-tmux.js';
|
|
12
12
|
import { notFound, usage } from '../../../core/errors.js';
|
|
13
|
-
/** Validate + narrow the attach-ensure body (
|
|
13
|
+
/** Validate + narrow the attach-ensure body (all fields optional, default true). */
|
|
14
14
|
function parseAttachBody(body) {
|
|
15
15
|
if (body === undefined || body === null)
|
|
16
|
-
return { resume: true, revive: true };
|
|
16
|
+
return { resume: true, revive: true, wakeParked: true };
|
|
17
17
|
if (typeof body !== 'object') {
|
|
18
|
-
throw usage('attach body must be a JSON object { resume?, revive? }');
|
|
18
|
+
throw usage('attach body must be a JSON object { resume?, revive?, wakeParked? }');
|
|
19
19
|
}
|
|
20
20
|
const resume = body.resume;
|
|
21
21
|
const revive = body.revive;
|
|
22
|
+
const wakeParked = body.wakeParked;
|
|
22
23
|
if (resume !== undefined && typeof resume !== 'boolean')
|
|
23
24
|
throw usage('resume must be a boolean');
|
|
24
25
|
if (revive !== undefined && typeof revive !== 'boolean')
|
|
25
26
|
throw usage('revive must be a boolean');
|
|
26
|
-
|
|
27
|
+
if (wakeParked !== undefined && typeof wakeParked !== 'boolean')
|
|
28
|
+
throw usage('wakeParked must be a boolean');
|
|
29
|
+
return { resume: resume ?? true, revive: revive ?? true, wakeParked: wakeParked ?? true };
|
|
27
30
|
}
|
|
28
31
|
async function handleAttach(ctx) {
|
|
29
32
|
const id = ctx.params['id'];
|
|
30
33
|
if (getNode(id) === null)
|
|
31
34
|
throw notFound(`unknown node: ${id}`, { received: id });
|
|
32
|
-
const { resume, revive } = parseAttachBody(ctx.body);
|
|
35
|
+
const { resume, revive, wakeParked } = parseAttachBody(ctx.body);
|
|
33
36
|
let revived = false;
|
|
34
37
|
let resumed = false;
|
|
38
|
+
let parked = false;
|
|
35
39
|
if (revive) {
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
await waitForBrokerViewSocket(id);
|
|
40
|
+
// This decision and reviveNode's fleet guard share one synchronous launch
|
|
41
|
+
// boundary. Focus may stop at an exact parked resident; ordinary/direct
|
|
42
|
+
// attach keeps wakeParked=true and preserves attach-implies-revive.
|
|
43
|
+
const decision = decideAttachRevive(id, { resume, capacity: 'refuse' }, wakeParked);
|
|
44
|
+
parked = decision.parked;
|
|
45
|
+
if (!decision.parked) {
|
|
46
|
+
revived = decision.result.launch !== undefined;
|
|
47
|
+
resumed = decision.result.resumed;
|
|
48
|
+
// If we launched, let the broker publish its view socket so the local
|
|
49
|
+
// viewer's immediate connect finds it (bounded wait; best-effort).
|
|
50
|
+
if (revived)
|
|
51
|
+
await waitForBrokerViewSocket(id);
|
|
52
|
+
}
|
|
50
53
|
}
|
|
51
54
|
const body = {
|
|
52
55
|
node_id: id,
|
|
53
56
|
socket_path: viewSocketPath(id),
|
|
54
57
|
revived,
|
|
55
58
|
resumed,
|
|
59
|
+
parked,
|
|
56
60
|
};
|
|
57
61
|
return { status: 200, body };
|
|
58
62
|
}
|
|
@@ -366,8 +366,15 @@ async function handleSnapshot(ctx) {
|
|
|
366
366
|
node_id: id,
|
|
367
367
|
snapshot: {
|
|
368
368
|
messages: read.snapshot.messages,
|
|
369
|
+
...(read.snapshot.messageIds === undefined ? {} : { messageIds: read.snapshot.messageIds }),
|
|
369
370
|
stats: read.snapshot.stats,
|
|
370
371
|
state: read.snapshot.state,
|
|
372
|
+
display: read.snapshot.display,
|
|
373
|
+
...(read.snapshot.queued === undefined ? {} : { queued: read.snapshot.queued }),
|
|
374
|
+
...(read.snapshot.toolGroupSummaries === undefined
|
|
375
|
+
? {}
|
|
376
|
+
: { toolGroupSummaries: read.snapshot.toolGroupSummaries }),
|
|
377
|
+
...(read.snapshot.workingActivity === undefined ? {} : { workingActivity: read.snapshot.workingActivity }),
|
|
371
378
|
},
|
|
372
379
|
commands: read.commands.map((c) => ({ name: c.name, description: c.description, source: c.source })),
|
|
373
380
|
captured_at: nowIso(),
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.260",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.260",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|