@north-light/crouter 0.3.269 → 0.3.271

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 (78) hide show
  1. package/dist/api/dto/broker.d.ts +11 -2
  2. package/dist/api/dto/broker.js +2 -2
  3. package/dist/api/dto/nodes.d.ts +4 -0
  4. package/dist/commands/pkg/plugin-inspect.js +2 -2
  5. package/dist/commands/pkg/plugin-manage.js +3 -3
  6. package/dist/commands/sys/doctor.js +4 -3
  7. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +2 -2
  8. package/dist/core/__tests__/helpers/harness.js +1 -0
  9. package/dist/core/__tests__/human-deliver.test.js +1 -1
  10. package/dist/core/__tests__/integration/lifecycle-hooks.test.d.ts +1 -0
  11. package/dist/core/__tests__/integration/lifecycle-hooks.test.js +182 -0
  12. package/dist/core/__tests__/integration/revive.test.js +15 -15
  13. package/dist/core/__tests__/push-final-guard.test.js +1 -1
  14. package/dist/core/__tests__/revive-capacity.test.js +20 -20
  15. package/dist/core/__tests__/revive-parked-fresh.test.js +14 -14
  16. package/dist/core/__tests__/seam/broker-cap-freeze.test.js +2 -2
  17. package/dist/core/__tests__/seam/broker-provider-retry.test.js +2 -0
  18. package/dist/core/__tests__/seam/dormancy-release.test.js +7 -7
  19. package/dist/core/__tests__/seam/held-deferred-human-prompt.test.js +1 -1
  20. package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +2 -2
  21. package/dist/core/command-hooks/discovery.d.ts +27 -1
  22. package/dist/core/command-hooks/discovery.js +52 -0
  23. package/dist/core/command-hooks/index.d.ts +4 -3
  24. package/dist/core/command-hooks/index.js +2 -1
  25. package/dist/core/command-hooks/lifecycle-catalog.d.ts +3 -0
  26. package/dist/core/command-hooks/lifecycle-catalog.js +4 -0
  27. package/dist/core/command-hooks/report.d.ts +11 -2
  28. package/dist/core/command-hooks/report.js +10 -1
  29. package/dist/core/command-hooks/schema.d.ts +16 -1
  30. package/dist/core/command-hooks/schema.js +128 -40
  31. package/dist/core/command-hooks/transport/exec-lifecycle.d.ts +12 -0
  32. package/dist/core/command-hooks/transport/exec-lifecycle.js +152 -0
  33. package/dist/core/human/feedback-companion.js +1 -1
  34. package/dist/core/review/realize.js +1 -1
  35. package/dist/core/runtime/broker/engine-drive.d.ts +14 -3
  36. package/dist/core/runtime/broker/engine-drive.js +29 -1
  37. package/dist/core/runtime/broker/frame-client.js +3 -4
  38. package/dist/core/runtime/broker/rebind.js +7 -0
  39. package/dist/core/runtime/broker-protocol.d.ts +11 -2
  40. package/dist/core/runtime/broker.d.ts +1 -1
  41. package/dist/core/runtime/broker.js +13 -3
  42. package/dist/core/runtime/fleet.d.ts +5 -6
  43. package/dist/core/runtime/node-read.d.ts +2 -0
  44. package/dist/core/runtime/node-read.js +18 -9
  45. package/dist/core/runtime/nodes.js +6 -1
  46. package/dist/core/runtime/revive-all.d.ts +1 -1
  47. package/dist/core/runtime/revive-all.js +2 -2
  48. package/dist/core/runtime/revive.d.ts +2 -2
  49. package/dist/core/runtime/revive.js +49 -13
  50. package/dist/core/runtime/session-visibility.d.ts +10 -14
  51. package/dist/core/runtime/session-visibility.js +43 -30
  52. package/dist/core/runtime/stamp/channel.d.ts +10 -6
  53. package/dist/core/runtime/stamp/channel.js +16 -7
  54. package/dist/core/runtime/stamp/protocol.d.ts +4 -0
  55. package/dist/core/runtime/turn-visibility.d.ts +10 -0
  56. package/dist/core/runtime/turn-visibility.js +31 -0
  57. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +2 -2
  58. package/dist/daemon/api/bridge.js +1 -1
  59. package/dist/daemon/api/handlers/attach.js +4 -4
  60. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  61. package/dist/daemon/api/handlers/messages.js +2 -2
  62. package/dist/daemon/api/handlers/nodes.js +7 -4
  63. package/dist/daemon/cron/sinks.js +3 -3
  64. package/dist/daemon/fleet.js +10 -22
  65. package/dist/daemon/messaging/node-message.js +1 -1
  66. package/dist/daemon/profile-delete.js +1 -1
  67. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +6 -6
  68. package/dist/hook-authoring.d.ts +31 -1
  69. package/dist/hook-authoring.js +42 -5
  70. package/dist/index.d.ts +1 -0
  71. package/dist/index.js +3 -0
  72. package/dist/pi-extensions/canvas-stamp.js +3 -3
  73. package/dist/shared/env.d.ts +2 -0
  74. package/dist/shared/env.js +4 -0
  75. package/dist/shared/generated-context.d.ts +1 -2
  76. package/dist/shared/generated-context.js +3 -4
  77. package/package.json +1 -1
  78. package/runtime.lock.json +2 -2
