@north-light/crouter 0.3.192 → 0.3.194

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 (190) hide show
  1. package/dist/api/client.d.ts +31 -10
  2. package/dist/api/client.js +83 -24
  3. package/dist/api/dto/bash-jobs.d.ts +24 -0
  4. package/dist/api/dto/bash-jobs.js +9 -0
  5. package/dist/api/dto/human.d.ts +3 -4
  6. package/dist/api/dto/inbox.d.ts +106 -65
  7. package/dist/api/dto/inbox.js +2 -8
  8. package/dist/api/index.d.ts +1 -0
  9. package/dist/api/index.js +1 -0
  10. package/dist/api/routes.d.ts +7 -0
  11. package/dist/api/routes.js +7 -0
  12. package/dist/builtin-memory/02-lifecycle/01-resident.md +1 -1
  13. package/dist/builtin-memory/internal/examples/imessage-assistant.md +1 -1
  14. package/dist/builtin-memory/internal/nodes-and-canvas.md +2 -2
  15. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  16. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/serial/provider-rotation.test.ts +101 -3
  17. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +6 -2
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +68 -14
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +87 -13
  20. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +2 -2
  21. package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
  22. package/dist/clients/attach/chrome/canvas-panels.d.ts +1 -1
  23. package/dist/clients/attach/chrome/canvas-panels.js +1 -1
  24. package/dist/clients/attach/input/overlay-owner.js +1 -1
  25. package/dist/clients/attach/overlays/auth.js +34 -8
  26. package/dist/clients/attach/overlays/pickers.d.ts +5 -8
  27. package/dist/clients/attach/overlays/pickers.js +37 -43
  28. package/dist/clients/attach/viewer.js +643 -620
  29. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +38 -128
  30. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +72 -148
  31. package/dist/clients/inbox/controller.d.ts +2 -3
  32. package/dist/clients/inbox/controller.js +41 -42
  33. package/dist/clients/inbox/page-adapter.d.ts +33 -0
  34. package/dist/clients/inbox/page-adapter.js +54 -0
  35. package/dist/clients/inbox/resolve.d.ts +2 -2
  36. package/dist/clients/inbox/resolve.js +1 -1
  37. package/dist/clients/inbox/review-visibility.d.ts +1 -1
  38. package/dist/clients/inbox/review-visibility.js +1 -1
  39. package/dist/clients/inbox/tui/ansi.d.ts +1 -1
  40. package/dist/clients/inbox/tui/ansi.js +2 -2
  41. package/dist/clients/inbox/tui/input.d.ts +1 -3
  42. package/dist/clients/inbox/tui/input.js +242 -553
  43. package/dist/clients/inbox/tui/panel.d.ts +3 -1
  44. package/dist/clients/inbox/tui/panel.js +59 -187
  45. package/dist/clients/inbox/tui/render.d.ts +0 -25
  46. package/dist/clients/inbox/tui/render.js +165 -416
  47. package/dist/clients/inbox/tui/slots.d.ts +32 -0
  48. package/dist/clients/inbox/tui/slots.js +156 -0
  49. package/dist/clients/inbox/tui/types.d.ts +25 -13
  50. package/dist/clients/inbox/tui.js +5 -3
  51. package/dist/commands/attention.js +2 -2
  52. package/dist/commands/human/doc.js +66 -20
  53. package/dist/commands/human/prompts.d.ts +3 -3
  54. package/dist/commands/human/prompts.js +158 -112
  55. package/dist/commands/human/queue.d.ts +0 -1
  56. package/dist/commands/human/queue.js +34 -94
  57. package/dist/commands/human/shared.d.ts +10 -4
  58. package/dist/commands/human/shared.js +62 -13
  59. package/dist/commands/human.js +8 -4
  60. package/dist/commands/memory/read.js +13 -5
  61. package/dist/commands/node/bash.js +18 -30
  62. package/dist/commands/node/lifecycle.js +2 -2
  63. package/dist/commands/surface/node/focus.js +1 -1
  64. package/dist/core/__tests__/human-deliver.test.js +40 -46
  65. package/dist/core/__tests__/seam/broker-attach-stream.test.js +7 -1
  66. package/dist/core/__tests__/seam/dormancy-release.test.js +64 -10
  67. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +40 -31
  68. package/dist/core/__tests__/serial/spawn-root.test.js +3 -1
  69. package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +3 -1
  70. package/dist/core/auth-interaction.d.ts +2 -2
  71. package/dist/core/bash-jobs.d.ts +43 -0
  72. package/dist/core/bash-jobs.js +96 -2
  73. package/dist/core/canvas/__tests__/attention.test.js +16 -10
  74. package/dist/core/canvas/__tests__/render-remote.test.js +9 -3
  75. package/dist/core/canvas/attention.js +1 -1
  76. package/dist/core/canvas/browse/app.js +1 -1
  77. package/dist/core/canvas/nav-model.d.ts +1 -1
  78. package/dist/core/canvas/nav-model.js +1 -1
  79. package/dist/core/canvas/remote-canvas-source.js +1 -1
  80. package/dist/core/canvas/types.d.ts +2 -1
  81. package/dist/core/config.js +4 -1
  82. package/dist/core/human/__tests__/page-render.test.d.ts +1 -0
  83. package/dist/core/human/__tests__/page-render.test.js +49 -0
  84. package/dist/core/human/__tests__/page-tickets.test.d.ts +1 -0
  85. package/dist/core/human/__tests__/page-tickets.test.js +112 -0
  86. package/dist/core/human/__tests__/page.test.d.ts +1 -0
  87. package/dist/core/human/__tests__/page.test.js +139 -0
  88. package/dist/core/human/__tests__/serial/inbox-core.test.js +42 -32
  89. package/dist/core/human/claim.js +2 -2
  90. package/dist/core/human/convention.d.ts +4 -4
  91. package/dist/core/human/convention.js +6 -13
  92. package/dist/core/human/markdown-html.d.ts +8 -0
  93. package/dist/core/human/markdown-html.js +581 -0
  94. package/dist/core/human/page-catalog.d.ts +6 -0
  95. package/dist/core/human/page-catalog.js +43 -0
  96. package/dist/core/human/page-render.d.ts +3 -0
  97. package/dist/core/human/page-render.js +131 -0
  98. package/dist/core/human/page-schema.d.ts +312 -0
  99. package/dist/core/human/page-schema.js +291 -0
  100. package/dist/core/human/page-synth.d.ts +9 -0
  101. package/dist/core/human/page-synth.js +17 -0
  102. package/dist/core/human/page.d.ts +22 -0
  103. package/dist/core/human/page.js +427 -0
  104. package/dist/core/human/review-schema.d.ts +19 -0
  105. package/dist/core/human/review-schema.js +34 -0
  106. package/dist/core/human/scan.d.ts +12 -3
  107. package/dist/core/human/scan.js +69 -26
  108. package/dist/core/human/summary.d.ts +5 -6
  109. package/dist/core/human/summary.js +54 -37
  110. package/dist/core/human/tickets.d.ts +61 -11
  111. package/dist/core/human/tickets.js +100 -57
  112. package/dist/core/human/types.d.ts +21 -66
  113. package/dist/core/human/types.js +1 -5
  114. package/dist/core/inspector/model.d.ts +5 -0
  115. package/dist/core/inspector/model.js +5 -0
  116. package/dist/core/inspector/text.js +2 -2
  117. package/dist/core/inspector/tui.js +10 -3
  118. package/dist/core/keybindings/catalog.d.ts +1 -1
  119. package/dist/core/keybindings/catalog.js +4 -2
  120. package/dist/core/preview-registry.js +0 -1
  121. package/dist/core/review/ticket-filter.d.ts +1 -1
  122. package/dist/core/review/ticket-filter.js +2 -8
  123. package/dist/core/runtime/broker/read-ops.js +3 -0
  124. package/dist/core/runtime/broker.js +19 -10
  125. package/dist/core/runtime/package-health.js +1 -1
  126. package/dist/core/runtime/persona.js +2 -1
  127. package/dist/core/runtime/tmux-bindings.js +8 -1
  128. package/dist/core/termrender/termrender.d.ts +1 -1
  129. package/dist/core/termrender/termrender.js +2 -2
  130. package/dist/core/termrender/version.d.ts +1 -1
  131. package/dist/core/termrender/version.js +1 -1
  132. package/dist/core/tui/page-host.d.ts +1 -1
  133. package/dist/core/tui/page-host.js +2 -2
  134. package/dist/core/user-settings.d.ts +19 -0
  135. package/dist/core/user-settings.js +22 -0
  136. package/dist/daemon/api/handlers/bash-jobs.d.ts +2 -0
  137. package/dist/daemon/api/handlers/bash-jobs.js +90 -0
  138. package/dist/daemon/api/handlers/human.js +16 -14
  139. package/dist/daemon/api/handlers/inbox.js +311 -227
  140. package/dist/daemon/api/router.d.ts +5 -1
  141. package/dist/daemon/api/router.js +5 -0
  142. package/dist/daemon/api/server.js +2 -0
  143. package/dist/daemon/cron-run.js +23 -10
  144. package/dist/daemon/human/finish.d.ts +8 -3
  145. package/dist/daemon/human/finish.js +35 -15
  146. package/dist/daemon/reconcilers/broker-supervision.js +7 -1
  147. package/dist/daemon/reconcilers/dormant-inbox.js +8 -0
  148. package/dist/daemon/reconcilers/live-obligation.d.ts +2 -0
  149. package/dist/daemon/reconcilers/live-obligation.js +11 -0
  150. package/dist/pages/bundle.css +1 -0
  151. package/dist/pages/bundle.js +1347 -0
  152. package/dist/pages/comments.d.ts +34 -0
  153. package/dist/pages/comments.js +72 -0
  154. package/dist/pages/elements/cards.d.ts +17 -0
  155. package/dist/pages/elements/cards.js +785 -0
  156. package/dist/pages/elements/chart.d.ts +1 -0
  157. package/dist/pages/elements/chart.js +648 -0
  158. package/dist/pages/elements/options.d.ts +21 -0
  159. package/dist/pages/elements/options.js +604 -0
  160. package/dist/pages/elements/pages.d.ts +24 -0
  161. package/dist/pages/elements/pages.js +355 -0
  162. package/dist/pages/elements/slot.d.ts +21 -0
  163. package/dist/pages/elements/slot.js +105 -0
  164. package/dist/pages/elements/table.d.ts +19 -0
  165. package/dist/pages/elements/table.js +906 -0
  166. package/dist/pages/elements/text.d.ts +25 -0
  167. package/dist/pages/elements/text.js +951 -0
  168. package/dist/pages/entry.d.ts +8 -0
  169. package/dist/pages/entry.js +8 -0
  170. package/dist/pages/host.d.ts +132 -0
  171. package/dist/pages/host.js +149 -0
  172. package/dist/pages/register.d.ts +2 -0
  173. package/dist/pages/register.js +3 -0
  174. package/dist/pages/slot-config.d.ts +36 -0
  175. package/dist/pages/slot-config.js +50 -0
  176. package/dist/pages/types.d.ts +98 -0
  177. package/dist/pages/types.js +23 -0
  178. package/dist/pi-extensions/canvas-bash-valve.d.ts +4 -2
  179. package/dist/pi-extensions/canvas-bash-valve.js +25 -7
  180. package/dist/types.d.ts +4 -0
  181. package/dist/types.js +2 -0
  182. package/package.json +8 -8
  183. package/runtime.lock.json +156 -101
  184. package/scripts/install-runtime.mjs +172 -5
  185. package/dist/clients/inbox/deck-adapter.d.ts +0 -29
  186. package/dist/clients/inbox/deck-adapter.js +0 -63
  187. package/dist/core/human/deck-factories.d.ts +0 -8
  188. package/dist/core/human/deck-factories.js +0 -15
  189. package/dist/core/human/deck-schema.d.ts +0 -70
  190. package/dist/core/human/deck-schema.js +0 -92
