@north-light/crouter 0.3.196 → 0.3.198
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 +2 -2
- package/dist/api/dto/inbox.d.ts +46 -0
- package/dist/builtin-memory/00-runtime-base.md +3 -3
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
- package/dist/clients/attach/render/chat-view.d.ts +3 -8
- package/dist/clients/attach/render/chat-view.js +3 -24
- package/dist/clients/attach/render/markdown-source.js +0 -6
- package/dist/clients/attach/render/tool-calls.js +16 -2
- package/dist/clients/attach/viewer.js +613 -602
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +2 -2
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +11 -4
- package/dist/clients/inbox/controller.js +4 -4
- package/dist/clients/inbox/page-adapter.d.ts +2 -1
- package/dist/clients/inbox/tui/render.js +46 -25
- package/dist/clients/inbox/tui/slots.d.ts +1 -3
- package/dist/clients/inbox/tui/slots.js +8 -37
- package/dist/clients/inbox/tui/types.d.ts +4 -3
- package/dist/commands/__tests__/human.test.js +160 -23
- package/dist/commands/canvas-browse.js +1 -1
- package/dist/commands/human/doc.js +21 -74
- package/dist/commands/human/html.d.ts +2 -0
- package/dist/commands/human/html.js +81 -0
- package/dist/commands/human/prompts.d.ts +5 -4
- package/dist/commands/human/prompts.js +90 -168
- package/dist/commands/human/shared.d.ts +5 -6
- package/dist/commands/human/shared.js +13 -61
- package/dist/commands/human.js +7 -2
- package/dist/core/__tests__/daemon-boot.test.js +7 -4
- package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
- package/dist/core/__tests__/human-deliver.test.js +28 -37
- package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
- package/dist/core/__tests__/seam/dormancy-release.test.js +63 -4
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +3 -13
- package/dist/core/canvas/__tests__/attention.test.js +2 -2
- package/dist/core/canvas/__tests__/render-remote.test.js +2 -2
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +21 -3
- package/dist/core/canvas/browse/render.d.ts +4 -0
- package/dist/core/canvas/browse/render.js +5 -1
- package/dist/core/canvas/canvas.d.ts +26 -0
- package/dist/core/canvas/canvas.js +55 -0
- package/dist/core/command.js +6 -11
- package/dist/core/config.js +3 -2
- package/dist/core/help.d.ts +3 -0
- package/dist/core/human/__tests__/html-markdown.test.d.ts +1 -0
- package/dist/core/human/__tests__/html-markdown.test.js +52 -0
- package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-catalog.test.js +16 -0
- package/dist/core/human/__tests__/page-render.test.js +40 -23
- package/dist/core/human/__tests__/page-tickets.test.js +48 -27
- package/dist/core/human/__tests__/page.test.js +48 -65
- package/dist/core/human/__tests__/serial/inbox-core.test.js +5 -15
- package/dist/core/human/answer-text.d.ts +3 -0
- package/dist/core/human/answer-text.js +98 -0
- package/dist/core/human/answer.d.ts +51 -0
- package/dist/core/human/answer.js +125 -0
- package/dist/core/human/component-docs.d.ts +14 -0
- package/dist/core/human/component-docs.js +161 -0
- package/dist/core/human/html-markdown.d.ts +3 -0
- package/dist/core/human/html-markdown.js +288 -0
- package/dist/core/human/page-catalog.d.ts +19 -3
- package/dist/core/human/page-catalog.js +73 -16
- package/dist/core/human/page-render.d.ts +3 -2
- package/dist/core/human/page-render.js +19 -45
- package/dist/core/human/page-schema.d.ts +108 -8
- package/dist/core/human/page-schema.js +88 -31
- package/dist/core/human/page-synth.d.ts +2 -4
- package/dist/core/human/page-synth.js +23 -7
- package/dist/core/human/page.d.ts +14 -7
- package/dist/core/human/page.js +178 -90
- package/dist/core/human/review-schema.d.ts +1 -0
- package/dist/core/human/review-schema.js +1 -1
- package/dist/core/human/scan.d.ts +3 -2
- package/dist/core/human/scan.js +7 -8
- package/dist/core/human/summary.d.ts +4 -7
- package/dist/core/human/summary.js +4 -60
- package/dist/core/human/tickets.d.ts +10 -9
- package/dist/core/human/tickets.js +5 -3
- package/dist/core/human/types.d.ts +4 -0
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +1 -0
- package/dist/core/review/realize.js +9 -1
- package/dist/core/runtime/boot-root.js +6 -1
- package/dist/core/runtime/broker.js +8 -6
- package/dist/core/runtime/busy.d.ts +4 -3
- package/dist/core/runtime/busy.js +4 -3
- package/dist/core/runtime/spawn.js +4 -4
- package/dist/core/termrender/display.d.ts +7 -3
- package/dist/core/termrender/display.js +45 -9
- package/dist/core/termrender/termrender.d.ts +3 -12
- package/dist/core/termrender/termrender.js +6 -37
- package/dist/core/user-settings.d.ts +17 -2
- package/dist/core/user-settings.js +32 -11
- package/dist/daemon/api/handlers/human.js +1 -1
- package/dist/daemon/api/handlers/inbox.js +22 -7
- package/dist/daemon/cron-run.js +16 -2
- package/dist/daemon/crtrd.js +1 -1
- package/dist/daemon/fleet.d.ts +22 -10
- package/dist/daemon/fleet.js +43 -24
- package/dist/daemon/human/finish.d.ts +2 -1
- package/dist/daemon/human/finish.js +45 -28
- package/dist/daemon/human/sweep.js +4 -2
- package/dist/daemon/reconcilers/broker-supervision.js +11 -8
- package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
- package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
- package/dist/daemon/reconcilers/live-obligation.js +30 -2
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
- package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
- package/dist/daemon/review/sweep.js +12 -1
- package/dist/pages/bundle.css +1 -1
- package/dist/pages/bundle.js +1008 -577
- package/dist/pages/comments.d.ts +29 -0
- package/dist/pages/comments.js +51 -0
- package/dist/pages/controls.d.ts +98 -0
- package/dist/pages/controls.js +261 -0
- package/dist/pages/elements/cards.d.ts +15 -4
- package/dist/pages/elements/cards.js +299 -186
- package/dist/pages/elements/chart.d.ts +22 -0
- package/dist/pages/elements/chart.js +107 -91
- package/dist/pages/elements/options.d.ts +9 -0
- package/dist/pages/elements/options.js +268 -155
- package/dist/pages/elements/pages.d.ts +6 -0
- package/dist/pages/elements/pages.js +60 -38
- package/dist/pages/elements/slot.d.ts +5 -0
- package/dist/pages/elements/slot.js +17 -10
- package/dist/pages/elements/table.d.ts +17 -0
- package/dist/pages/elements/table.js +336 -228
- package/dist/pages/elements/text.d.ts +9 -3
- package/dist/pages/elements/text.js +350 -143
- package/dist/pages/host.d.ts +0 -24
- package/dist/pages/host.js +0 -17
- package/dist/pages/readonly.d.ts +24 -0
- package/dist/pages/readonly.js +31 -0
- package/dist/pages/responses.d.ts +37 -0
- package/dist/pages/responses.js +56 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
- package/dist/pi-extensions/canvas-bash-valve.js +12 -8
- package/dist/types.d.ts +19 -5
- package/dist/types.js +2 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/scripts/install-runtime.mjs +19 -24
- package/dist/clients/attach/render/html-markdown.d.ts +0 -13
- package/dist/clients/attach/render/html-markdown.js +0 -206
- package/dist/core/human/markdown-html.d.ts +0 -8
- package/dist/core/human/markdown-html.js +0 -581
|
@@ -7,10 +7,12 @@ import { basename, resolve, join } from 'node:path';
|
|
|
7
7
|
import { ApiError } from '../../../api/index.js';
|
|
8
8
|
import { assetRoot } from '../../../core/asset-root.js';
|
|
9
9
|
import { pageManifestPath, reviewPath } from '../../../core/human/convention.js';
|
|
10
|
+
import { parsePage } from '../../../core/human/page.js';
|
|
10
11
|
import { renderPageHtml } from '../../../core/human/page-render.js';
|
|
11
12
|
import { ticketsRoot } from '../../../core/human/root.js';
|
|
12
13
|
import { scanInbox, scanPageHistory } from '../../../core/human/scan.js';
|
|
13
14
|
import { readPageTicket, readTicketResult, validatePageTicketResponses, writePageProgress, } from '../../../core/human/tickets.js';
|
|
15
|
+
import { describePageAnswer } from '../../../core/human/answer.js';
|
|
14
16
|
import { readConfig } from '../../../core/config.js';
|
|
15
17
|
import { cancelHumanTicket, resolvePageTicket } from '../../human/finish.js';
|
|
16
18
|
import { filterTerminalReviewTickets } from '../../../core/review/ticket-filter.js';
|
|
@@ -80,6 +82,8 @@ function pageSlotToDTO(slot) {
|
|
|
80
82
|
dto.id = slot.id;
|
|
81
83
|
if (slot.unvalidated === true)
|
|
82
84
|
dto.unvalidated = true;
|
|
85
|
+
if (slot.display === true)
|
|
86
|
+
dto.display = true;
|
|
83
87
|
return dto;
|
|
84
88
|
}
|
|
85
89
|
function pageManifestToDTO(manifest) {
|
|
@@ -102,6 +106,8 @@ function pageManifestToDTO(manifest) {
|
|
|
102
106
|
source.askedBy = manifest.source.askedBy;
|
|
103
107
|
if (manifest.source.blockedSince !== undefined)
|
|
104
108
|
source.blockedSince = manifest.source.blockedSince;
|
|
109
|
+
if (manifest.source.profileName !== undefined)
|
|
110
|
+
source.profileName = manifest.source.profileName;
|
|
105
111
|
if (manifest.source.nodeId !== undefined)
|
|
106
112
|
source.nodeId = manifest.source.nodeId;
|
|
107
113
|
if (Object.keys(source).length > 0)
|
|
@@ -163,7 +169,7 @@ function isSlotValidationError(err) {
|
|
|
163
169
|
// GET /v1/human/inbox — enumerate
|
|
164
170
|
// ===========================================================================
|
|
165
171
|
function handleList() {
|
|
166
|
-
const productKinds = readConfig('user').page_components
|
|
172
|
+
const productKinds = readConfig('user').page_components;
|
|
167
173
|
const allItems = scanInbox(productKinds);
|
|
168
174
|
const filtered = filterTerminalReviewTickets(allItems);
|
|
169
175
|
const tickets = filtered.map((item) => {
|
|
@@ -210,7 +216,7 @@ function handleHistory(ctx) {
|
|
|
210
216
|
if (!nodeId || typeof nodeId !== 'string') {
|
|
211
217
|
throw new ApiError(400, 'invalid_request', 'history requires a node_id query param');
|
|
212
218
|
}
|
|
213
|
-
const productKinds = readConfig('user').page_components
|
|
219
|
+
const productKinds = readConfig('user').page_components;
|
|
214
220
|
const history = scanPageHistory(nodeId, productKinds);
|
|
215
221
|
const canonicalRoot = realpathSync(ticketsRoot());
|
|
216
222
|
const tickets = history.map((item) => {
|
|
@@ -230,6 +236,8 @@ function handleHistory(ctx) {
|
|
|
230
236
|
};
|
|
231
237
|
if (item.subtitle !== '')
|
|
232
238
|
dto.subtitle = item.subtitle;
|
|
239
|
+
if (item.answerDigest !== undefined)
|
|
240
|
+
dto.answer_digest = item.answerDigest;
|
|
233
241
|
return dto;
|
|
234
242
|
});
|
|
235
243
|
return { status: 200, body: { tickets } };
|
|
@@ -245,7 +253,7 @@ function handleGetPage(ctx) {
|
|
|
245
253
|
if (resolved.kind === 'review') {
|
|
246
254
|
throw new ApiError(409, 'ticket_kind_unsupported', 'review tickets have no page read operation');
|
|
247
255
|
}
|
|
248
|
-
const productKinds = readConfig('user').page_components
|
|
256
|
+
const productKinds = readConfig('user').page_components;
|
|
249
257
|
let ticket;
|
|
250
258
|
try {
|
|
251
259
|
ticket = readPageTicket(resolved.dir, productKinds);
|
|
@@ -287,7 +295,7 @@ async function handleRespond(ctx) {
|
|
|
287
295
|
if (result?.kind === 'canceled') {
|
|
288
296
|
throw new ApiError(409, 'ticket_canceled', 'canceled tickets cannot be answered');
|
|
289
297
|
}
|
|
290
|
-
const productKinds = readConfig('user').page_components
|
|
298
|
+
const productKinds = readConfig('user').page_components;
|
|
291
299
|
try {
|
|
292
300
|
validatePageTicketResponses(resolved.dir, request.responses, productKinds);
|
|
293
301
|
}
|
|
@@ -330,7 +338,7 @@ function handleProgress(ctx) {
|
|
|
330
338
|
if (result !== null && result.kind !== 'canceled') {
|
|
331
339
|
throw new ApiError(409, 'ticket_already_resolved', 'ticket was already resolved');
|
|
332
340
|
}
|
|
333
|
-
const productKinds = readConfig('user').page_components
|
|
341
|
+
const productKinds = readConfig('user').page_components;
|
|
334
342
|
try {
|
|
335
343
|
writePageProgress(resolved.dir, request.responses, productKinds);
|
|
336
344
|
return { status: 200, body: {} };
|
|
@@ -390,7 +398,14 @@ function handleGetResponse(ctx) {
|
|
|
390
398
|
if (result.kind !== 'page') {
|
|
391
399
|
throw new ApiError(409, 'ticket_kind_mismatch', 'unexpected result kind');
|
|
392
400
|
}
|
|
393
|
-
|
|
401
|
+
const productKinds = readConfig('user').page_components;
|
|
402
|
+
try {
|
|
403
|
+
const manifest = parsePage(resolved.dir, productKinds);
|
|
404
|
+
return { status: 200, body: { ...result, answer: describePageAnswer(manifest, result.responses, { completedAt: result.completedAt, productKinds }) } };
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
throw new ApiError(500, 'internal', 'the ticket answer could not be read');
|
|
408
|
+
}
|
|
394
409
|
}
|
|
395
410
|
// ===========================================================================
|
|
396
411
|
// GET /v1/human/inbox/:ticket_id/render — the page's HTML payload
|
|
@@ -403,7 +418,7 @@ function handleRenderPage(ctx) {
|
|
|
403
418
|
if (resolved.kind === 'review') {
|
|
404
419
|
throw new ApiError(409, 'ticket_kind_unsupported', 'review tickets have no page render operation');
|
|
405
420
|
}
|
|
406
|
-
const productKinds = readConfig('user').page_components
|
|
421
|
+
const productKinds = readConfig('user').page_components;
|
|
407
422
|
// Addressable for pending, resolved, and canceled tickets alike — the same
|
|
408
423
|
// read rule as the detail route: a page stays readable after it resolves.
|
|
409
424
|
let ticket;
|
package/dist/daemon/cron-run.js
CHANGED
|
@@ -163,6 +163,16 @@ function resolveLiveProfile(c) {
|
|
|
163
163
|
return null;
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
+
function resolveLiveProfileName(c) {
|
|
167
|
+
if (c.profile == null)
|
|
168
|
+
return undefined;
|
|
169
|
+
try {
|
|
170
|
+
return loadProfileManifest(c.profile).manifest.name;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
166
176
|
// ---------------------------------------------------------------------------
|
|
167
177
|
// Delivery sinks
|
|
168
178
|
// ---------------------------------------------------------------------------
|
|
@@ -223,10 +233,14 @@ function deliverHumanSink(c, stdout) {
|
|
|
223
233
|
// half-built `nfy-*` directory behind.
|
|
224
234
|
let prepared;
|
|
225
235
|
try {
|
|
236
|
+
const profileName = resolveLiveProfileName(c);
|
|
226
237
|
prepared = preparePage({
|
|
227
238
|
sourceFile: synthesized.path,
|
|
228
|
-
dialect: '
|
|
229
|
-
source: {
|
|
239
|
+
dialect: 'html',
|
|
240
|
+
source: {
|
|
241
|
+
sessionName: `cron ${c.name} (${c.cron_id})`,
|
|
242
|
+
...(profileName === undefined ? {} : { profileName }),
|
|
243
|
+
},
|
|
230
244
|
productKinds: readConfig('user').page_components,
|
|
231
245
|
});
|
|
232
246
|
}
|
package/dist/daemon/crtrd.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// The fleet registry's entry IS the node's liveness — no pid probe, no grace
|
|
9
9
|
// clock. A broker's real `exit` event enqueues an exit-policy job on the
|
|
10
10
|
// daemon's serial work lane (never inline on the Node event callback), where
|
|
11
|
-
// classifyDeadNode — one pure
|
|
11
|
+
// classifyDeadNode — one pure 8-row policy table over durable row state —
|
|
12
12
|
// decides forget / dormant / respawn / boot-failure. The startup recovery
|
|
13
13
|
// sweep classifies boot-time dead rows through the SAME table, so exit
|
|
14
14
|
// recovery and startup recovery can never diverge. Every broker's argv is
|
package/dist/daemon/fleet.d.ts
CHANGED
|
@@ -10,12 +10,16 @@ import type { HostHandle } from '../core/runtime/host.js';
|
|
|
10
10
|
* seeing a silent `dead`. */
|
|
11
11
|
export declare function surfaceBootFailure(meta: NodeMeta): Promise<void>;
|
|
12
12
|
/** Whether a dead relaunch should resume the saved session strictly, or retry
|
|
13
|
-
* as a fresh cycle instead. A cycling attempt (cycle_pending) that dies
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
13
|
+
* as a fresh cycle instead. A cycling attempt (cycle_pending) that dies before
|
|
14
|
+
* session_start must retry AS a cycle: the fresh branch it was opening is not
|
|
15
|
+
* confirmed, so resuming would silently continue the old leaf the refresh was
|
|
16
|
+
* meant to leave behind.
|
|
17
|
+
*
|
|
18
|
+
* An interrupted turn is NOT a reason to cycle. A strict reopen does restore
|
|
19
|
+
* the transcript at an idle prompt without restarting the dead turn — but the
|
|
20
|
+
* fix for that is the continuation prompt recovery delivers (see `#respawn`),
|
|
21
|
+
* not throwing the conversation away. */
|
|
22
|
+
export declare function retryResumeMode(meta: Pick<NodeMeta, 'cycle_pending'> | null): boolean;
|
|
19
23
|
export type DeadNodeAction = 'forget' | 'respawn-fresh' | 'dormant' | 'respawn-cycle' | 'respawn-resume' | 'boot-failure';
|
|
20
24
|
/** The narrow durable state the policy table reads. Structural (a Pick, not
|
|
21
25
|
* NodeMeta itself) so tests fabricate table rows directly. */
|
|
@@ -23,10 +27,18 @@ export type DeadNodeRow = Pick<NodeMeta, 'status' | 'intent' | 'pi_session_id' |
|
|
|
23
27
|
/** One pure function producing the action for a node known-dead. Used at
|
|
24
28
|
* exactly two call sites — an exit-policy job and the startup recovery sweep
|
|
25
29
|
* (design D-12) — so exit recovery and startup recovery can never diverge.
|
|
26
|
-
* Evaluated in table order; `busy` is the on-disk busy marker.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
+
* Evaluated in table order; `busy` is the on-disk busy marker.
|
|
31
|
+
*
|
|
32
|
+
* `busy` no longer selects the ACTION. Whether the engine died mid-turn or
|
|
33
|
+
* parked at an idle prompt, the saved transcript is equally resumable — pi
|
|
34
|
+
* persists whole entries (`appendFileSync` per line) so a killed generation
|
|
35
|
+
* simply leaves the file ending at its last complete entry, and the loader
|
|
36
|
+
* skips a malformed line rather than rejecting the session. A mid-turn death
|
|
37
|
+
* needs the turn re-driven, which is a continuation prompt on top of the
|
|
38
|
+
* resume (see `#respawn`), not a fresh branch. `busy` therefore survives here
|
|
39
|
+
* only to route a node that died before session_start ever recorded a session
|
|
40
|
+
* to a fresh relaunch instead of terminalizing it as a boot failure. */
|
|
41
|
+
export declare function classifyDeadNode(row: DeadNodeRow | null, busy: boolean): DeadNodeAction;
|
|
30
42
|
/** An exit with uptime at or above this resets the consecutive-failure
|
|
31
43
|
* counter before classification; a shorter-lived exit increments it. */
|
|
32
44
|
export declare const HEALTHY_UPTIME_MS = 60000;
|
package/dist/daemon/fleet.js
CHANGED
|
@@ -51,21 +51,33 @@ async function surfaceRecoveryFailure(meta, error) {
|
|
|
51
51
|
await pushUrgent(meta.node_id, body, { from: meta.node_id });
|
|
52
52
|
}
|
|
53
53
|
/** Whether a dead relaunch should resume the saved session strictly, or retry
|
|
54
|
-
* as a fresh cycle instead. A cycling attempt (cycle_pending) that dies
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
* as a fresh cycle instead. A cycling attempt (cycle_pending) that dies before
|
|
55
|
+
* session_start must retry AS a cycle: the fresh branch it was opening is not
|
|
56
|
+
* confirmed, so resuming would silently continue the old leaf the refresh was
|
|
57
|
+
* meant to leave behind.
|
|
58
|
+
*
|
|
59
|
+
* An interrupted turn is NOT a reason to cycle. A strict reopen does restore
|
|
60
|
+
* the transcript at an idle prompt without restarting the dead turn — but the
|
|
61
|
+
* fix for that is the continuation prompt recovery delivers (see `#respawn`),
|
|
62
|
+
* not throwing the conversation away. */
|
|
63
|
+
export function retryResumeMode(meta) {
|
|
64
|
+
return meta?.cycle_pending !== true;
|
|
61
65
|
}
|
|
62
66
|
/** One pure function producing the action for a node known-dead. Used at
|
|
63
67
|
* exactly two call sites — an exit-policy job and the startup recovery sweep
|
|
64
68
|
* (design D-12) — so exit recovery and startup recovery can never diverge.
|
|
65
|
-
* Evaluated in table order; `busy` is the on-disk busy marker.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
|
|
69
|
+
* Evaluated in table order; `busy` is the on-disk busy marker.
|
|
70
|
+
*
|
|
71
|
+
* `busy` no longer selects the ACTION. Whether the engine died mid-turn or
|
|
72
|
+
* parked at an idle prompt, the saved transcript is equally resumable — pi
|
|
73
|
+
* persists whole entries (`appendFileSync` per line) so a killed generation
|
|
74
|
+
* simply leaves the file ending at its last complete entry, and the loader
|
|
75
|
+
* skips a malformed line rather than rejecting the session. A mid-turn death
|
|
76
|
+
* needs the turn re-driven, which is a continuation prompt on top of the
|
|
77
|
+
* resume (see `#respawn`), not a fresh branch. `busy` therefore survives here
|
|
78
|
+
* only to route a node that died before session_start ever recorded a session
|
|
79
|
+
* to a fresh relaunch instead of terminalizing it as a boot failure. */
|
|
80
|
+
export function classifyDeadNode(row, busy) {
|
|
69
81
|
if (row === null)
|
|
70
82
|
return 'forget'; // row 1: row deleted
|
|
71
83
|
if (row.status === 'done' || row.status === 'canceled' || row.status === 'dead')
|
|
@@ -74,15 +86,13 @@ export function classifyDeadNode(row, busy, cleanAbort = false) {
|
|
|
74
86
|
return 'respawn-fresh'; // row 3
|
|
75
87
|
if (row.intent === 'idle-release')
|
|
76
88
|
return 'dormant'; // row 4
|
|
77
|
-
if (cleanAbort && row.pi_session_id != null)
|
|
78
|
-
return 'respawn-resume'; // row 5: broker-certified interrupted turn
|
|
79
|
-
if (busy)
|
|
80
|
-
return 'respawn-cycle'; // row 6: dirty interrupted turn
|
|
81
89
|
if (row.pi_session_id != null)
|
|
82
|
-
return retryResumeMode(row
|
|
83
|
-
if (
|
|
84
|
-
return 'respawn-fresh'; // row
|
|
85
|
-
|
|
90
|
+
return retryResumeMode(row) ? 'respawn-resume' : 'respawn-cycle'; // row 5: any saved session
|
|
91
|
+
if (busy)
|
|
92
|
+
return 'respawn-fresh'; // row 6: died mid-turn before a session was ever recorded
|
|
93
|
+
if (row.fork_from != null)
|
|
94
|
+
return 'respawn-fresh'; // row 7: bounded ordinary fork-birth retry
|
|
95
|
+
return 'boot-failure'; // row 8
|
|
86
96
|
}
|
|
87
97
|
// ---------------------------------------------------------------------------
|
|
88
98
|
// Respawn throttle (replaces the stranded-relaunch counters, event-driven)
|
|
@@ -248,8 +258,12 @@ export class DaemonFleet {
|
|
|
248
258
|
}
|
|
249
259
|
async #applyPolicy(nodeId, origin) {
|
|
250
260
|
const meta = getNode(nodeId);
|
|
251
|
-
|
|
252
|
-
|
|
261
|
+
// Two durable witnesses mean the dead broker had a turn in flight. A dirty
|
|
262
|
+
// death leaves the busy marker behind; a bounded graceful abort clears busy
|
|
263
|
+
// as the run settles, then writes the clean-abort certificate. Both need the
|
|
264
|
+
// same recovery: resume the saved transcript and re-drive the stopped turn.
|
|
265
|
+
const interruptedTurn = isBusy(nodeId) || hasCleanAbort(nodeId);
|
|
266
|
+
const action = classifyDeadNode(meta, interruptedTurn);
|
|
253
267
|
switch (action) {
|
|
254
268
|
case 'forget':
|
|
255
269
|
// Fanouts for terminal exits already happened at the transition site.
|
|
@@ -271,12 +285,12 @@ export class DaemonFleet {
|
|
|
271
285
|
case 'respawn-fresh':
|
|
272
286
|
case 'respawn-cycle':
|
|
273
287
|
case 'respawn-resume':
|
|
274
|
-
await this.#respawn(nodeId, meta, action, origin,
|
|
288
|
+
await this.#respawn(nodeId, meta, action, origin, interruptedTurn);
|
|
275
289
|
break;
|
|
276
290
|
}
|
|
277
291
|
return action;
|
|
278
292
|
}
|
|
279
|
-
async #respawn(nodeId, meta, action, origin,
|
|
293
|
+
async #respawn(nodeId, meta, action, origin, interruptedTurn) {
|
|
280
294
|
const state = this.#throttle.get(nodeId) ?? { failures: 0 };
|
|
281
295
|
if (state.failures >= MAX_RESPAWN_ATTEMPTS) {
|
|
282
296
|
await this.#terminalize(nodeId, meta, {
|
|
@@ -327,7 +341,12 @@ export class DaemonFleet {
|
|
|
327
341
|
try {
|
|
328
342
|
reviveNode(nodeId, {
|
|
329
343
|
resume: action === 'respawn-resume',
|
|
330
|
-
|
|
344
|
+
// A resumed session whose turn was interrupted comes back parked at an
|
|
345
|
+
// idle prompt with the dead turn unfinished. The continuation re-drives
|
|
346
|
+
// it in place, so the conversation continues instead of being rebuilt
|
|
347
|
+
// from disk on a fresh branch. Only a true resume can carry it; a cycle
|
|
348
|
+
// or fresh relaunch gets its own kickoff.
|
|
349
|
+
wakeReason: action === 'respawn-resume' && interruptedTurn ? 'runtime-restart-abort' : undefined,
|
|
331
350
|
// Replacing a dead broker instance is recovery, not a wake: nothing
|
|
332
351
|
// external arrived for this node, so an armed deadline survives the
|
|
333
352
|
// relaunch instead of being consumed by it.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProductPageComponents } from '../../types.js';
|
|
1
2
|
import type { TicketResult } from '../../core/human/types.js';
|
|
2
3
|
/**
|
|
3
4
|
* The one completion path for a page ticket: claim takeover, exclusive result
|
|
@@ -5,7 +6,7 @@ import type { TicketResult } from '../../core/human/types.js';
|
|
|
5
6
|
* node-addressed resolve route and the ticket-id-addressed inbox respond route —
|
|
6
7
|
* finishes a page here, so an answer always reaches the node that asked.
|
|
7
8
|
*/
|
|
8
|
-
export declare function resolvePageTicket(nodeId: string, responses: unknown, productKinds?:
|
|
9
|
+
export declare function resolvePageTicket(nodeId: string, responses: unknown, productKinds?: ProductPageComponents): Promise<TicketResult>;
|
|
9
10
|
/** Cancel a ticket with the store's first-writer-wins publication. */
|
|
10
11
|
export declare function cancelHumanTicket(nodeId: string, opts: {
|
|
11
12
|
reason?: string;
|
|
@@ -1,36 +1,45 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { ApiError } from '../../api/index.js';
|
|
3
|
-
import {
|
|
3
|
+
import { getRow, subscribersOf } from '../../core/canvas/canvas.js';
|
|
4
|
+
import { readConfig } from '../../core/config.js';
|
|
4
5
|
import { ticketDir } from '../../core/human/root.js';
|
|
6
|
+
import { parsePage } from '../../core/human/page.js';
|
|
5
7
|
import { cancelTicket, projectReviewOutput, readTicketResult, takeoverAndCompletePage, } from '../../core/human/tickets.js';
|
|
8
|
+
import { describePageAnswer } from '../../core/human/answer.js';
|
|
9
|
+
import { renderAnswerText } from '../../core/human/answer-text.js';
|
|
6
10
|
import { responsePath } from '../../core/human/convention.js';
|
|
7
11
|
import { readJsonOrNull } from '../../core/fs-utils.js';
|
|
8
12
|
import { appendInbox } from '../../core/feed/inbox.js';
|
|
9
13
|
import { FinalDeliveryError, pushFinal } from '../../core/feed/feed.js';
|
|
10
14
|
import { transition } from '../../core/runtime/lifecycle.js';
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
/** A corrupt manifest must not strand the asking node: the raw response remains its escape hatch. */
|
|
16
|
+
function renderCorruptPageAnswer(result, resultPath) {
|
|
17
|
+
return [
|
|
18
|
+
'The page manifest could not be read, so the raw response is provided.',
|
|
19
|
+
'',
|
|
20
|
+
JSON.stringify(result.responses),
|
|
21
|
+
'',
|
|
22
|
+
`Response record: ${resultPath}`,
|
|
23
|
+
].join('\n');
|
|
24
|
+
}
|
|
25
|
+
function pageTitle(dir) {
|
|
26
|
+
try {
|
|
27
|
+
return parsePage(dir, readConfig('user').page_components).title;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function renderPageAnswer(dir, result, resultPath) {
|
|
34
|
+
const productKinds = readConfig('user').page_components;
|
|
35
|
+
let manifest;
|
|
36
|
+
try {
|
|
37
|
+
manifest = parsePage(dir, productKinds);
|
|
23
38
|
}
|
|
24
|
-
|
|
25
|
-
|
|
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);
|
|
39
|
+
catch {
|
|
40
|
+
return renderCorruptPageAnswer(result, resultPath);
|
|
31
41
|
}
|
|
32
|
-
|
|
33
|
-
return lines.join('\n');
|
|
42
|
+
return renderAnswerText(describePageAnswer(manifest, result.responses, { completedAt: result.completedAt, productKinds }), resultPath);
|
|
34
43
|
}
|
|
35
44
|
function deliveryFailed(error) {
|
|
36
45
|
// A canonical final is committed inside its own write boundary and can never
|
|
@@ -96,12 +105,20 @@ export async function deliverTerminalResult(nodeId) {
|
|
|
96
105
|
if (result === null)
|
|
97
106
|
return false;
|
|
98
107
|
projectReviewOutput(dir);
|
|
99
|
-
|
|
108
|
+
// The ROW, not `getNode`: a ticket directory IS the bridge's node directory,
|
|
109
|
+
// so a bridge whose row has been pruned still answers `getNode` from its
|
|
110
|
+
// surviving meta.json — with no status at all, which passed every guard below
|
|
111
|
+
// and then threw out of `transition` on every sweep, re-fanning the
|
|
112
|
+
// cancellation notice each time. No row means nothing left to deliver to.
|
|
113
|
+
const node = getRow(nodeId);
|
|
100
114
|
if (node === null || node.status === 'dead' || node.status === 'canceled' || (node.status === 'done' && node.final_report !== null)) {
|
|
101
115
|
return true;
|
|
102
116
|
}
|
|
103
117
|
if (result.kind === 'canceled') {
|
|
104
|
-
const
|
|
118
|
+
const title = pageTitle(dir);
|
|
119
|
+
const reason = result.reason !== undefined && result.reason !== '' ? result.reason : 'no reason was given';
|
|
120
|
+
const subject = title === undefined ? `human interaction ${nodeId} canceled — no answer is coming` : `human interaction ${nodeId} — “${title}” canceled, no answer is coming`;
|
|
121
|
+
const body = title === undefined ? `The human interaction ${nodeId} was canceled — ${reason}. No response will arrive, and any partial answers the human had entered were discarded unsent.` : `The page “${title}” was canceled — ${reason}. No response will arrive, and any partial answers the human had entered were discarded unsent.`;
|
|
105
122
|
// One failing subscriber must not silence the rest, and the bridge stays
|
|
106
123
|
// live until every notice lands: a partial fanout leaves the ticket for the
|
|
107
124
|
// next resolve/cancel attempt or the daemon-start sweep to replay. A
|
|
@@ -114,10 +131,10 @@ export async function deliverTerminalResult(nodeId) {
|
|
|
114
131
|
try {
|
|
115
132
|
appendInbox(subscriber.node_id, {
|
|
116
133
|
from: nodeId,
|
|
117
|
-
tier: '
|
|
134
|
+
tier: 'normal',
|
|
118
135
|
kind: 'message',
|
|
119
|
-
label:
|
|
120
|
-
data: { body
|
|
136
|
+
label: subject,
|
|
137
|
+
data: { body },
|
|
121
138
|
});
|
|
122
139
|
}
|
|
123
140
|
catch {
|
|
@@ -130,7 +147,7 @@ export async function deliverTerminalResult(nodeId) {
|
|
|
130
147
|
return true;
|
|
131
148
|
}
|
|
132
149
|
if (result.kind === 'page') {
|
|
133
|
-
await pushFinal(nodeId, renderPageAnswer(result, responsePath(dir)));
|
|
150
|
+
await pushFinal(nodeId, renderPageAnswer(dir, result, responsePath(dir)));
|
|
134
151
|
}
|
|
135
152
|
return true;
|
|
136
153
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { getRow } from '../../core/canvas/canvas.js';
|
|
4
4
|
import { getReviewByBridge } from '../../core/review/store.js';
|
|
5
5
|
import { emitEvent } from '../../core/events/emit.js';
|
|
6
6
|
import { ticketsRoot } from '../../core/human/root.js';
|
|
@@ -27,7 +27,9 @@ export async function reconcileUndeliveredTickets() {
|
|
|
27
27
|
// finisher retries only when its winning operation is explicitly repeated.
|
|
28
28
|
if (getReviewByBridge(entry) !== null)
|
|
29
29
|
continue;
|
|
30
|
-
|
|
30
|
+
// The ROW is what carries status; `getNode` would answer from a pruned
|
|
31
|
+
// bridge's surviving meta.json with no status at all.
|
|
32
|
+
const bridge = getRow(entry);
|
|
31
33
|
if (bridge === null || isTerminalBridge(bridge.status))
|
|
32
34
|
continue;
|
|
33
35
|
if (await deliverTerminalResult(entry))
|
|
@@ -3,7 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
-
import { getRow, getNode, subscribersOf } from '../../core/canvas/index.js';
|
|
6
|
+
import { getRow, getNode, isWarmSpare, subscribersOf } from '../../core/canvas/index.js';
|
|
7
7
|
import { contextDir, isSafeNodeId } from '../../core/canvas/paths.js';
|
|
8
8
|
import { fullName } from '../../core/canvas/labels.js';
|
|
9
9
|
import { recordedPidLiveness } from '../../core/canvas/pid.js';
|
|
@@ -20,7 +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 {
|
|
23
|
+
import { parkEvent } from './live-obligation.js';
|
|
24
24
|
const execFileAsync = promisify(execFile);
|
|
25
25
|
function configuredPositiveInteger(name, fallback) {
|
|
26
26
|
const raw = process.env[name];
|
|
@@ -174,6 +174,14 @@ export class BrokerSupervisionReconciler {
|
|
|
174
174
|
return thresholds;
|
|
175
175
|
}
|
|
176
176
|
handleUnattendedParking(id, row, livePid, now) {
|
|
177
|
+
// An unclaimed warm-pool spare is not a conversation: the booted broker it
|
|
178
|
+
// holds IS its whole value, and nobody is meant to be attending it. Its
|
|
179
|
+
// lifetime belongs to `reapStaleSpares` (warm-pool.ts) and its own TTL, so
|
|
180
|
+
// parking one here would empty the pool every 15 minutes.
|
|
181
|
+
if (isWarmSpare(id)) {
|
|
182
|
+
this.unattendedSince.delete(id);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
177
185
|
if (row === null
|
|
178
186
|
|| row.pi_session_id == null
|
|
179
187
|
|| row.pi_pid !== livePid
|
|
@@ -206,12 +214,7 @@ export class BrokerSupervisionReconciler {
|
|
|
206
214
|
if (now - since < unattendedParkMsForDaemon())
|
|
207
215
|
return;
|
|
208
216
|
this.unattendedSince.delete(id);
|
|
209
|
-
|
|
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');
|
|
217
|
+
transition(id, parkEvent(id, row.lifecycle));
|
|
215
218
|
headlessBrokerHost.teardown(id);
|
|
216
219
|
}
|
|
217
220
|
async warnOverCapInTmux(count, warning, max) {
|
|
@@ -7,7 +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 {
|
|
10
|
+
import { parkEvent } from './live-obligation.js';
|
|
11
11
|
// Idle-release/inbox retry cap (2026-07-10 diagnosis, round 2): the SAME
|
|
12
12
|
// livelock shape as the stranded-relaunch case, but on the separate pass that
|
|
13
13
|
// resumes a dormant idle-released node when its inbox gains an unseen entry.
|
|
@@ -80,11 +80,16 @@ export class DormantInboxReconciler {
|
|
|
80
80
|
if (!entries.some((entry) => entry.tier !== 'deferred')) {
|
|
81
81
|
this.relaunchState.delete(id);
|
|
82
82
|
const meta = getNode(id);
|
|
83
|
-
// An idle
|
|
84
|
-
// lifecycle settlement
|
|
85
|
-
// history rather than a permanently protected
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
// An idle row with no wake-worthy inbox entry still needs the same
|
|
84
|
+
// lifecycle settlement the live park clock applies: once nothing
|
|
85
|
+
// live remains, this is history rather than a permanently protected
|
|
86
|
+
// idle row. The row is already parked, so only a terminalizing
|
|
87
|
+
// verdict is worth writing. Unclaimed warm-pool spares never reach
|
|
88
|
+
// here — `listNodes` hides them from `ctx.rows`.
|
|
89
|
+
if (meta !== null) {
|
|
90
|
+
const event = parkEvent(id, meta.lifecycle);
|
|
91
|
+
if (event !== 'release')
|
|
92
|
+
transition(id, event);
|
|
88
93
|
}
|
|
89
94
|
return;
|
|
90
95
|
}
|
|
@@ -1,2 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
import { type Lifecycle } from '../../core/canvas/index.js';
|
|
2
|
+
/** Durable work that requires a node to remain automatically wakeable. */
|
|
2
3
|
export declare function hasLiveObligation(nodeId: string): boolean;
|
|
4
|
+
/** What parking an unattended node actually amounts to — the one rule both the
|
|
5
|
+
* live-broker park clock (broker-supervision) and the already-parked
|
|
6
|
+
* reconciliation (dormant-inbox) apply.
|
|
7
|
+
*
|
|
8
|
+
* `release` — something can still wake it, so parking is a pause.
|
|
9
|
+
* `finish` — a resident with nothing left to wake it has completed its
|
|
10
|
+
* conversation, and the row becomes prunable history.
|
|
11
|
+
* `cancel` — a terminal node that never pushed a final was torn down before
|
|
12
|
+
* finishing, which is what `canceled` records; it stays revivable
|
|
13
|
+
* and becomes prunable. A live subscriber still awaiting its
|
|
14
|
+
* report is an outstanding deliverable, so such a node keeps its
|
|
15
|
+
* wakeable idle row instead of being written off.
|
|
16
|
+
*
|
|
17
|
+
* Undelivered mail of ANY tier also holds the row. A deferred-only unseen set
|
|
18
|
+
* never wakes a node on its own — it rides the next natural cycle — but a
|
|
19
|
+
* terminalized row no longer takes that cycle, so writing one off would drop
|
|
20
|
+
* content the sender already handed over.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parkEvent(nodeId: string, lifecycle: Lifecycle): 'release' | 'finish' | 'cancel';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { hasActiveLiveSubscription, hasLiveMessageWait, hasPendingCancelOnWakeCron, } from '../../core/canvas/index.js';
|
|
1
|
+
import { hasActiveLiveSubscription, hasLiveMessageWait, hasLiveSubscriber, hasPendingCancelOnWakeCron, } from '../../core/canvas/index.js';
|
|
2
2
|
import { countTickets } from '../../core/canvas/attention.js';
|
|
3
|
+
import { readCursor, readInboxSince } from '../../core/feed/inbox.js';
|
|
3
4
|
import { readOutputRequest } from '../../core/runtime/structured-output.js';
|
|
4
|
-
/** Durable work that requires a
|
|
5
|
+
/** Durable work that requires a node to remain automatically wakeable. */
|
|
5
6
|
export function hasLiveObligation(nodeId) {
|
|
6
7
|
return hasActiveLiveSubscription(nodeId)
|
|
7
8
|
|| hasLiveMessageWait(nodeId)
|
|
@@ -9,3 +10,30 @@ export function hasLiveObligation(nodeId) {
|
|
|
9
10
|
|| countTickets(nodeId) > 0
|
|
10
11
|
|| readOutputRequest(nodeId).state !== 'absent';
|
|
11
12
|
}
|
|
13
|
+
/** What parking an unattended node actually amounts to — the one rule both the
|
|
14
|
+
* live-broker park clock (broker-supervision) and the already-parked
|
|
15
|
+
* reconciliation (dormant-inbox) apply.
|
|
16
|
+
*
|
|
17
|
+
* `release` — something can still wake it, so parking is a pause.
|
|
18
|
+
* `finish` — a resident with nothing left to wake it has completed its
|
|
19
|
+
* conversation, and the row becomes prunable history.
|
|
20
|
+
* `cancel` — a terminal node that never pushed a final was torn down before
|
|
21
|
+
* finishing, which is what `canceled` records; it stays revivable
|
|
22
|
+
* and becomes prunable. A live subscriber still awaiting its
|
|
23
|
+
* report is an outstanding deliverable, so such a node keeps its
|
|
24
|
+
* wakeable idle row instead of being written off.
|
|
25
|
+
*
|
|
26
|
+
* Undelivered mail of ANY tier also holds the row. A deferred-only unseen set
|
|
27
|
+
* never wakes a node on its own — it rides the next natural cycle — but a
|
|
28
|
+
* terminalized row no longer takes that cycle, so writing one off would drop
|
|
29
|
+
* content the sender already handed over.
|
|
30
|
+
*/
|
|
31
|
+
export function parkEvent(nodeId, lifecycle) {
|
|
32
|
+
if (hasLiveObligation(nodeId))
|
|
33
|
+
return 'release';
|
|
34
|
+
if (readInboxSince(nodeId, readCursor(nodeId)).length > 0)
|
|
35
|
+
return 'release';
|
|
36
|
+
if (lifecycle === 'resident')
|
|
37
|
+
return 'finish';
|
|
38
|
+
return hasLiveSubscriber(nodeId) ? 'release' : 'cancel';
|
|
39
|
+
}
|
|
@@ -4,7 +4,12 @@ export declare const DEAD_REAP_GRACE_MS: number;
|
|
|
4
4
|
export declare class StorageMaintenanceReconciler {
|
|
5
5
|
private lastPruneAt;
|
|
6
6
|
private lastSpareSweepAt;
|
|
7
|
+
private lastGhostSweepAt;
|
|
7
8
|
run(now: number): void;
|
|
9
|
+
/** Drop rows whose on-disk node dir is gone. Such a row can never be revived,
|
|
10
|
+
* focused, or closed — every one of those resolves the node through meta.json —
|
|
11
|
+
* so it sits in the roster jamming the surfaces that try. One readdir per sweep. */
|
|
12
|
+
private reapGhostRows;
|
|
8
13
|
/** Milliseconds since meta/telemetry was last written. Inbox activity is
|
|
9
14
|
* deliberately excluded: children can keep writing a dead manager's inbox. */
|
|
10
15
|
private msSinceDiskActivity;
|