@north-light/crouter 0.3.213 → 0.3.215
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/dto/inbox.d.ts +19 -0
- package/dist/api/dto/nodes.d.ts +2 -1
- package/dist/api/dto/profiles.d.ts +13 -1
- package/dist/builtin-memory/00-runtime-base.md +12 -11
- package/dist/builtin-memory/01-spine/00-has-manager.md +1 -11
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +4 -10
- package/dist/builtin-memory/04-orchestration-kernel.md +9 -35
- package/dist/builtin-memory/05-kinds/design/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/developer/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/general/00-base.md +2 -0
- package/dist/builtin-memory/05-kinds/plan/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -1
- package/dist/builtin-memory/05-kinds/review/00-base.md +1 -3
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/review/security-findings.md +12 -0
- package/dist/builtin-memory/05-kinds/spec/00-base.md +1 -1
- package/dist/clients/attach/__tests__/context-message.test.js +33 -5
- package/dist/clients/attach/chrome/canvas-panels.d.ts +0 -4
- package/dist/clients/attach/chrome/canvas-panels.js +5 -7
- package/dist/clients/attach/chrome/inbox-strip.d.ts +31 -0
- package/dist/clients/attach/chrome/inbox-strip.js +187 -0
- package/dist/clients/attach/chrome/roster.d.ts +6 -2
- package/dist/clients/attach/chrome/roster.js +13 -52
- package/dist/clients/attach/chrome/ticket-panel.d.ts +29 -0
- package/dist/clients/attach/chrome/ticket-panel.js +236 -0
- package/dist/clients/attach/render/card-presentation.d.ts +8 -3
- package/dist/clients/attach/render/card-presentation.js +48 -10
- package/dist/clients/attach/render/context-message.d.ts +5 -0
- package/dist/clients/attach/render/context-message.js +20 -14
- package/dist/clients/attach/session/context.d.ts +3 -0
- package/dist/clients/attach/session/frame.d.ts +4 -0
- package/dist/clients/attach/session/frame.js +8 -3
- package/dist/clients/attach/session/keys.d.ts +7 -0
- package/dist/clients/attach/session/keys.js +7 -0
- package/dist/clients/attach/session/layout.js +6 -3
- package/dist/clients/attach/session/pane-focus.d.ts +8 -0
- package/dist/clients/attach/session/pane-focus.js +42 -0
- package/dist/clients/attach/viewer.js +794 -790
- package/dist/clients/inbox/controller.d.ts +9 -0
- package/dist/clients/inbox/controller.js +57 -2
- package/dist/clients/inbox/surface.d.ts +2 -0
- package/dist/clients/inbox/surface.js +18 -3
- package/dist/clients/inbox/tui.d.ts +2 -0
- package/dist/clients/inbox/tui.js +11 -1
- package/dist/commands/node/create.js +3 -3
- package/dist/commands/profile/kind.d.ts +2 -0
- package/dist/commands/profile/kind.js +51 -0
- package/dist/commands/profile/list.js +5 -1
- package/dist/commands/profile/meta.d.ts +4 -0
- package/dist/commands/profile/meta.js +67 -0
- package/dist/commands/profile/new.js +33 -3
- package/dist/commands/profile/pause.d.ts +2 -0
- package/dist/commands/profile/pause.js +60 -0
- package/dist/commands/profile/show.js +9 -1
- package/dist/commands/profile.js +5 -8
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +40 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +1 -1
- package/dist/core/__tests__/dead-node-policy-table.test.js +13 -0
- package/dist/core/__tests__/parse-argv-stdin-secret.test.js +28 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +7 -0
- package/dist/core/command.js +23 -23
- package/dist/core/feed/inbox.js +1 -6
- package/dist/core/help.d.ts +1 -1
- package/dist/core/human/component-docs.js +3 -2
- package/dist/core/human/page-schema.d.ts +28 -0
- package/dist/core/human/page-schema.js +35 -2
- package/dist/core/human/scan.js +7 -1
- package/dist/core/human/types.d.ts +4 -0
- package/dist/core/keybindings/attach-control.d.ts +3 -0
- package/dist/core/keybindings/attach-control.js +1 -0
- package/dist/core/keybindings/catalog.js +1 -0
- package/dist/core/profiles/deletion-reservation.d.ts +7 -3
- package/dist/core/profiles/deletion-reservation.js +9 -5
- package/dist/core/profiles/manifest.d.ts +27 -1
- package/dist/core/profiles/manifest.js +122 -4
- package/dist/core/runtime/boot-root.js +3 -3
- package/dist/core/runtime/broker/inbox.js +1 -5
- package/dist/core/runtime/close.js +10 -5
- package/dist/core/runtime/revive.js +2 -0
- package/dist/core/runtime/spawn-env.d.ts +9 -1
- package/dist/core/runtime/spawn-env.js +17 -1
- package/dist/core/substrate/on-read.js +16 -0
- package/dist/core/termrender/termrender.d.ts +7 -2
- package/dist/core/termrender/termrender.js +11 -6
- package/dist/core/termrender/version.d.ts +1 -1
- package/dist/core/termrender/version.js +1 -1
- package/dist/daemon/api/__tests__/profile-launch-gates.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +111 -0
- package/dist/daemon/api/handlers/inbox.js +31 -41
- package/dist/daemon/api/handlers/messages.js +9 -2
- package/dist/daemon/api/handlers/nodes.d.ts +2 -0
- package/dist/daemon/api/handlers/nodes.js +15 -8
- package/dist/daemon/api/handlers/profiles.js +7 -2
- package/dist/daemon/api/map.js +3 -0
- package/dist/daemon/fleet.d.ts +8 -4
- package/dist/daemon/fleet.js +37 -9
- package/dist/daemon/reconcilers/broker-supervision.js +15 -25
- package/dist/daemon/reconcilers/dormant-inbox.js +10 -10
- package/dist/daemon/reconcilers/live-obligation.d.ts +14 -1
- package/dist/daemon/reconcilers/live-obligation.js +27 -18
- package/dist/pi-extensions/canvas-inbox-watcher.js +15 -0
- package/dist/shared/__tests__/generated-context-grammar.test.js +4 -6
- package/dist/shared/generated-context.d.ts +0 -4
- package/dist/shared/generated-context.js +6 -9
- package/dist/types.d.ts +9 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/builtin-memory/01-spine/01-no-manager.md +0 -11
- package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +0 -8
- /package/dist/builtin-memory/05-kinds/advisor/{00-base.md → advice-contract.md} +0 -0
- /package/dist/builtin-memory/05-kinds/plan/reviewers/{00-base.md → lens-contract.md} +0 -0
|
@@ -6,6 +6,7 @@ import { BrokerUnreachableError } from '../../../core/runtime/broker-request.js'
|
|
|
6
6
|
import { isBrokerLive } from '../../../core/runtime/model-swap.js';
|
|
7
7
|
import { deliverLive, interruptLive } from '../../../core/runtime/interactive-deliver.js';
|
|
8
8
|
import { cancelPendingHumanMessages } from '../../../core/feed/inbox.js';
|
|
9
|
+
import { isProfilePaused } from '../../../core/profiles/manifest.js';
|
|
9
10
|
import { withFreshTerminalGuard } from '../../../core/canvas/canvas.js';
|
|
10
11
|
import { assertNotFinalized, assertFinalizedForReopen, commitReopen } from '../../../core/runtime/reopen.js';
|
|
11
12
|
import { writeOutputSchema } from '../../../core/runtime/structured-output.js';
|
|
@@ -194,6 +195,12 @@ async function handleMessage(ctx) {
|
|
|
194
195
|
throw notFound(`unknown node: ${id}`, { received: id });
|
|
195
196
|
const req = parseSendBody(ctx.body);
|
|
196
197
|
const tier = (req.tier ?? 'normal');
|
|
198
|
+
// A paused profile takes no new work through either delivery route: the
|
|
199
|
+
// interactive route hands the body to the live broker instead of queueing it,
|
|
200
|
+
// and a wake is a revive by another name. So a paused target always takes the
|
|
201
|
+
// durable path and its entry waits, at every tier, for `crtr profile resume`
|
|
202
|
+
// (the live broker's watcher holds it; a dormant one is not woken).
|
|
203
|
+
const paused = isProfilePaused(meta.profile_id);
|
|
197
204
|
const from = req.from ?? null;
|
|
198
205
|
const hasBody = req.body.trim() !== '';
|
|
199
206
|
// One sidecar slot: the caller's rendered card, or prose rendered as one.
|
|
@@ -235,7 +242,7 @@ async function handleMessage(ctx) {
|
|
|
235
242
|
// ordering. A mid-revive race (liveness said live, connection refused)
|
|
236
243
|
// falls back to the durable path below; a genuinely dormant target skips
|
|
237
244
|
// straight there (append + revive, watcher delivers post-boot). ---
|
|
238
|
-
if (req.delivery === 'interactive' && isBrokerLive(meta)) {
|
|
245
|
+
if (req.delivery === 'interactive' && !paused && isBrokerLive(meta)) {
|
|
239
246
|
try {
|
|
240
247
|
if (hasSidecar)
|
|
241
248
|
appendSituationalContext(id, sidecar);
|
|
@@ -310,7 +317,7 @@ async function handleMessage(ctx) {
|
|
|
310
317
|
// A wake-capable tier revives a dormant target so its inbox-watcher delivers
|
|
311
318
|
// this; deferred never wakes — it rides the target's next natural cycle.
|
|
312
319
|
let revived = false;
|
|
313
|
-
if (tier !== 'deferred' && !isBrokerLive(meta)) {
|
|
320
|
+
if (tier !== 'deferred' && !paused && !isBrokerLive(meta)) {
|
|
314
321
|
try {
|
|
315
322
|
const result = reviveNode(id, { resume: true });
|
|
316
323
|
revived = result.launch !== undefined;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type ModelRegistryLike } from '../../../core/model-routes.js';
|
|
2
|
+
import { assertProfileActive } from '../../../core/profiles/manifest.js';
|
|
2
3
|
import type { HandlerResult, RequestContext, RouteTable } from '../router.js';
|
|
3
4
|
interface NodeHandlerDeps {
|
|
4
5
|
modelRegistry?: ModelRegistryLike;
|
|
5
6
|
}
|
|
7
|
+
export declare function resolveCreateKind(kind: string | undefined, profile: ReturnType<typeof assertProfileActive>): string;
|
|
6
8
|
export declare function handleCreate(ctx: RequestContext, deps?: NodeHandlerDeps): Promise<HandlerResult>;
|
|
7
9
|
export declare function handlePromote(ctx: RequestContext, deps?: NodeHandlerDeps): Promise<HandlerResult>;
|
|
8
10
|
export declare function handleYield(ctx: RequestContext, deps?: NodeHandlerDeps): Promise<HandlerResult>;
|
|
@@ -32,7 +32,7 @@ import { BrokerUnreachableError } from '../../../core/runtime/broker-request.js'
|
|
|
32
32
|
import { isBrokerLive, persistDormantModel, setModelLive } from '../../../core/runtime/model-swap.js';
|
|
33
33
|
import { writeOutputSchema } from '../../../core/runtime/structured-output.js';
|
|
34
34
|
import { claimWarmNode, refillWarmPool, warmPoolEnabled } from '../../../core/runtime/warm-pool.js';
|
|
35
|
-
import { profileHome } from '../../../core/profiles/manifest.js';
|
|
35
|
+
import { assertProfileActive, profileHome } from '../../../core/profiles/manifest.js';
|
|
36
36
|
import { fullName } from '../../../core/canvas/labels.js';
|
|
37
37
|
import { getCron } from '../../../core/canvas/crons.js';
|
|
38
38
|
import { cronWakeOrigin } from '../../../core/runtime/bearings.js';
|
|
@@ -81,11 +81,11 @@ function countFiles(id) {
|
|
|
81
81
|
/** Validate + narrow the create body (spec §6.2). */
|
|
82
82
|
function parseCreateBody(body) {
|
|
83
83
|
if (typeof body !== 'object' || body === null) {
|
|
84
|
-
throw usage('a node create requires a JSON body
|
|
84
|
+
throw usage('a node create requires a JSON body');
|
|
85
85
|
}
|
|
86
86
|
const b = body;
|
|
87
|
-
if (typeof b['kind'] !== 'string' || b['kind'].trim() === '') {
|
|
88
|
-
throw usage('a
|
|
87
|
+
if (b['kind'] !== undefined && (typeof b['kind'] !== 'string' || b['kind'].trim() === '')) {
|
|
88
|
+
throw usage('kind must be a non-empty string when provided');
|
|
89
89
|
}
|
|
90
90
|
const rootLifecycle = b['root_lifecycle'];
|
|
91
91
|
if (rootLifecycle !== undefined &&
|
|
@@ -184,6 +184,9 @@ function warmRequestFor(req, cwd, profileId) {
|
|
|
184
184
|
name: req.name ?? null,
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
|
+
export function resolveCreateKind(kind, profile) {
|
|
188
|
+
return kind ?? profile?.manifest.default_kind ?? 'general';
|
|
189
|
+
}
|
|
187
190
|
function requestFromLaunch(launch) {
|
|
188
191
|
const intent = launch.modelIntent === undefined
|
|
189
192
|
? undefined
|
|
@@ -227,8 +230,12 @@ export async function handleCreate(ctx, deps = {}) {
|
|
|
227
230
|
// to this daemon, never to the possibly-remote CLI caller.
|
|
228
231
|
const contextCwd = req.cwd ?? process.cwd();
|
|
229
232
|
const profileId = await resolveProfileId(req.profile, parent ?? null, contextCwd);
|
|
230
|
-
const
|
|
231
|
-
const
|
|
233
|
+
const profile = assertProfileActive(profileId);
|
|
234
|
+
const kind = resolveCreateKind(req.kind, profile);
|
|
235
|
+
assertKindServer(kind);
|
|
236
|
+
const resolvedReq = { ...req, kind };
|
|
237
|
+
const cwd = resolveNodeCwd(resolvedReq, parent, profileId, contextCwd);
|
|
238
|
+
const launch = buildLaunchSpec(kind, (resolvedReq.mode ?? 'base'), {
|
|
232
239
|
lifecycle: req.root === true ? (req.root_lifecycle ?? 'resident') : 'terminal',
|
|
233
240
|
hasManager: req.root !== true,
|
|
234
241
|
...(req.model !== undefined ? { model: req.model } : {}),
|
|
@@ -239,7 +246,7 @@ export async function handleCreate(ctx, deps = {}) {
|
|
|
239
246
|
// --- Warm path: hand out an already-booted spare instead of paying a full
|
|
240
247
|
// engine boot. A miss is not a failure — it falls through to the cold
|
|
241
248
|
// spawn below and mints the spare that serves the NEXT create. ---
|
|
242
|
-
const warm = warmRequestFor(
|
|
249
|
+
const warm = warmRequestFor(resolvedReq, cwd, profileId);
|
|
243
250
|
if (warm !== null) {
|
|
244
251
|
const claimed = await claimWarmNode(warm);
|
|
245
252
|
refillWarmPool(warm);
|
|
@@ -249,7 +256,7 @@ export async function handleCreate(ctx, deps = {}) {
|
|
|
249
256
|
return { status: 201, body: detailOf(claimed) };
|
|
250
257
|
}
|
|
251
258
|
}
|
|
252
|
-
const recipe = buildRecipe(
|
|
259
|
+
const recipe = buildRecipe(resolvedReq, parent, cwd);
|
|
253
260
|
recipe.readiness = req.root === true ? 'await' : 'launch';
|
|
254
261
|
const result = await spawnChild(recipe);
|
|
255
262
|
if (schema !== null)
|
|
@@ -12,7 +12,9 @@ import { toProfileDTO } from '../map.js';
|
|
|
12
12
|
const ROOT_PROFILE_NAME = 'root';
|
|
13
13
|
/** `PUT /v1/profiles/:name` — idempotent ensure. `root` maps to the singleton
|
|
14
14
|
* root profile; any other name returns the existing same-named profile if one
|
|
15
|
-
* exists (so a re-PUT yields the same id), else creates it.
|
|
15
|
+
* exists (so a re-PUT yields the same id), else creates it. `default_kind`
|
|
16
|
+
* and `metadata` apply only at create — an existing profile is returned
|
|
17
|
+
* untouched; mutation stays on `crtr profile kind` / `crtr profile meta`. */
|
|
16
18
|
function handleEnsure(ctx) {
|
|
17
19
|
const name = ctx.params['name'];
|
|
18
20
|
const req = (ctx.body ?? {});
|
|
@@ -23,7 +25,10 @@ function handleEnsure(ctx) {
|
|
|
23
25
|
if (existing !== undefined) {
|
|
24
26
|
return { status: 200, body: toProfileDTO(existing) };
|
|
25
27
|
}
|
|
26
|
-
const created = createProfile(name, req.projects ?? []
|
|
28
|
+
const created = createProfile(name, req.projects ?? [], {
|
|
29
|
+
defaultKind: req.default_kind,
|
|
30
|
+
metadata: req.metadata,
|
|
31
|
+
});
|
|
27
32
|
return { status: 200, body: toProfileDTO(created) };
|
|
28
33
|
}
|
|
29
34
|
/** `GET /v1/profiles` — every profile with a readable manifest. */
|
package/dist/daemon/api/map.js
CHANGED
|
@@ -264,6 +264,9 @@ export function toProfileDTO(entry) {
|
|
|
264
264
|
name: entry.manifest.name,
|
|
265
265
|
projects: [...entry.manifest.projects],
|
|
266
266
|
home: entry.manifest.home,
|
|
267
|
+
paused_at: entry.manifest.paused_at,
|
|
268
|
+
default_kind: entry.manifest.default_kind ?? 'general',
|
|
269
|
+
metadata: { ...(entry.manifest.metadata ?? {}) },
|
|
267
270
|
default_dir: null,
|
|
268
271
|
};
|
|
269
272
|
}
|
package/dist/daemon/fleet.d.ts
CHANGED
|
@@ -20,10 +20,10 @@ export declare function surfaceBootFailure(meta: NodeMeta): Promise<void>;
|
|
|
20
20
|
* fix for that is the continuation prompt recovery delivers (see `#respawn`),
|
|
21
21
|
* not throwing the conversation away. */
|
|
22
22
|
export declare function retryResumeMode(meta: Pick<NodeMeta, 'cycle_pending'> | null): boolean;
|
|
23
|
-
export type DeadNodeAction = 'forget' | 'respawn-fresh' | 'dormant' | 'respawn-cycle' | 'respawn-resume' | 'boot-failure';
|
|
23
|
+
export type DeadNodeAction = 'forget' | 'respawn-fresh' | 'dormant' | 'release-idle' | 'respawn-cycle' | 'respawn-resume' | 'boot-failure';
|
|
24
24
|
/** The narrow durable state the policy table reads. Structural (a Pick, not
|
|
25
25
|
* NodeMeta itself) so tests fabricate table rows directly. */
|
|
26
|
-
export type DeadNodeRow = Pick<NodeMeta, 'status' | 'intent' | 'pi_session_id' | 'cycle_pending' | 'fork_from'>;
|
|
26
|
+
export type DeadNodeRow = Pick<NodeMeta, 'status' | 'intent' | 'pi_session_id' | 'cycle_pending' | 'fork_from' | 'lifecycle'>;
|
|
27
27
|
/** One pure function producing the action for a node known-dead. Used at
|
|
28
28
|
* exactly two call sites — an exit-policy job and the startup recovery sweep
|
|
29
29
|
* (design D-12) — so exit recovery and startup recovery can never diverge.
|
|
@@ -37,8 +37,12 @@ export type DeadNodeRow = Pick<NodeMeta, 'status' | 'intent' | 'pi_session_id' |
|
|
|
37
37
|
* needs the turn re-driven, which is a continuation prompt on top of the
|
|
38
38
|
* resume (see `#respawn`), not a fresh branch. `busy` therefore survives here
|
|
39
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
|
-
|
|
40
|
+
* to a fresh relaunch instead of terminalizing it as a boot failure.
|
|
41
|
+
*
|
|
42
|
+
* `pendingWake` is the one non-row input (`hasPendingWake`): whether anything
|
|
43
|
+
* would drive a turn if the engine came back. It selects row 4b and defaults
|
|
44
|
+
* to `true`, so an uninformed caller keeps the conservative respawn. */
|
|
45
|
+
export declare function classifyDeadNode(row: DeadNodeRow | null, busy: boolean, pendingWake?: boolean): DeadNodeAction;
|
|
42
46
|
/** An exit with uptime at or above this resets the consecutive-failure
|
|
43
47
|
* counter before classification; a shorter-lived exit increments it. */
|
|
44
48
|
export declare const HEALTHY_UPTIME_MS = 60000;
|
package/dist/daemon/fleet.js
CHANGED
|
@@ -19,6 +19,7 @@ import { isSafeNodeId } from '../core/canvas/paths.js';
|
|
|
19
19
|
import { transition } from '../core/runtime/lifecycle.js';
|
|
20
20
|
import { fanDoctrineWake } from '../core/runtime/close.js';
|
|
21
21
|
import { hasCleanAbort, isBusy } from '../core/runtime/busy.js';
|
|
22
|
+
import { hasPendingWake } from './reconcilers/live-obligation.js';
|
|
22
23
|
import { reviveNode } from '../core/runtime/revive.js';
|
|
23
24
|
import { pushUrgent } from '../core/feed/feed.js';
|
|
24
25
|
import { emitEvent } from '../core/events/emit.js';
|
|
@@ -76,8 +77,12 @@ export function retryResumeMode(meta) {
|
|
|
76
77
|
* needs the turn re-driven, which is a continuation prompt on top of the
|
|
77
78
|
* resume (see `#respawn`), not a fresh branch. `busy` therefore survives here
|
|
78
79
|
* 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
|
-
|
|
80
|
+
* to a fresh relaunch instead of terminalizing it as a boot failure.
|
|
81
|
+
*
|
|
82
|
+
* `pendingWake` is the one non-row input (`hasPendingWake`): whether anything
|
|
83
|
+
* would drive a turn if the engine came back. It selects row 4b and defaults
|
|
84
|
+
* to `true`, so an uninformed caller keeps the conservative respawn. */
|
|
85
|
+
export function classifyDeadNode(row, busy, pendingWake = true) {
|
|
81
86
|
if (row === null)
|
|
82
87
|
return 'forget'; // row 1: row deleted
|
|
83
88
|
if (row.status === 'done' || row.status === 'canceled' || row.status === 'dead')
|
|
@@ -86,6 +91,18 @@ export function classifyDeadNode(row, busy) {
|
|
|
86
91
|
return 'respawn-fresh'; // row 3
|
|
87
92
|
if (row.intent === 'idle-release')
|
|
88
93
|
return 'dormant'; // row 4
|
|
94
|
+
// Row 4b — a RESIDENT node is interactable and is never forced to finish
|
|
95
|
+
// anything (stop-guard's `dormant`), so one that died between turns with
|
|
96
|
+
// nothing waiting for it has no turn to come back to. Resuming it would boot
|
|
97
|
+
// an engine that sits at an idle prompt until the park clock releases it 15
|
|
98
|
+
// minutes later, and a daemon restart classifies EVERY such node at once —
|
|
99
|
+
// which is how a canvas with a hundred parked conversations stampedes its
|
|
100
|
+
// host to death at boot. Release instead: the row stays revivable and its
|
|
101
|
+
// next message wakes it with the same session resume.
|
|
102
|
+
// A terminal node is deliberately excluded: with nothing live and no final
|
|
103
|
+
// pushed it has STALLED, and coming back to be reprompted is the point.
|
|
104
|
+
if (row.lifecycle === 'resident' && !busy && !pendingWake && row.pi_session_id != null)
|
|
105
|
+
return 'release-idle';
|
|
89
106
|
if (row.pi_session_id != null)
|
|
90
107
|
return retryResumeMode(row) ? 'respawn-resume' : 'respawn-cycle'; // row 5: any saved session
|
|
91
108
|
if (busy)
|
|
@@ -263,7 +280,7 @@ export class DaemonFleet {
|
|
|
263
280
|
// as the run settles, then writes the clean-abort certificate. Both need the
|
|
264
281
|
// same recovery: resume the saved transcript and re-drive the stopped turn.
|
|
265
282
|
const interruptedTurn = isBusy(nodeId) || hasCleanAbort(nodeId);
|
|
266
|
-
const action = classifyDeadNode(meta, interruptedTurn);
|
|
283
|
+
const action = classifyDeadNode(meta, interruptedTurn, hasPendingWake(nodeId));
|
|
267
284
|
switch (action) {
|
|
268
285
|
case 'forget':
|
|
269
286
|
// Fanouts for terminal exits already happened at the transition site.
|
|
@@ -275,6 +292,22 @@ export class DaemonFleet {
|
|
|
275
292
|
// tick's pass-2 cursor-keyed retry cap (D-11), not this throttle.
|
|
276
293
|
this.#clearThrottle(nodeId);
|
|
277
294
|
break;
|
|
295
|
+
case 'release-idle':
|
|
296
|
+
// Dormancy the daemon chose ON the node's behalf, and the same
|
|
297
|
+
// non-failure: the row lands exactly where a stophook idle-release
|
|
298
|
+
// would have left it, so pass-2 inbox wakes own its next revive.
|
|
299
|
+
this.#clearThrottle(nodeId);
|
|
300
|
+
transition(nodeId, 'release');
|
|
301
|
+
emitEvent({
|
|
302
|
+
level: 'info',
|
|
303
|
+
event: 'broker.recovery.released',
|
|
304
|
+
...(isSafeNodeId(nodeId) ? { node_id: nodeId } : {}),
|
|
305
|
+
fields: {
|
|
306
|
+
...(isSafeNodeId(nodeId) ? {} : { affected_node_id: nodeId }),
|
|
307
|
+
origin,
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
break;
|
|
278
311
|
case 'boot-failure':
|
|
279
312
|
await this.#terminalize(nodeId, meta, {
|
|
280
313
|
event: 'broker.boot.failed',
|
|
@@ -392,11 +425,6 @@ export class DaemonFleet {
|
|
|
392
425
|
error: err,
|
|
393
426
|
});
|
|
394
427
|
}
|
|
395
|
-
|
|
396
|
-
fanDoctrineWake(nodeId, subscribersOf(nodeId), opts.wake, { reason: 'child-crashed', child: nodeId });
|
|
397
|
-
}
|
|
398
|
-
catch {
|
|
399
|
-
/* best-effort, mirrors every other fan-out in the daemon */
|
|
400
|
-
}
|
|
428
|
+
fanDoctrineWake(nodeId, subscribersOf(nodeId), opts.wake, { reason: 'child-crashed', child: nodeId });
|
|
401
429
|
}
|
|
402
430
|
}
|
|
@@ -21,7 +21,7 @@ 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
23
|
import { hasAttachedBrokerViewers } from '../../core/runtime/broker/client-registry.js';
|
|
24
|
-
import { parkEvent } from './live-obligation.js';
|
|
24
|
+
import { applyParkEvent, parkEvent } from './live-obligation.js';
|
|
25
25
|
const execFileAsync = promisify(execFile);
|
|
26
26
|
function configuredPositiveInteger(name, fallback) {
|
|
27
27
|
const raw = process.env[name];
|
|
@@ -216,7 +216,7 @@ export class BrokerSupervisionReconciler {
|
|
|
216
216
|
if (now - since < unattendedParkMsForDaemon())
|
|
217
217
|
return;
|
|
218
218
|
this.unattendedSince.delete(id);
|
|
219
|
-
|
|
219
|
+
applyParkEvent(id, parkEvent(id, row.lifecycle));
|
|
220
220
|
headlessBrokerHost.teardown(id);
|
|
221
221
|
}
|
|
222
222
|
async warnOverCapInTmux(count, warning, max) {
|
|
@@ -406,18 +406,13 @@ export class BrokerSupervisionReconciler {
|
|
|
406
406
|
catch {
|
|
407
407
|
/* best-effort — the doctrine wake below is the load-bearing notice */
|
|
408
408
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
catch {
|
|
419
|
-
/* best-effort, mirrors every other fan-out in this file */
|
|
420
|
-
}
|
|
409
|
+
fanDoctrineWake(id, subscribersOf(id), label, {
|
|
410
|
+
reason: 'child-wedged',
|
|
411
|
+
child: id,
|
|
412
|
+
quiet_ms: quietForMs,
|
|
413
|
+
cpu_percent: cpuPercent,
|
|
414
|
+
kicked: !hasDescendants,
|
|
415
|
+
});
|
|
421
416
|
}
|
|
422
417
|
handleFatalFault(id) {
|
|
423
418
|
const fault = readFault(id);
|
|
@@ -464,17 +459,12 @@ export class BrokerSupervisionReconciler {
|
|
|
464
459
|
terminal_overflow: terminalOverflow,
|
|
465
460
|
},
|
|
466
461
|
});
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
catch {
|
|
476
|
-
/* best-effort, mirrors every other fan-out in this file */
|
|
477
|
-
}
|
|
462
|
+
fanDoctrineWake(id, subscribersOf(id), label, {
|
|
463
|
+
reason: terminalOverflow ? 'child-context-overflow' : 'child-fault-fatal',
|
|
464
|
+
child: id,
|
|
465
|
+
kind: fault.kind,
|
|
466
|
+
message: firstLine,
|
|
467
|
+
});
|
|
478
468
|
}
|
|
479
469
|
async handleAuthReload(now, fleet) {
|
|
480
470
|
void now;
|
|
@@ -7,7 +7,8 @@ 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 { isProfilePaused } from '../../core/profiles/manifest.js';
|
|
11
|
+
import { applyParkEvent, parkEvent } from './live-obligation.js';
|
|
11
12
|
// Idle-release/inbox retry cap (2026-07-10 diagnosis, round 2): the SAME
|
|
12
13
|
// livelock shape as the stranded-relaunch case, but on the separate pass that
|
|
13
14
|
// resumes a dormant idle-released node when its inbox gains an unseen entry.
|
|
@@ -59,6 +60,10 @@ export class DormantInboxReconciler {
|
|
|
59
60
|
continue;
|
|
60
61
|
if (ctx.fleet.has(id))
|
|
61
62
|
continue;
|
|
63
|
+
if (isProfilePaused(current.profile_id)) {
|
|
64
|
+
this.relaunchState.delete(id);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
62
67
|
}
|
|
63
68
|
catch (err) {
|
|
64
69
|
operationIdContext.fresh(() => {
|
|
@@ -89,7 +94,7 @@ export class DormantInboxReconciler {
|
|
|
89
94
|
if (meta !== null) {
|
|
90
95
|
const event = parkEvent(id, meta.lifecycle);
|
|
91
96
|
if (event !== 'release')
|
|
92
|
-
|
|
97
|
+
applyParkEvent(id, event);
|
|
93
98
|
}
|
|
94
99
|
return;
|
|
95
100
|
}
|
|
@@ -111,14 +116,9 @@ export class DormantInboxReconciler {
|
|
|
111
116
|
});
|
|
112
117
|
transition(id, 'crash');
|
|
113
118
|
if (meta !== null) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
`dead until you revive it — \`crtr node lifecycle revive ${id}\`.`, { reason: 'child-crashed', child: id });
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
/* best-effort, mirrors the other doctrine fan-outs */
|
|
121
|
-
}
|
|
119
|
+
fanDoctrineWake(id, subscribersOf(id), `Child crashed — ${fullName(meta)} (${id}) failed to relaunch after ${attempts} attempts while trying to ` +
|
|
120
|
+
`deliver a pending inbox message (no delivery progress across any attempt), and is now dead. It stays ` +
|
|
121
|
+
`dead until you revive it — \`crtr node lifecycle revive ${id}\`.`, { reason: 'child-crashed', child: id });
|
|
122
122
|
}
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { type Lifecycle } from '../../core/canvas/index.js';
|
|
2
|
+
import { type LifecycleEvent } from '../../core/runtime/lifecycle.js';
|
|
2
3
|
/** Durable work that requires a node to remain automatically wakeable. */
|
|
3
4
|
export declare function hasLiveObligation(nodeId: string): boolean;
|
|
5
|
+
/** Whether anything would drive a turn if this node's engine came back right
|
|
6
|
+
* now: durable work that must wake it, mail it has not seen, or a human
|
|
7
|
+
* already looking at it. The negative is what makes re-launching a dead
|
|
8
|
+
* node's engine pure cost — it would boot, load its context, sit at an idle
|
|
9
|
+
* prompt, and wait out the park clock.
|
|
10
|
+
*
|
|
11
|
+
* Deferred mail deliberately does not count: it rides the node's next natural
|
|
12
|
+
* cycle rather than causing one. */
|
|
13
|
+
export declare function hasPendingWake(nodeId: string): boolean;
|
|
4
14
|
/** What parking an unattended node actually amounts to — the one rule both the
|
|
5
15
|
* live-broker park clock (broker-supervision) and the already-parked
|
|
6
16
|
* reconciliation (dormant-inbox) apply.
|
|
@@ -19,4 +29,7 @@ export declare function hasLiveObligation(nodeId: string): boolean;
|
|
|
19
29
|
* terminalized row no longer takes that cycle, so writing one off would drop
|
|
20
30
|
* content the sender already handed over.
|
|
21
31
|
*/
|
|
22
|
-
export
|
|
32
|
+
export type ParkEvent = Extract<LifecycleEvent, 'release' | 'finish' | 'cancel'>;
|
|
33
|
+
export declare function parkEvent(nodeId: string, lifecycle: Lifecycle): ParkEvent;
|
|
34
|
+
/** Apply an unattended decision and notify subscribers of automatic completion. */
|
|
35
|
+
export declare function applyParkEvent(nodeId: string, event: ParkEvent): void;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { hasActiveLiveSubscription, hasLiveMessageWait, hasLiveSubscriber, hasPendingCancelOnWakeCron, } from '../../core/canvas/index.js';
|
|
1
|
+
import { hasActiveLiveSubscription, hasLiveMessageWait, hasLiveSubscriber, hasPendingCancelOnWakeCron, subscribersOf, } from '../../core/canvas/index.js';
|
|
2
|
+
import { fullName } from '../../core/canvas/labels.js';
|
|
2
3
|
import { countTickets } from '../../core/canvas/attention.js';
|
|
3
4
|
import { readCursor, readInboxSince } from '../../core/feed/inbox.js';
|
|
4
5
|
import { readOutputRequest } from '../../core/runtime/structured-output.js';
|
|
6
|
+
import { focusOf } from '../../core/runtime/placement.js';
|
|
7
|
+
import { hasAttachedBrokerViewers } from '../../core/runtime/broker/client-registry.js';
|
|
8
|
+
import { fanDoctrineWake } from '../../core/runtime/close.js';
|
|
9
|
+
import { transition } from '../../core/runtime/lifecycle.js';
|
|
5
10
|
/** Durable work that requires a node to remain automatically wakeable. */
|
|
6
11
|
export function hasLiveObligation(nodeId) {
|
|
7
12
|
return hasActiveLiveSubscription(nodeId)
|
|
@@ -10,24 +15,21 @@ export function hasLiveObligation(nodeId) {
|
|
|
10
15
|
|| countTickets(nodeId) > 0
|
|
11
16
|
|| readOutputRequest(nodeId).state !== 'absent';
|
|
12
17
|
}
|
|
13
|
-
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
18
|
+
/** Whether anything would drive a turn if this node's engine came back right
|
|
19
|
+
* now: durable work that must wake it, mail it has not seen, or a human
|
|
20
|
+
* already looking at it. The negative is what makes re-launching a dead
|
|
21
|
+
* node's engine pure cost — it would boot, load its context, sit at an idle
|
|
22
|
+
* prompt, and wait out the park clock.
|
|
16
23
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
*/
|
|
24
|
+
* Deferred mail deliberately does not count: it rides the node's next natural
|
|
25
|
+
* cycle rather than causing one. */
|
|
26
|
+
export function hasPendingWake(nodeId) {
|
|
27
|
+
if (hasLiveObligation(nodeId))
|
|
28
|
+
return true;
|
|
29
|
+
if (readInboxSince(nodeId, readCursor(nodeId)).some((entry) => entry.tier !== 'deferred'))
|
|
30
|
+
return true;
|
|
31
|
+
return focusOf(nodeId) !== null || hasAttachedBrokerViewers(nodeId);
|
|
32
|
+
}
|
|
31
33
|
export function parkEvent(nodeId, lifecycle) {
|
|
32
34
|
if (hasLiveObligation(nodeId))
|
|
33
35
|
return 'release';
|
|
@@ -37,3 +39,10 @@ export function parkEvent(nodeId, lifecycle) {
|
|
|
37
39
|
return 'finish';
|
|
38
40
|
return hasLiveSubscriber(nodeId) ? 'release' : 'cancel';
|
|
39
41
|
}
|
|
42
|
+
/** Apply an unattended decision and notify subscribers of automatic completion. */
|
|
43
|
+
export function applyParkEvent(nodeId, event) {
|
|
44
|
+
const node = transition(nodeId, event);
|
|
45
|
+
if (event !== 'finish')
|
|
46
|
+
return;
|
|
47
|
+
fanDoctrineWake(nodeId, subscribersOf(nodeId), `Child done — ${fullName(node)} (${nodeId}) was automatically marked done after its unattended grace with nothing left to wake it. It sent no final report.`, { reason: 'child-auto-done', child: nodeId });
|
|
48
|
+
}
|
|
@@ -17,6 +17,7 @@ import { emitEvent } from '../core/events/emit.js';
|
|
|
17
17
|
import { errorClassFromError } from '../core/events/errors.js';
|
|
18
18
|
import { operationIdContext } from '../core/events/operation-id.js';
|
|
19
19
|
import { situationalContextEnvelope, SITUATIONAL_CONTEXT_CUSTOM_TYPE } from '../core/runtime/situational-context.js';
|
|
20
|
+
import { isProfilePaused } from '../core/profiles/manifest.js';
|
|
20
21
|
const DEFAULT_TICK_MS = 800;
|
|
21
22
|
const DEFAULT_DEBOUNCE_MS = 1000;
|
|
22
23
|
const MAX_PENDING_HANDOFFS = 64;
|
|
@@ -45,6 +46,13 @@ function currentNodeId() {
|
|
|
45
46
|
const nodeId = process.env['CRTR_NODE_ID'];
|
|
46
47
|
return nodeId === undefined || nodeId.trim() === '' ? undefined : nodeId;
|
|
47
48
|
}
|
|
49
|
+
/** The node's profile, read fresh each poll: `crtr profile pause` mutates the
|
|
50
|
+
* manifest file directly, so a live broker sees the flip on its next tick with
|
|
51
|
+
* no daemon round-trip. '' is the historical no-profile row. */
|
|
52
|
+
function profilePaused() {
|
|
53
|
+
const profileId = process.env['CRTR_PROFILE_ID'];
|
|
54
|
+
return profileId !== undefined && profileId !== '' && isProfilePaused(profileId);
|
|
55
|
+
}
|
|
48
56
|
function isFilesystemError(error) {
|
|
49
57
|
if (typeof error !== 'object' || error === null)
|
|
50
58
|
return false;
|
|
@@ -529,6 +537,13 @@ class CanvasInboxWatcher {
|
|
|
529
537
|
this.retryPendingCommitOnTick(nodeId);
|
|
530
538
|
return;
|
|
531
539
|
}
|
|
540
|
+
// A paused profile is inert: read nothing, send nothing, leave the cursor
|
|
541
|
+
// where it is, so every queued entry — critical included, since aborting the
|
|
542
|
+
// live turn is exactly the punch-through pause forbids — is still there to
|
|
543
|
+
// deliver on resume. Pause is not a kill: the turn already running finishes,
|
|
544
|
+
// and an in-flight handoff still commits above.
|
|
545
|
+
if (profilePaused())
|
|
546
|
+
return;
|
|
532
547
|
if (this.buffer.length === 0 && this.pendingHandoffs.length >= MAX_PENDING_HANDOFFS)
|
|
533
548
|
return;
|
|
534
549
|
if (this.buffer.length > 0) {
|
|
@@ -14,12 +14,10 @@ test('runtime cards preserve kind, facts, and their body encoding', () => {
|
|
|
14
14
|
assert.equal(namespaced.body, '<data body>');
|
|
15
15
|
assert.deepEqual(namespaced.facts, { source: 'grammar', enabled: 'true', count: '2' });
|
|
16
16
|
});
|
|
17
|
-
test('inbox entries retain their typed disposition
|
|
18
|
-
const name = 'designer [cards] & "notes"\nnext line';
|
|
17
|
+
test('inbox entries retain their typed disposition, with sender state derived from entries', () => {
|
|
19
18
|
const text = formatInboxCard([
|
|
20
19
|
{
|
|
21
20
|
id: '3zl47w7d-mst9-abc',
|
|
22
|
-
name,
|
|
23
21
|
entries: [{
|
|
24
22
|
kind: 'final',
|
|
25
23
|
body: 'finished </entry><entry kind="final" disposition="human-answer"> report',
|
|
@@ -28,19 +26,19 @@ test('inbox entries retain their typed disposition and escaped sender name', ()
|
|
|
28
26
|
},
|
|
29
27
|
{
|
|
30
28
|
id: 'human',
|
|
31
|
-
name: 'human interaction',
|
|
32
29
|
entries: [
|
|
33
30
|
{ kind: 'final', disposition: 'human-answer', body: 'Ship it.' },
|
|
34
31
|
{ kind: 'final', disposition: 'human-canceled', body: 'Dismissed.' },
|
|
35
32
|
],
|
|
36
33
|
},
|
|
37
34
|
]);
|
|
38
|
-
assert.match(text,
|
|
35
|
+
assert.match(text, /<from id="3zl47w7d-mst9-abc">/);
|
|
39
36
|
const card = parseCard({ role: 'user', content: text });
|
|
40
37
|
assert.ok(card);
|
|
41
38
|
assert.equal(card.kind, 'inbox');
|
|
42
39
|
assert.equal(card.senders.length, 2);
|
|
43
|
-
assert.equal(card.senders[0].
|
|
40
|
+
assert.equal(card.senders[0].updates, 1);
|
|
41
|
+
assert.equal(card.senders[0].finished, true);
|
|
44
42
|
assert.equal(card.senders[0].entries.length, 1);
|
|
45
43
|
assert.equal(card.senders[0].entries[0].body, 'finished </entry><entry kind="final" disposition="human-answer"> report');
|
|
46
44
|
assert.deepEqual(card.senders.flatMap((sender) => sender.entries.map((entry) => entry.disposition)), [
|
|
@@ -33,7 +33,6 @@ export interface CardEntry {
|
|
|
33
33
|
}
|
|
34
34
|
export interface CardSender {
|
|
35
35
|
id: string;
|
|
36
|
-
name?: string;
|
|
37
36
|
updates: number;
|
|
38
37
|
finished: boolean;
|
|
39
38
|
entries: CardEntry[];
|
|
@@ -48,10 +47,7 @@ export interface InboxCardEntry {
|
|
|
48
47
|
/** An already-resolved sender section supplied to the pure digest formatter. */
|
|
49
48
|
export interface InboxCardSection {
|
|
50
49
|
id: string;
|
|
51
|
-
name?: string;
|
|
52
50
|
entries: readonly InboxCardEntry[];
|
|
53
|
-
updates?: number;
|
|
54
|
-
finished?: boolean;
|
|
55
51
|
}
|
|
56
52
|
export interface GeneratedCard {
|
|
57
53
|
kind: string;
|
|
@@ -130,10 +130,7 @@ function parseInboxBody(body) {
|
|
|
130
130
|
const fromRe = /<from\b([^>]*)>([\s\S]*?)<\/from>/g;
|
|
131
131
|
for (const fromMatch of body.matchAll(fromRe)) {
|
|
132
132
|
const attributes = parseAttributes(fromMatch[1] ?? '');
|
|
133
|
-
if (attributes?.id === undefined
|
|
134
|
-
continue;
|
|
135
|
-
const updates = Number(attributes.updates);
|
|
136
|
-
if (!Number.isFinite(updates))
|
|
133
|
+
if (attributes?.id === undefined)
|
|
137
134
|
continue;
|
|
138
135
|
const entries = [];
|
|
139
136
|
const entryBody = fromMatch[2] ?? '';
|
|
@@ -152,10 +149,12 @@ function parseInboxBody(body) {
|
|
|
152
149
|
...(entryAttributes.ref === undefined ? {} : { ref: entryAttributes.ref }),
|
|
153
150
|
});
|
|
154
151
|
}
|
|
152
|
+
// `updates` and `finished` are derived from the entry list; pre-cut
|
|
153
|
+
// scrollback still carries them as attributes, honored when present.
|
|
154
|
+
const legacyUpdates = attributes.updates === undefined ? Number.NaN : Number(attributes.updates);
|
|
155
155
|
senders.push({
|
|
156
156
|
id: attributes.id,
|
|
157
|
-
|
|
158
|
-
updates,
|
|
157
|
+
updates: Number.isFinite(legacyUpdates) ? legacyUpdates : entries.length,
|
|
159
158
|
finished: attributes.finished === 'true' || entries.some((entry) => entry.kind === 'final'),
|
|
160
159
|
entries,
|
|
161
160
|
});
|
|
@@ -203,15 +202,13 @@ export function parseCard(message) {
|
|
|
203
202
|
/** Format a complete inbox card from report bodies resolved by the caller. */
|
|
204
203
|
export function formatInboxCard(sections) {
|
|
205
204
|
const body = sections.map((section) => {
|
|
206
|
-
const updates = section.updates ?? section.entries.length;
|
|
207
|
-
const finished = section.finished ?? section.entries.some((entry) => entry.kind === 'final');
|
|
208
205
|
const entries = section.entries.map((entry) => {
|
|
209
206
|
const disposition = entry.disposition === 'human-answer' || entry.disposition === 'human-canceled'
|
|
210
207
|
? entry.disposition
|
|
211
208
|
: undefined;
|
|
212
209
|
return `<entry${formatAttributes({ kind: entry.kind, ref: entry.ref, disposition })}>${escapeXmlText(entry.body)}</entry>`;
|
|
213
210
|
}).join('\n');
|
|
214
|
-
return `<from${formatAttributes({ id: section.id
|
|
211
|
+
return `<from${formatAttributes({ id: section.id })}>${entries}</from>`;
|
|
215
212
|
}).join('\n');
|
|
216
213
|
return formatCard('inbox', {}, body);
|
|
217
214
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -397,6 +397,15 @@ export interface ProfileManifest {
|
|
|
397
397
|
* `profileHome`). Normalized on read to the first project when the manifest
|
|
398
398
|
* carries none, so a profile written before homes existed still has one. */
|
|
399
399
|
home: string | null;
|
|
400
|
+
/** ISO timestamp while the profile is inert; null (or absent on older manifests) means active. */
|
|
401
|
+
paused_at: string | null;
|
|
402
|
+
/** Persona kind for creates that omit kind; absent means `general`. */
|
|
403
|
+
default_kind?: string;
|
|
404
|
+
/** Optional string map of profile facts (identity, role, description).
|
|
405
|
+
* Not secrets — readable back from every projection, and surfaced to
|
|
406
|
+
* every broker launched under the profile as `CRTR_PROFILE_META_<KEY>`
|
|
407
|
+
* env (see `core/runtime/spawn-env.ts`). Absent means none. */
|
|
408
|
+
metadata?: Record<string, string>;
|
|
400
409
|
created_at: string;
|
|
401
410
|
last_used_at: string | null;
|
|
402
411
|
}
|