@@ -18,10 +18,14 @@ export interface RequestContext {
18
18
  res: ServerResponse;
19
19
  }
20
20
  /** What a handler returns. `body` is JSON-serialized; omit it (or use 204) for
21
- * an empty response. `headers` are merged over the default `content-type`. */
21
+ * an empty response. `headers` are merged over the default `content-type`.
22
+ * `rawBody` is the escape hatch for the routes that serve bytes rather than
23
+ * JSON (the page runtime bundle): it is written verbatim and the handler owns
24
+ * the `content-type`. Setting both is a handler bug — `rawBody` wins. */
22
25
  export interface HandlerResult {
23
26
  status: number;
24
27
  body?: unknown;
28
+ rawBody?: string | Buffer;
25
29
  headers?: Record<string, string>;
26
30
  }
27
31
  /** A route handler — sync or async. A throw is caught and mapped by the router
@@ -164,6 +164,11 @@ function writeResult(res, result) {
164
164
  if (res.headersSent)
165
165
  return;
166
166
  const headers = { ...(result.headers ?? {}) };
167
+ if (result.rawBody !== undefined) {
168
+ res.writeHead(result.status, headers);
169
+ res.end(result.rawBody);
170
+ return;
171
+ }
167
172
  if (result.status === 204 || result.body === undefined) {
168
173
  res.writeHead(result.status, headers);
169
174
  res.end();
@@ -19,6 +19,7 @@ import { emitEvent } from '../../core/events/emit.js';
19
19
  import { Router } from './router.js';
20
20
  import { handleAttachUpgrade } from './bridge.js';
21
21
  import { attachRoutes } from './handlers/attach.js';
22
+ import { bashJobRoutes } from './handlers/bash-jobs.js';
22
23
  import { brokerOperationRoutes } from './handlers/broker-ops.js';
23
24
  import { canvasRoutes } from './handlers/canvas.js';
24
25
  import { daemonRoutes } from './handlers/daemon.js';
@@ -46,6 +47,7 @@ function buildRouter() {
46
47
  .registerAll(healthRoutes)
47
48
  .registerAll(daemonRoutes)
48
49
  .registerAll(nodeRoutes)
50
+ .registerAll(bashJobRoutes)
49
51
  .registerAll(brokerOperationRoutes)
50
52
  .registerAll(worktreeRoutes)
51
53
  .registerAll(messageRoutes)
@@ -32,9 +32,11 @@ import { spawn } from 'node:child_process';
32
32
  import { createHash, randomBytes, randomUUID } from 'node:crypto';
33
33
  import { join } from 'node:path';
34
34
  import { setTimeout as delay } from 'node:timers/promises';
35
- import { notifyDeck } from '../core/human/deck-factories.js';
35
+ import { rmSync } from 'node:fs';
36
+ import { synthesizedPageDocument, writeSynthesizedPage } from '../core/human/page-synth.js';
36
37
  import { ticketDir } from '../core/human/root.js';
37
- import { submitDeck } from '../core/human/tickets.js';
38
+ import { preparePage, publishPage } from '../core/human/tickets.js';
39
+ import { readConfig } from '../core/config.js';
38
40
  import { dueClockCrons, dueOverlapCrons, expiredIdleCrons, runningCrons, acquireCronRunLease, releaseCronRunLease, recordCronRun, listCronRuns, advanceCron, consumeCron, cancelCron, killLeasedCronRun, verifyLeasedCronRun, getCron, setCronState, setCronLastOutputHash, } from '../core/canvas/crons.js';
39
41
  import { getNode } from '../core/canvas/canvas.js';
40
42
  import { fullName } from '../core/canvas/labels.js';
@@ -211,19 +213,30 @@ async function deliverSpawnSink(c, kind, stdout) {
211
213
  });
212
214
  return res.node.node_id;
213
215
  }
214
- /** Deliver stdout through crouter as a durable notify deck (fire-and-forget
216
+ /** Deliver stdout through crouter as a durable notify page (fire-and-forget
215
217
  * inbox state — jobs whose output is a person's call). Mirrors `crtr human
216
- * notify` minus the CLI-env source stamp: the deck names the cron instead. */
218
+ * notify` minus the CLI-env source stamp: the page names the cron instead. */
217
219
  function deliverHumanSink(c, stdout) {
220
+ const synthesized = writeSynthesizedPage(synthesizedPageDocument(`⏰ cron ${c.name}`, `Scheduled cron "${c.name}" delivered output for your attention.`, stdout, false));
221
+ // Cron stdout rides into the page verbatim, so it can carry invalid page
222
+ // markup. Validate before minting the ticket, so a rejected page leaves no
223
+ // half-built `nfy-*` directory behind.
224
+ let prepared;
225
+ try {
226
+ prepared = preparePage({
227
+ sourceFile: synthesized.path,
228
+ dialect: 'md',
229
+ source: { sessionName: `cron ${c.name} (${c.cron_id})` },
230
+ productKinds: readConfig('user').page_components,
231
+ });
232
+ }
233
+ finally {
234
+ rmSync(synthesized.dir, { recursive: true, force: true });
235
+ }
218
236
  const id = `nfy-${randomBytes(4).toString('hex')}`;
219
237
  const rc = { mode: 'notify' };
220
238
  atomicWriteJson(join(ticketDir(id), 'run.json'), rc);
221
- let deck = notifyDeck(`⏰ cron ${c.name}`, {
222
- subtitle: `Scheduled cron "${c.name}" delivered output for your attention.`,
223
- body: stdout,
224
- });
225
- deck = { ...deck, source: { sessionName: `cron ${c.name} (${c.cron_id})` } };
226
- submitDeck({ dir: ticketDir(id), deck });
239
+ publishPage(ticketDir(id), prepared);
227
240
  }
228
241
  /** Route stdout to the cron's armed sink. Returns the delivered string on
229
242
  * success; throws on an undeliverable sink (the caller's failure path). */
@@ -1,6 +1,11 @@
1
- import type { InteractionResponse, TicketResult } from '../../core/human/types.js';
2
- /** Resolve a deck ticket under the store-owned claim-takeover lock boundary. */
3
- export declare function resolveDeckTicket(nodeId: string, responses: InteractionResponse[]): Promise<TicketResult>;
1
+ import type { TicketResult } from '../../core/human/types.js';
2
+ /**
3
+ * The one completion path for a page ticket: claim takeover, exclusive result
4
+ * publication, then the asking node's wake. Every addressing surface — the
5
+ * node-addressed resolve route and the ticket-id-addressed inbox respond route —
6
+ * finishes a page here, so an answer always reaches the node that asked.
7
+ */
8
+ export declare function resolvePageTicket(nodeId: string, responses: unknown, productKinds?: string[]): Promise<TicketResult>;
4
9
  /** Cancel a ticket with the store's first-writer-wins publication. */
5
10
  export declare function cancelHumanTicket(nodeId: string, opts: {
6
11
  reason?: string;
@@ -2,19 +2,35 @@ import { join } from 'node:path';
2
2
  import { ApiError } from '../../api/index.js';
3
3
  import { getNode, subscribersOf } from '../../core/canvas/canvas.js';
4
4
  import { ticketDir } from '../../core/human/root.js';
5
- import { cancelTicket, projectReviewOutput, readTicketResult, takeoverAndCompleteDeck, } from '../../core/human/tickets.js';
5
+ import { cancelTicket, projectReviewOutput, readTicketResult, takeoverAndCompletePage, } from '../../core/human/tickets.js';
6
6
  import { responsePath } from '../../core/human/convention.js';
7
7
  import { readJsonOrNull } from '../../core/fs-utils.js';
8
8
  import { appendInbox } from '../../core/feed/inbox.js';
9
9
  import { FinalDeliveryError, pushFinal } from '../../core/feed/feed.js';
10
10
  import { transition } from '../../core/runtime/lifecycle.js';
11
- function envelopeFromDeckResult(responses, summary, completedAt, resultPath) {
12
- return { summary, responsePath: resultPath, schema: 'humanloop.response/v2', responses, completedAt };
13
- }
14
- function renderDeckAnswer(env) {
15
- const summary = env.summary.trim();
16
- const body = summary === '' ? '(the human submitted no answer text)' : summary;
17
- return `${body}\n\nResponse metadata (question ids, selected options, timestamp): ${env.responsePath}`;
11
+ /**
12
+ * The answer a page's asking node wakes with. Slot responses are small and
13
+ * typed, so they are inlined as compact JSON rather than left behind the path:
14
+ * the node reads its answers out of its own report. An acknowledgement carries
15
+ * no responses at all and reads as one line saying the human saw the page.
16
+ * Interim rendering — the designed answer view is phase 5.
17
+ */
18
+ function renderPageAnswer(result, resultPath) {
19
+ const entries = Object.entries(result.responses);
20
+ const lines = [];
21
+ if (entries.length === 0) {
22
+ lines.push('The human read the page and dismissed it. No slot responses came back, so the acknowledgement is the whole answer.');
23
+ }
24
+ else {
25
+ lines.push(`The human answered ${entries.length} slot${entries.length === 1 ? '' : 's'}:`, '');
26
+ for (const [slotId, response] of entries)
27
+ lines.push(`${slotId}: ${JSON.stringify(response)}`);
28
+ const summary = result.summary.trim();
29
+ if (summary !== '')
30
+ lines.push('', summary);
31
+ }
32
+ lines.push('', `Response metadata (slot ids, typed responses, timestamp): ${resultPath}`);
33
+ return lines.join('\n');
18
34
  }
19
35
  function deliveryFailed(error) {
20
36
  // A canonical final is committed inside its own write boundary and can never
@@ -35,9 +51,14 @@ async function deliverOrConflict(nodeId) {
35
51
  }
36
52
  throw new ApiError(409, 'ticket_already_resolved', 'ticket already resolved');
37
53
  }
38
- /** Resolve a deck ticket under the store-owned claim-takeover lock boundary. */
39
- export async function resolveDeckTicket(nodeId, responses) {
40
- const completed = takeoverAndCompleteDeck(ticketDir(nodeId), responses, { host: 'crtrd', pid: process.pid });
54
+ /**
55
+ * The one completion path for a page ticket: claim takeover, exclusive result
56
+ * publication, then the asking node's wake. Every addressing surface — the
57
+ * node-addressed resolve route and the ticket-id-addressed inbox respond route —
58
+ * finishes a page here, so an answer always reaches the node that asked.
59
+ */
60
+ export async function resolvePageTicket(nodeId, responses, productKinds = []) {
61
+ const completed = takeoverAndCompletePage(ticketDir(nodeId), responses, { host: 'crtrd', pid: process.pid }, { productKinds });
41
62
  if (!completed.won)
42
63
  return deliverOrConflict(nodeId);
43
64
  try {
@@ -108,9 +129,8 @@ export async function deliverTerminalResult(nodeId) {
108
129
  transition(nodeId, 'finish');
109
130
  return true;
110
131
  }
111
- if (result.kind !== 'deck')
112
- return true;
113
- const envelope = envelopeFromDeckResult(result.responses, result.summary, result.completedAt, responsePath(dir));
114
- await pushFinal(nodeId, renderDeckAnswer(envelope));
132
+ if (result.kind === 'page') {
133
+ await pushFinal(nodeId, renderPageAnswer(result, responsePath(dir)));
134
+ }
115
135
  return true;
116
136
  }
@@ -20,6 +20,7 @@ import { operationIdContext } from '../../core/events/operation-id.js';
20
20
  import { readCursor, readInboxSince } from '../../core/feed/inbox.js';
21
21
  import { activeBackgroundBashJobs } from '../../core/bash-jobs.js';
22
22
  import { focusOf } from '../../core/runtime/placement.js';
23
+ import { hasLiveObligation } from './live-obligation.js';
23
24
  const execFileAsync = promisify(execFile);
24
25
  function configuredPositiveInteger(name, fallback) {
25
26
  const raw = process.env[name];
@@ -205,7 +206,12 @@ export class BrokerSupervisionReconciler {
205
206
  if (now - since < unattendedParkMsForDaemon())
206
207
  return;
207
208
  this.unattendedSince.delete(id);
208
- transition(id, 'release');
209
+ const liveObligation = hasLiveObligation(id);
210
+ // A resident with nothing left to wake it has completed its conversation:
211
+ // terminalize it so history retention can eventually prune it. A resident
212
+ // still awaiting a child/controller/human/deadline/output stays wakeable, as does
213
+ // every terminal node (whose own stop guard owns semantic completion).
214
+ transition(id, row.lifecycle === 'resident' && !liveObligation ? 'finish' : 'release');
209
215
  headlessBrokerHost.teardown(id);
210
216
  }
211
217
  async warnOverCapInTmux(count, warning, max) {
@@ -7,6 +7,7 @@ import { appendControllerDeathNotice, readCursor, readInboxSince, } from '../../
7
7
  import { fanDoctrineWake } from '../../core/runtime/close.js';
8
8
  import { transition } from '../../core/runtime/lifecycle.js';
9
9
  import { reviveNode } from '../../core/runtime/revive.js';
10
+ import { hasLiveObligation } from './live-obligation.js';
10
11
  // Idle-release/inbox retry cap (2026-07-10 diagnosis, round 2): the SAME
11
12
  // livelock shape as the stranded-relaunch case, but on the separate pass that
12
13
  // resumes a dormant idle-released node when its inbox gains an unseen entry.
@@ -78,6 +79,13 @@ export class DormantInboxReconciler {
78
79
  // set neither revives the node nor starts a stuck-relaunch episode.
79
80
  if (!entries.some((entry) => entry.tier !== 'deferred')) {
80
81
  this.relaunchState.delete(id);
82
+ const meta = getNode(id);
83
+ // An idle resident with no wake-worthy inbox entry still needs
84
+ // lifecycle settlement. Once nothing live remains, this is completed
85
+ // history rather than a permanently protected idle row.
86
+ if (meta?.lifecycle === 'resident' && !hasLiveObligation(id)) {
87
+ transition(id, 'finish');
88
+ }
81
89
  return;
82
90
  }
83
91
  const prior = this.relaunchState.get(id);
@@ -0,0 +1,2 @@
1
+ /** Durable work that requires a resident to remain automatically wakeable. */
2
+ export declare function hasLiveObligation(nodeId: string): boolean;
@@ -0,0 +1,11 @@
1
+ import { hasActiveLiveSubscription, hasLiveMessageWait, hasPendingCancelOnWakeCron, } from '../../core/canvas/index.js';
2
+ import { countTickets } from '../../core/canvas/attention.js';
3
+ import { readOutputRequest } from '../../core/runtime/structured-output.js';
4
+ /** Durable work that requires a resident to remain automatically wakeable. */
5
+ export function hasLiveObligation(nodeId) {
6
+ return hasActiveLiveSubscription(nodeId)
7
+ || hasLiveMessageWait(nodeId)
8
+ || hasPendingCancelOnWakeCron(nodeId)
9
+ || countTickets(nodeId) > 0
10
+ || readOutputRequest(nodeId).state !== 'absent';
11
+ }
@@ -0,0 +1 @@
1
+ :where(:root,:host){color-scheme:light dark;--crtr-color-background: #ffffff;--crtr-color-surface: #f7f7f8;--crtr-color-surface-hover: #eeeeef;--crtr-color-text: #1c1c1f;--crtr-color-text-muted: #68686f;--crtr-color-border: #d9d9de;--crtr-color-accent: #5753c9;--crtr-color-accent-contrast: #ffffff;--crtr-color-danger: #c73535;--crtr-color-focus: #706bea;--crtr-chart-1: #5753c9;--crtr-chart-2: #2c8c75;--crtr-chart-3: #cf7c2a;--crtr-chart-4: #b84c84;--crtr-chart-5: #477eb8;--crtr-font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--crtr-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;--crtr-font-size: .875rem;--crtr-line-height: 1.5;--crtr-radius-small: .375rem;--crtr-radius-medium: .625rem;--crtr-space-1: .25rem;--crtr-space-2: .5rem;--crtr-space-3: .75rem;--crtr-space-4: 1rem;--crtr-shadow: 0 1px 2px rgb(0 0 0 / .08)}:where(crtr-options,crtr-text,crtr-table,crtr-cards,crtr-chart,crtr-pages,crtr-page,crtr-slot){color:var(--crtr-color-text);font-family:var(--crtr-font-family);font-size:var(--crtr-font-size);line-height:var(--crtr-line-height)}@media(prefers-color-scheme:dark){:where(:root,:host){--crtr-color-background: #161618;--crtr-color-surface: #222225;--crtr-color-surface-hover: #2d2d31;--crtr-color-text: #f2f2f3;--crtr-color-text-muted: #aaaab1;--crtr-color-border: #414147;--crtr-color-accent: #9692ff;--crtr-color-accent-contrast: #18181b;--crtr-color-danger: #f06a6a;--crtr-color-focus: #aaa7ff}}