@@ -83,9 +83,9 @@ export class FreezeLaneReconciler {
83
83
  const id = entry.row.node_id;
84
84
  try {
85
85
  if (entry.enact === 'thaw')
86
- return this.enactThaw(entry.row);
86
+ return await this.enactThaw(entry.row);
87
87
  if (entry.enact === 'idle-release')
88
- return this.enactIdleRelease(now, entry.row);
88
+ return await this.enactIdleRelease(now, entry.row);
89
89
  // The SAME policy table an exit event runs, so tick recovery and exit
90
90
  // recovery can never diverge. It relaunches through
91
91
  // `reviveNode(capacity: 'freeze')`, which freezes the row when the ledger
@@ -106,17 +106,17 @@ export class FreezeLaneReconciler {
106
106
  return 'skipped';
107
107
  }
108
108
  }
109
- enactThaw(row) {
109
+ async enactThaw(row) {
110
110
  if (row.lifecycle === 'resident' && getNode(row.node_id)?.pi_session_id != null && !hasPendingWake(row.node_id)) {
111
111
  transition(row.node_id, 'release');
112
112
  clearFrozen(row.node_id);
113
113
  return 'skipped';
114
114
  }
115
- return reviveNode(row.node_id, { resume: true, capacity: 'freeze', recovery: true }).outcome === 'frozen'
115
+ return (await reviveNode(row.node_id, { resume: true, capacity: 'freeze', recovery: true })).outcome === 'frozen'
116
116
  ? 'frozen'
117
117
  : 'launched';
118
118
  }
119
- enactIdleRelease(now, row) {
119
+ async enactIdleRelease(now, row) {
120
120
  const id = row.node_id;
121
121
  const cursorNow = readCursor(id);
122
122
  const prior = this.relaunchState.get(id);
@@ -147,7 +147,7 @@ export class FreezeLaneReconciler {
147
147
  // A relaunch the broker cap froze got no slot and therefore proved nothing
148
148
  // about this delivery, so it must not spend an attempt: only a launch that
149
149
  // actually happened advances the episode toward exhaustion.
150
- if (reviveNode(id, { resume: true, capacity: 'freeze' }).outcome === 'frozen')
150
+ if ((await reviveNode(id, { resume: true, capacity: 'freeze' })).outcome === 'frozen')
151
151
  return 'frozen';
152
152
  this.relaunchState.set(id, { cursor: cursorNow, attempts: attempts + 1, lastAttempt: now });
153
153
  emitEvent({
@@ -6,6 +6,23 @@ export interface HookContext {
6
6
  cwd: string;
7
7
  providedParams: string[];
8
8
  }
9
+ export interface LifecycleHookNode {
10
+ id: string;
11
+ name: string;
12
+ kind: string;
13
+ mode: string;
14
+ lifecycle: string;
15
+ cwd: string;
16
+ nodeDir: string;
17
+ contextDir: string;
18
+ jobDir: string;
19
+ reportsDir: string;
20
+ }
21
+ export interface LifecycleHookRuntime {
22
+ isBirth: boolean;
23
+ canvasHome: string;
24
+ profile: string | null;
25
+ }
9
26
  interface HookRequestBase {
10
27
  protocolVersion: 1;
11
28
  op: string;
@@ -26,7 +43,20 @@ export interface AfterHookRequest extends HookRequestBase {
26
43
  export interface ReplaceHookRequest extends HookRequestBase {
27
44
  phase: 'replace';
28
45
  }
29
- export type HookRequest = BeforeHookRequest | AfterHookRequest | ReplaceHookRequest;
46
+ /** Lifecycle handlers run on every matching event. They must be idempotent because `node:start` fires for birth and every revive. */
47
+ export interface LifecycleHookRequest {
48
+ protocolVersion: 1;
49
+ op: string;
50
+ event: 'node:start';
51
+ phase: 'on';
52
+ operationId: string;
53
+ node: LifecycleHookNode;
54
+ runtime: LifecycleHookRuntime;
55
+ context: {
56
+ cwd: string;
57
+ };
58
+ }
59
+ export type HookRequest = BeforeHookRequest | AfterHookRequest | ReplaceHookRequest | LifecycleHookRequest;
30
60
  export interface HookSuccess {
31
61
  protocolVersion: 1;
32
62
  ok: true;
@@ -239,18 +239,40 @@ function concatBytes(chunks, total) {
239
239
  function validateRequest(value) {
240
240
  if (!isRecord(value))
241
241
  return invalid('request must be an object');
242
- if (!hasOnlyKeys(value, ['protocolVersion', 'op', 'phase', 'command', 'operationId', 'input', 'context', 'result'])) {
243
- return invalid('request contains unknown fields');
244
- }
245
242
  if (value['protocolVersion'] !== 1)
246
243
  return invalid('unsupported hook protocol version');
247
244
  if (typeof value['op'] !== 'string' || value['op'] === '')
248
245
  return invalid('op must be a non-empty string');
249
- if (!isStringArray(value['command']) || value['command'].length === 0)
250
- return invalid('command must be a non-empty string array');
251
246
  if (typeof value['operationId'] !== 'string' || !/^[0-9a-f]{32}$/.test(value['operationId'])) {
252
247
  return invalid('operationId must be a 32-character lowercase hexadecimal string');
253
248
  }
249
+ if (value['phase'] === 'on') {
250
+ if (!hasOnlyKeys(value, ['protocolVersion', 'op', 'event', 'phase', 'operationId', 'node', 'runtime', 'context']))
251
+ return invalid('lifecycle request contains unknown fields');
252
+ if (value['event'] !== 'node:start')
253
+ return invalid('unsupported lifecycle event');
254
+ if (!isLifecycleNode(value['node']))
255
+ return invalid('lifecycle request node contains invalid fields');
256
+ if (!isLifecycleRuntime(value['runtime']))
257
+ return invalid('lifecycle request runtime contains invalid fields');
258
+ if (!isLifecycleContext(value['context']))
259
+ return invalid('lifecycle request context must contain cwd');
260
+ return valid({
261
+ protocolVersion: 1,
262
+ op: value['op'],
263
+ event: 'node:start',
264
+ phase: 'on',
265
+ operationId: value['operationId'],
266
+ node: value['node'],
267
+ runtime: value['runtime'],
268
+ context: value['context'],
269
+ });
270
+ }
271
+ if (!hasOnlyKeys(value, ['protocolVersion', 'op', 'phase', 'command', 'operationId', 'input', 'context', 'result'])) {
272
+ return invalid('request contains unknown fields');
273
+ }
274
+ if (!isStringArray(value['command']) || value['command'].length === 0)
275
+ return invalid('command must be a non-empty string array');
254
276
  if (!isRecord(value['input']) || !isJson(value['input']))
255
277
  return invalid('input must be a JSON object');
256
278
  if (!isContext(value['context']))
@@ -317,6 +339,21 @@ function isContext(value) {
317
339
  && typeof value['cwd'] === 'string'
318
340
  && isStringArray(value['providedParams']);
319
341
  }
342
+ function isLifecycleContext(value) {
343
+ return isRecord(value) && hasOnlyKeys(value, ['cwd']) && typeof value['cwd'] === 'string';
344
+ }
345
+ function isLifecycleNode(value) {
346
+ return isRecord(value)
347
+ && hasOnlyKeys(value, ['id', 'name', 'kind', 'mode', 'lifecycle', 'cwd', 'nodeDir', 'contextDir', 'jobDir', 'reportsDir'])
348
+ && ['id', 'name', 'kind', 'mode', 'lifecycle', 'cwd', 'nodeDir', 'contextDir', 'jobDir', 'reportsDir'].every((field) => typeof value[field] === 'string');
349
+ }
350
+ function isLifecycleRuntime(value) {
351
+ return isRecord(value)
352
+ && hasOnlyKeys(value, ['isBirth', 'canvasHome', 'profile'])
353
+ && typeof value['isBirth'] === 'boolean'
354
+ && typeof value['canvasHome'] === 'string'
355
+ && (typeof value['profile'] === 'string' || value['profile'] === null);
356
+ }
320
357
  function isHookError(value) {
321
358
  return isRecord(value)
322
359
  && hasOnlyKeys(value, ['code', 'message', 'field', 'next'])
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export type { CloseNodeResult } from './core/runtime/close.js';
5
5
  export { appendInbox } from './core/feed/inbox.js';
6
6
  export type { InboxEntry, InboxTier, InboxKind } from './core/feed/inbox.js';
7
7
  export { appendSituationalContext } from './core/runtime/situational-context.js';
8
+ export { isTurnInternal } from './core/runtime/turn-visibility.js';
8
9
  export { getNode, listNodes } from './core/canvas/canvas.js';
9
10
  export { nodeDir } from './core/canvas/paths.js';
10
11
  export type { NodeMeta, NodeRow, NodeStatus, Lifecycle, Mode, } from './core/canvas/types.js';
package/dist/index.js CHANGED
@@ -21,6 +21,9 @@ export { appendInbox } from './core/feed/inbox.js';
21
21
  // upsert-only, narrow surface so an external composer (e.g. Hearth) never needs to
22
22
  // learn the storage file itself.
23
23
  export { appendSituationalContext } from './core/runtime/situational-context.js';
24
+ // In-process extensions use crouter's admitted turn classification rather than
25
+ // attempting to reconstruct origin and route state independently.
26
+ export { isTurnInternal } from './core/runtime/turn-visibility.js';
24
27
  // ── Canvas reads ─────────────────────────────────────────────────────────
25
28
  export { getNode, listNodes } from './core/canvas/canvas.js';
26
29
  export { nodeDir } from './core/canvas/paths.js';
@@ -18,7 +18,7 @@
18
18
  // join is by `msgTs`, so tree position carries no meaning here.
19
19
  import { envNodeId } from '../shared/env.js';
20
20
  import { crtrVersion } from '../shared/crtr-version.js';
21
- import { claimMessageOrigin, drainStampExt, hasPendingStampExt, } from '../core/runtime/stamp/channel.js';
21
+ import { claimMessageAnnouncement, drainStampExt, hasPendingStampExt, } from '../core/runtime/stamp/channel.js';
22
22
  import { CRTR_STAMP_CUSTOM_TYPE, latestSessionStamp, STAMP_SCHEMA_VERSION, } from '../core/runtime/stamp/protocol.js';
23
23
  /** Plain text of a message's content (string or content blocks). */
24
24
  function messageText(content) {
@@ -61,10 +61,10 @@ export function registerCanvasStamp(pi) {
61
61
  return;
62
62
  const msgTs = message.timestamp;
63
63
  if (message.role === 'user') {
64
- const origin = claimMessageOrigin(messageText(message.content));
64
+ const announcement = claimMessageAnnouncement(messageText(message.content));
65
65
  pi.appendEntry(CRTR_STAMP_CUSTOM_TYPE, envelope('message', {
66
66
  msgTs,
67
- ...(origin === null ? {} : { origin }),
67
+ ...(announcement === null ? {} : announcement),
68
68
  }));
69
69
  return;
70
70
  }
@@ -15,6 +15,8 @@ export declare function envModelExact(): boolean;
15
15
  * only when a caller needs the override itself — to check whether one is
16
16
  * set, or to propagate it verbatim into a child process's env. */
17
17
  export declare function envHomeOverride(): string | undefined;
18
+ /** The raw installed-runtime directory override, unresolved. */
19
+ export declare function envRuntimeHomeOverride(): string | undefined;
18
20
  /** Dead-provider watchdog timeout (`CRTR_STREAM_WATCHDOG_MS`), 5 minutes by
19
21
  * default. See `runtime/stream-watchdog.ts` for what it guards. */
20
22
  export declare function envStreamWatchdogMs(): number;
@@ -51,6 +51,10 @@ export function envModelExact() {
51
51
  export function envHomeOverride() {
52
52
  return process.env['CRTR_HOME'];
53
53
  }
54
+ /** The raw installed-runtime directory override, unresolved. */
55
+ export function envRuntimeHomeOverride() {
56
+ return process.env['CRTR_RUNTIME_HOME'];
57
+ }
54
58
  // Tuning
55
59
  /** Generic `Number(raw)` parse with a positive-finite guard, shared by every
56
60
  * millisecond tuning var below (each previously reimplemented this locally,
@@ -11,8 +11,7 @@ export declare const STALL_REPROMPT: string;
11
11
  /** The daemon's parking mandate: the last turn of a conversation the unattended
12
12
  * clock is concluding. Delivered live, wrapped in a `park` runtime card. It
13
13
  * governs both the node's durable inheritance and reader-facing output:
14
- * one update for subscribers and history, then a silent stop with no assistant
15
- * prose for an external channel to relay.
14
+ * one update for subscribers and history, then a brief completion reply.
16
15
  *
17
16
  * The durable inheritance shares a CONTRACT with `node yield`'s pre-invocation
18
17
  * guide (roadmap current, short and shrinking; context dir for in-progress
@@ -16,8 +16,7 @@ export const STALL_REPROMPT = "You've stopped but you're not waiting on anyone a
16
16
  /** The daemon's parking mandate: the last turn of a conversation the unattended
17
17
  * clock is concluding. Delivered live, wrapped in a `park` runtime card. It
18
18
  * governs both the node's durable inheritance and reader-facing output:
19
- * one update for subscribers and history, then a silent stop with no assistant
20
- * prose for an external channel to relay.
19
+ * one update for subscribers and history, then a brief completion reply.
21
20
  *
22
21
  * The durable inheritance shares a CONTRACT with `node yield`'s pre-invocation
23
22
  * guide (roadmap current, short and shrinking; context dir for in-progress
@@ -28,8 +27,8 @@ export const PARK_SUMMARY_PROMPT = 'This conversation has been idle with nothing
28
27
  + '2. Rewrite `$CRTR_CONTEXT_DIR/roadmap.md` for a fresh context window. This is the only handoff document a later fresh cycle receives in full. Preserve the current goal and exit criteria when they still apply, then state the present outcome; what remains or is blocked; decisions or questions still open; exact recovery handles for in-flight state; and the first safe move on return. Keep strategy and present state, not a transcript recap. Delete stale and completed steps instead of marking them done; the roadmap should stay short and shrink. Write a minimal one now if none exists.\n\n'
29
28
  + '3. Put supporting material in your context directory only when the roadmap would become bulky without it. Rewrite existing living documents rather than leave superseded versions. Name every supporting file the next cycle must read from the roadmap and say what it is for—the revive shows filenames but does not inject their contents. Task state, identifiers, and recovery detail belong here, not in memory.\n\n'
30
29
  + '4. Use memory only for a non-obvious, reusable lesson that should survive this task and is not already recorded. Read `crtr memory write -h`, find before writing, and choose the narrowest scope that will reach the next agent who needs it. Do not put a conversation recap, task status, recovery handles, or facts already captured in code or docs into memory.\n\n'
31
- + '5. Push exactly one regular update with `crtr push update --tier deferred`, never `crtr push final`. Write it for subscribers and history, not as a second roadmap. Its first line must stand alone as the current outcome, blocker, or decision that matters; then include only unfinished work, a needed decision, and concrete handles a subscriber may need. This concludes the conversation; it does not finish the mandate.\n\n'
32
- + '6. Keep this entire turn silent in the conversation: produce no assistant prose before, between, or after tool calls, and do not narrate the work. Once every required tool call has finished, end the response immediately without emitting any text, sign-off, summary, acknowledgement, or other visible content by emitting the `stop` token with no text. The deferred update from step 5 is the only reader-facing conclusion. A later message reopens you on a fresh context window grounded in your goal and roadmap, so the inheritance you leave now is what you get back.';
30
+ + '5. Push exactly one regular update with `crtr push update --tier deferred`, never `crtr push final`. Write for someone who has not seen this conversation: name the work in plain terms, say where it stands, and say what remains or is blocked. Put the current outcome in the first line; include only needed decisions and recovery handles. This concludes the conversation, not the mandate.\n\n'
31
+ + '6. When finished, reply with exactly `done`.';
33
32
  /** Static recovery prompts shared by the broker producer and display classifier. */
34
33
  export const AUTH_FAULT_RECOVERY_BODY = 'Provider credentials were just updated (a new login landed). Your previous turn stopped on a provider authentication failure. Continue from where you left off and retry the work that failed.';
35
34
  export const CONNECTION_FAULT_RECOVERY_BODY = 'The network connection is back online. Your previous turn stopped on a connection error (the network was down). Continue from where you left off and retry the work that failed.';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.269",
3
+ "version": "0.3.271",
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.269",
3
+ "version": "0.3.271",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.269",
9
+ "version": "0.3.271",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {