@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.
Files changed (48) hide show
  1. package/dist/api/dto/attach.d.ts +6 -0
  2. package/dist/api/dto/nodes.d.ts +15 -0
  3. package/dist/clients/attach/command.js +3 -1
  4. package/dist/clients/attach/input/capabilities.d.ts +3 -0
  5. package/dist/clients/attach/input/capabilities.js +7 -3
  6. package/dist/clients/attach/input/controller.d.ts +1 -1
  7. package/dist/clients/attach/input/controller.js +31 -17
  8. package/dist/clients/attach/session/connection.d.ts +12 -3
  9. package/dist/clients/attach/session/connection.js +40 -25
  10. package/dist/clients/attach/session/input-wiring.d.ts +1 -1
  11. package/dist/clients/attach/session/input-wiring.js +6 -2
  12. package/dist/clients/attach/session/reconnect.d.ts +7 -3
  13. package/dist/clients/attach/session/reconnect.js +13 -2
  14. package/dist/clients/attach/slash/dispatch.js +1 -1
  15. package/dist/clients/attach/viewer.d.ts +1 -1
  16. package/dist/clients/attach/viewer.js +742 -742
  17. package/dist/commands/pkg/plugin-inspect.js +2 -1
  18. package/dist/commands/surface/node/cycle.js +2 -2
  19. package/dist/commands/surface/node/focus.js +11 -11
  20. package/dist/commands/surface/node/placement.js +14 -5
  21. package/dist/commands/sys/doctor.d.ts +35 -0
  22. package/dist/commands/sys/doctor.js +22 -2
  23. package/dist/core/__tests__/integration/command-plugins.test.js +317 -2
  24. package/dist/core/broker-client/client.d.ts +4 -5
  25. package/dist/core/broker-client/client.js +9 -5
  26. package/dist/core/canvas/browse/model.d.ts +3 -3
  27. package/dist/core/canvas/browse/model.js +3 -3
  28. package/dist/core/canvas/browse/render.js +3 -3
  29. package/dist/core/command-manifests/manifest.d.ts +5 -3
  30. package/dist/core/command-manifests/manifest.js +20 -31
  31. package/dist/core/command-manifests/registry.d.ts +3 -0
  32. package/dist/core/command-manifests/schema.d.ts +12 -2
  33. package/dist/core/command-manifests/schema.js +24 -11
  34. package/dist/core/command-plugins/compose.js +1 -1
  35. package/dist/core/command-plugins/discovery.d.ts +1 -1
  36. package/dist/core/command-plugins/discovery.js +10 -4
  37. package/dist/core/command-plugins/extensions.d.ts +20 -0
  38. package/dist/core/command-plugins/extensions.js +166 -0
  39. package/dist/core/command-plugins/transport/exec-invoke.d.ts +15 -2
  40. package/dist/core/command-plugins/transport/exec-invoke.js +11 -7
  41. package/dist/core/help.d.ts +5 -0
  42. package/dist/core/help.js +3 -0
  43. package/dist/core/runtime/revive.d.ts +19 -6
  44. package/dist/core/runtime/revive.js +34 -9
  45. package/dist/daemon/api/handlers/attach.js +24 -20
  46. package/dist/daemon/api/handlers/nodes.js +7 -0
  47. package/package.json +1 -1
  48. package/runtime.lock.json +2 -2
@@ -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
- resume: boolean;
80
- capacity: ReviveCapacityPolicy;
81
- wakeReason?: ReviveWakeReason;
82
- recovery?: boolean;
83
- }): ReviveResult;
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
- window: null,
174
- session: meta.tmux_session ?? null,
175
- resumed: false,
176
- outcome: 'already-live',
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
- window: null,
207
- session: meta.tmux_session ?? null,
208
- resumed: false,
209
- outcome: 'frozen',
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 { reviveNode } from '../../../core/runtime/revive.js';
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 (both fields optional, default true). */
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
- return { resume: resume ?? true, revive: revive ?? true };
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
- // reviveNode is the ONLY sanctioned broker launcher; a double-revive of a
37
- // live node is an idempotent no-op (no `launch` handle) — `revived` reflects
38
- // whether a fresh engine actually started.
39
- // An attach has a viewer waiting on a socket that only a live broker
40
- // publishes, so there is nothing to wait for it with: refuse at the cap and
41
- // let the caller see why, rather than freezing and returning a `revived`
42
- // that never becomes true.
43
- const result = reviveNode(id, { resume, capacity: 'refuse' });
44
- revived = result.launch !== undefined;
45
- resumed = result.resumed;
46
- // If we launched, let the broker publish its view socket so the local
47
- // viewer's immediate connect finds it (bounded wait; best-effort).
48
- if (revived)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.258",
3
+ "version": "0.3.260",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.258",
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.258",
9
+ "version": "0.3.260",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {