@north-light/crouter 0.3.248 → 0.3.250
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/README.md +1 -1
- package/dist/api/__tests__/integration/client.test.js +28 -7
- package/dist/api/client.d.ts +1 -1
- package/dist/api/client.js +2 -2
- package/dist/api/dto/lifecycle.d.ts +3 -0
- package/dist/api/dto/messages.d.ts +4 -0
- package/dist/api/dto/nodes.d.ts +4 -0
- package/dist/api/dto/reports.d.ts +7 -2
- package/dist/builtin-memory/00-runtime-base/01-escalation.md +1 -1
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +3 -0
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +0 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +4 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +4 -3
- package/dist/clients/attach/session/chrome-refresh.d.ts +3 -0
- package/dist/clients/attach/session/chrome-refresh.js +1 -0
- package/dist/clients/attach/viewer.js +532 -532
- package/dist/clients/inbox/__tests__/integration/stale-row-activation.test.d.ts +1 -0
- package/dist/clients/inbox/__tests__/integration/stale-row-activation.test.js +53 -0
- package/dist/clients/inbox/controller.js +32 -5
- package/dist/clients/inbox/tui/ansi.d.ts +3 -1
- package/dist/clients/inbox/tui/ansi.js +14 -10
- package/dist/commands/__tests__/api-client-cold-start-diagnostic.test.js +7 -7
- package/dist/commands/api-client.d.ts +2 -2
- package/dist/commands/api-client.js +5 -5
- package/dist/commands/canvas-browse.js +1 -1
- package/dist/commands/node/create.js +3 -1
- package/dist/commands/node/inspect.js +1 -0
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node/message.js +6 -4
- package/dist/commands/node/wait.js +1 -1
- package/dist/commands/push.js +36 -37
- package/dist/commands/sys/daemon.js +21 -8
- package/dist/commands/sys/setup-core.js +2 -2
- package/dist/core/__tests__/broker-stream-watchdog-floor.test.d.ts +1 -0
- package/dist/core/__tests__/broker-stream-watchdog-floor.test.js +84 -0
- package/dist/core/__tests__/broker-turn-admission.test.d.ts +1 -0
- package/dist/core/__tests__/broker-turn-admission.test.js +44 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +5 -1
- package/dist/core/__tests__/child-death-wake.test.js +11 -7
- package/dist/core/__tests__/collectible.test.d.ts +1 -0
- package/dist/core/__tests__/collectible.test.js +88 -0
- package/dist/core/__tests__/context-intro.test.js +4 -41
- package/dist/core/__tests__/daemon-boot.test.js +30 -14
- package/dist/core/__tests__/daemon-wedge.test.js +25 -19
- package/dist/core/__tests__/fixtures/fake-engine.js +11 -0
- package/dist/core/__tests__/human-deliver.test.js +5 -5
- package/dist/core/__tests__/integration/deferred-no-wake.test.js +65 -4
- package/dist/core/__tests__/integration/human-deliver-e2e.test.js +3 -3
- package/dist/core/__tests__/integration/revive.test.js +48 -0
- package/dist/core/__tests__/integration/spawn-root.test.js +36 -1
- package/dist/core/__tests__/integration/worktree-land.test.js +82 -3
- package/dist/core/__tests__/integration/worktree-reap.test.js +44 -6
- package/dist/core/__tests__/migration.test.js +30 -0
- package/dist/core/__tests__/passive-subscription.test.js +39 -1
- package/dist/core/__tests__/prune.test.js +12 -3
- package/dist/core/__tests__/relaunch-root.test.js +1 -1
- package/dist/core/__tests__/respawn-throttle.test.js +16 -146
- package/dist/core/__tests__/seam/broker-crash-teardown.test.js +23 -11
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +81 -2
- package/dist/core/__tests__/seam/dormancy-release.test.js +1 -1
- package/dist/core/__tests__/seam/self-close-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/seam/self-close-teardown.test.js +49 -0
- package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +18 -10
- package/dist/core/canvas/canvas.d.ts +9 -15
- package/dist/core/canvas/canvas.js +41 -47
- package/dist/core/canvas/crons.d.ts +5 -8
- package/dist/core/canvas/crons.js +5 -8
- package/dist/core/canvas/history.d.ts +5 -0
- package/dist/core/canvas/history.js +12 -1
- package/dist/core/canvas/migrations.js +58 -1
- package/dist/core/canvas/pid.d.ts +1 -1
- package/dist/core/canvas/pid.js +5 -2
- package/dist/core/canvas/types.d.ts +4 -0
- package/dist/core/feed/feed.d.ts +4 -0
- package/dist/core/feed/feed.js +13 -8
- package/dist/core/feed/inbox.d.ts +2 -1
- package/dist/core/feed/inbox.js +22 -3
- package/dist/core/git.d.ts +3 -0
- package/dist/core/git.js +3 -1
- package/dist/core/human/feedback-companion.js +1 -0
- package/dist/core/preview-registry.js +0 -1
- package/dist/core/review/companion.js +1 -0
- package/dist/core/review/realize.js +1 -0
- package/dist/core/runtime/broker/auth-reload.d.ts +2 -0
- package/dist/core/runtime/broker/auth-reload.js +9 -3
- package/dist/core/runtime/broker/engine-drive.d.ts +2 -0
- package/dist/core/runtime/broker/engine-drive.js +28 -23
- package/dist/core/runtime/broker/event-projection.d.ts +4 -0
- package/dist/core/runtime/broker/event-projection.js +43 -23
- package/dist/core/runtime/broker/fault-retry.d.ts +9 -0
- package/dist/core/runtime/broker/fault-retry.js +94 -8
- package/dist/core/runtime/broker/frame-dispatch.d.ts +2 -0
- package/dist/core/runtime/broker/frame-dispatch.js +4 -1
- package/dist/core/runtime/broker/inbox.js +4 -0
- package/dist/core/runtime/broker/rebind.js +18 -0
- package/dist/core/runtime/broker/turn-admission.d.ts +11 -0
- package/dist/core/runtime/broker/turn-admission.js +31 -0
- package/dist/core/runtime/broker/turn-ignition.d.ts +2 -0
- package/dist/core/runtime/broker/turn-ignition.js +25 -15
- package/dist/core/runtime/broker.js +21 -2
- package/dist/core/runtime/close.d.ts +11 -9
- package/dist/core/runtime/close.js +23 -19
- package/dist/core/runtime/fault.d.ts +21 -0
- package/dist/core/runtime/fault.js +129 -19
- package/dist/core/runtime/fleet.d.ts +0 -6
- package/dist/core/runtime/host.d.ts +7 -1
- package/dist/core/runtime/host.js +20 -2
- package/dist/core/runtime/nodes.d.ts +5 -1
- package/dist/core/runtime/nodes.js +24 -1
- package/dist/core/runtime/placement.d.ts +6 -6
- package/dist/core/runtime/placement.js +15 -8
- package/dist/core/runtime/reset.js +2 -2
- package/dist/core/runtime/revive.js +40 -4
- package/dist/core/runtime/spawn.d.ts +3 -0
- package/dist/core/runtime/spawn.js +1 -0
- package/dist/core/runtime/warm-pool.d.ts +4 -0
- package/dist/core/runtime/warm-pool.js +2 -0
- package/dist/core/worktree.d.ts +4 -3
- package/dist/core/worktree.js +73 -27
- package/dist/daemon/__tests__/integration/api-startup-readiness.test.d.ts +1 -0
- package/dist/daemon/__tests__/integration/api-startup-readiness.test.js +81 -0
- package/dist/daemon/api/__tests__/reopen-delivery.test.js +38 -0
- package/dist/daemon/api/__tests__/seam/api-server.test.js +1 -0
- package/dist/daemon/api/__tests__/seam/leaf-api-parity.test.js +13 -2
- package/dist/daemon/api/handlers/bash-jobs.js +2 -0
- package/dist/daemon/api/handlers/broker-ops.js +23 -2
- package/dist/daemon/api/handlers/human.js +1 -1
- package/dist/daemon/api/handlers/messages.js +11 -2
- package/dist/daemon/api/handlers/nodes.js +13 -1
- package/dist/daemon/api/handlers/reports.js +14 -6
- package/dist/daemon/api/map.js +1 -0
- package/dist/daemon/api/server.d.ts +9 -12
- package/dist/daemon/api/server.js +30 -19
- package/dist/daemon/companion-retire.js +3 -9
- package/dist/daemon/cron/sinks.js +7 -2
- package/dist/daemon/crtrd.js +4 -1
- package/dist/daemon/fleet.d.ts +2 -16
- package/dist/daemon/fleet.js +63 -150
- package/dist/daemon/human/finish.js +2 -0
- package/dist/daemon/manage.d.ts +11 -0
- package/dist/daemon/manage.js +32 -3
- package/dist/daemon/messaging/node-message.js +7 -1
- package/dist/daemon/reconcilers/bash-deadline.js +2 -0
- package/dist/daemon/reconcilers/broker-supervision.d.ts +1 -0
- package/dist/daemon/reconcilers/broker-supervision.js +22 -9
- package/dist/daemon/reconcilers/live-obligation.d.ts +16 -6
- package/dist/daemon/reconcilers/live-obligation.js +18 -6
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.d.ts +15 -4
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.js +34 -3
- package/dist/daemon/reconcilers/node-lifecycle/terminating.d.ts +4 -1
- package/dist/daemon/reconcilers/node-lifecycle/terminating.js +11 -2
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +14 -7
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +2 -2
- package/dist/daemon/reconcilers/storage-maintenance.js +3 -3
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
- package/dist/pi-extensions/canvas-context-intro.d.ts +3 -34
- package/dist/pi-extensions/canvas-context-intro.js +4 -76
- package/dist/pi-extensions/canvas-review-boundary.d.ts +3 -24
- package/dist/pi-extensions/canvas-review-boundary.js +4 -29
- package/dist/pi-extensions/canvas-stophook.js +1 -6
- package/dist/shared/birth-announcement.d.ts +12 -0
- package/dist/shared/birth-announcement.js +25 -0
- package/dist/shared/generated-context.d.ts +8 -4
- package/dist/shared/generated-context.js +15 -9
- package/package.json +4 -4
- package/runtime.lock.json +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +0 -254
- package/dist/clients/inbox/tui/clipboard.d.ts +0 -2
- package/dist/clients/inbox/tui/clipboard.js +0 -26
- package/dist/pi-extensions/truncate.d.ts +0 -5
- package/dist/pi-extensions/truncate.js +0 -14
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasActiveLiveSubscription, hasLiveMessageWait, hasLiveSubscriber, hasPendingCancelOnWakeCron, subscribersOf, } from '../../core/canvas/index.js';
|
|
1
|
+
import { newestReportRef, hasActiveLiveSubscription, hasLiveMessageWait, hasLiveSubscriber, hasPendingCancelOnWakeCron, subscribersOf, } from '../../core/canvas/index.js';
|
|
2
2
|
import { fullName } from '../../core/canvas/labels.js';
|
|
3
3
|
import { countTickets } from '../../core/canvas/attention.js';
|
|
4
4
|
import { hasUnseenWakeMail, readCursor, readInboxSince } from '../../core/feed/inbox.js';
|
|
@@ -33,22 +33,34 @@ export function hasPendingWake(nodeId) {
|
|
|
33
33
|
export function parkEvent(nodeId, lifecycle) {
|
|
34
34
|
if (hasLiveObligation(nodeId))
|
|
35
35
|
return 'release';
|
|
36
|
+
if (lifecycle === 'resident')
|
|
37
|
+
return hasUnseenWakeMail(nodeId) ? 'release' : 'park';
|
|
36
38
|
if (readInboxSince(nodeId, readCursor(nodeId)).length > 0)
|
|
37
39
|
return 'release';
|
|
38
|
-
if (lifecycle === 'resident')
|
|
39
|
-
return 'park';
|
|
40
40
|
return hasLiveSubscriber(nodeId) ? 'release' : 'cancel';
|
|
41
41
|
}
|
|
42
42
|
/** Apply an unattended decision and notify subscribers of automatic completion.
|
|
43
43
|
* This is the DEGRADED enactment: no summary turn ran, so subscribers get the
|
|
44
|
-
* doctrine
|
|
44
|
+
* doctrine notice instead of the node's own report. The live clock's good path
|
|
45
45
|
* (broker settlement after a parking turn) enacts `park` directly and fans
|
|
46
|
-
* nothing — the node's report is the notice.
|
|
46
|
+
* nothing — the node's report is the notice.
|
|
47
|
+
*
|
|
48
|
+
* The notice is DEFERRED: a node settling quietly at the end of its unattended
|
|
49
|
+
* grace is not an event worth starting a manager's cycle for, so it rides the
|
|
50
|
+
* manager's next natural cycle and arrives alongside whatever does.
|
|
51
|
+
*
|
|
52
|
+
* Only `park` fans, and only a resident parks, so the notice speaks about a
|
|
53
|
+
* resident throughout: its conversation ended without a final report, its state
|
|
54
|
+
* lives in whatever report it did leave, and a message reopens it. */
|
|
47
55
|
export function applyParkEvent(nodeId, event) {
|
|
48
56
|
const node = event === 'cancel'
|
|
49
57
|
? transition(nodeId, event, { reason: 'stranded' })
|
|
50
58
|
: transition(nodeId, event);
|
|
51
59
|
if (event !== 'park')
|
|
52
60
|
return;
|
|
53
|
-
|
|
61
|
+
const ref = newestReportRef(nodeId);
|
|
62
|
+
const pointer = ref === null
|
|
63
|
+
? `It left no report behind; \`crtr node inspect show ${nodeId}\` shows where it stands.`
|
|
64
|
+
: `Its latest report is where it left things: \`crtr canvas history read ${ref}\`.`;
|
|
65
|
+
fanDoctrineWake(nodeId, subscribersOf(nodeId), `Resident concluded — ${fullName(node)} (${nodeId}) had nothing left to wake it through its unattended grace, so its conversation was concluded. It submitted no final report. ${pointer} A message to it reopens it on a fresh cycle.`, { reason: 'child-auto-done', child: nodeId }, 'deferred');
|
|
54
66
|
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { type NodeRow } from '../../../core/canvas/index.js';
|
|
2
|
+
export declare const MAX_RESPAWN_ATTEMPTS = 6;
|
|
3
|
+
export declare function respawnBackoffMs(consecutiveFailures: number): number;
|
|
4
|
+
export interface RespawnAttempt {
|
|
5
|
+
failures: number;
|
|
6
|
+
notBefore: string | null;
|
|
7
|
+
exhausted: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Whether a durable backoff still holds this row back from a relaunch. */
|
|
5
10
|
export declare function isInBackoff(row: NodeRow, now: number): boolean;
|
|
11
|
+
/** The only respawn-failure policy: progress since launch clears the sequence;
|
|
12
|
+
* every other missing-engine observation advances it and records its backoff. */
|
|
13
|
+
export declare function decideRespawnAttempt(row: Pick<NodeRow, 'last_settled_at' | 'launched_at' | 'respawn_failures'>, now: number): RespawnAttempt;
|
|
14
|
+
/** Persist the policy decision through the daemon-only canvas writer. */
|
|
15
|
+
export declare function recordRespawnPolicy(row: NodeRow, now: number): RespawnAttempt;
|
|
16
|
+
export declare function clearRespawnPolicy(nodeId: string): void;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { recordRespawnAttempt } from '../../../core/canvas/index.js';
|
|
2
|
+
export const MAX_RESPAWN_ATTEMPTS = 6;
|
|
3
|
+
const RESPAWN_BACKOFF_BASE_MS = 5_000;
|
|
4
|
+
const RESPAWN_BACKOFF_MAX_MS = 10 * 60_000;
|
|
5
|
+
export function respawnBackoffMs(consecutiveFailures) {
|
|
6
|
+
if (consecutiveFailures <= 0)
|
|
7
|
+
return 0;
|
|
8
|
+
return Math.min(RESPAWN_BACKOFF_BASE_MS * 2 ** (consecutiveFailures - 1), RESPAWN_BACKOFF_MAX_MS);
|
|
9
|
+
}
|
|
10
|
+
/** Whether a durable backoff still holds this row back from a relaunch. */
|
|
4
11
|
export function isInBackoff(row, now) {
|
|
5
12
|
const notBefore = row.respawn_not_before;
|
|
6
13
|
if (notBefore == null)
|
|
@@ -8,3 +15,27 @@ export function isInBackoff(row, now) {
|
|
|
8
15
|
const at = Date.parse(notBefore);
|
|
9
16
|
return Number.isFinite(at) && now < at;
|
|
10
17
|
}
|
|
18
|
+
/** The only respawn-failure policy: progress since launch clears the sequence;
|
|
19
|
+
* every other missing-engine observation advances it and records its backoff. */
|
|
20
|
+
export function decideRespawnAttempt(row, now) {
|
|
21
|
+
const settledAt = row.last_settled_at == null ? Number.NaN : Date.parse(row.last_settled_at);
|
|
22
|
+
const launchedAt = row.launched_at == null ? Number.NaN : Date.parse(row.launched_at);
|
|
23
|
+
if (Number.isFinite(settledAt) && Number.isFinite(launchedAt) && settledAt > launchedAt) {
|
|
24
|
+
return { failures: 0, notBefore: null, exhausted: false };
|
|
25
|
+
}
|
|
26
|
+
const failures = (row.respawn_failures ?? 0) + 1;
|
|
27
|
+
return {
|
|
28
|
+
failures,
|
|
29
|
+
notBefore: new Date(now + respawnBackoffMs(failures)).toISOString(),
|
|
30
|
+
exhausted: failures >= MAX_RESPAWN_ATTEMPTS,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Persist the policy decision through the daemon-only canvas writer. */
|
|
34
|
+
export function recordRespawnPolicy(row, now) {
|
|
35
|
+
const attempt = decideRespawnAttempt(row, now);
|
|
36
|
+
recordRespawnAttempt(row.node_id, { failures: attempt.failures, notBefore: attempt.notBefore });
|
|
37
|
+
return attempt;
|
|
38
|
+
}
|
|
39
|
+
export function clearRespawnPolicy(nodeId) {
|
|
40
|
+
recordRespawnAttempt(nodeId, { failures: 0, notBefore: null });
|
|
41
|
+
}
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
import type { NodeRow } from '../../../core/canvas/index.js';
|
|
2
|
-
|
|
2
|
+
/** Request teardown only while a terminal row still has a daemon handle or a
|
|
3
|
+
* provably live recorded process. An indeterminate pid is left for the next
|
|
4
|
+
* tick: uncertainty never authorizes a signal. */
|
|
5
|
+
export declare function handleTerminatingRow(row: NodeRow, hasFleetHandle: boolean): boolean;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { recordedPidLiveness } from '../../../core/canvas/pid.js';
|
|
2
|
+
import { requestBrokerTeardown } from '../../../core/runtime/host.js';
|
|
3
|
+
/** Request teardown only while a terminal row still has a daemon handle or a
|
|
4
|
+
* provably live recorded process. An indeterminate pid is left for the next
|
|
5
|
+
* tick: uncertainty never authorizes a signal. */
|
|
6
|
+
export function handleTerminatingRow(row, hasFleetHandle) {
|
|
7
|
+
if (!hasFleetHandle && (row.pi_pid == null || recordedPidLiveness(row.pi_pid, row.pi_pid_identity) !== 'alive')) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
requestBrokerTeardown(row.node_id);
|
|
11
|
+
return true;
|
|
3
12
|
}
|
|
@@ -59,18 +59,21 @@ export class NodeLifecycleReconciler {
|
|
|
59
59
|
}
|
|
60
60
|
async classifyRow(now, row, ctx, candidates) {
|
|
61
61
|
const id = row.node_id;
|
|
62
|
+
if (isTerminal(row)) {
|
|
63
|
+
if (handleTerminatingRow(row, ctx.fleet.has(id)))
|
|
64
|
+
return;
|
|
65
|
+
if (row.pi_pid != null && recordedPidLiveness(row.pi_pid, row.pi_pid_identity) === 'dead') {
|
|
66
|
+
clearRecordedPid(id);
|
|
67
|
+
}
|
|
68
|
+
if (row.frozen_at != null)
|
|
69
|
+
candidates.push({ row, enact: 'thaw' });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
62
72
|
// 1. A fleet handle IS execution. Whatever froze this row is over.
|
|
63
73
|
if (ctx.fleet.has(id)) {
|
|
64
74
|
this.thawMark(row);
|
|
65
75
|
return;
|
|
66
76
|
}
|
|
67
|
-
if (isTerminal(row)) {
|
|
68
|
-
if (row.pi_pid != null)
|
|
69
|
-
handleTerminatingRow(row);
|
|
70
|
-
else if (row.frozen_at != null)
|
|
71
|
-
candidates.push({ row, enact: 'thaw' });
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
77
|
// A paused profile launches nothing, and its stuck-delivery episode is over
|
|
75
78
|
// rather than merely stalled — the pause is not a delivery failure.
|
|
76
79
|
if (isProfilePaused(row.profile_id)) {
|
|
@@ -101,6 +104,10 @@ export class NodeLifecycleReconciler {
|
|
|
101
104
|
// its way to the launch seam, which owns its own launch and must be left
|
|
102
105
|
// alone rather than raced or written off as a boot failure.
|
|
103
106
|
if (row.pi_pid == null) {
|
|
107
|
+
if (row.respawn_not_before != null) {
|
|
108
|
+
candidates.push({ row, enact: 'policy' });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
104
111
|
if (row.frozen_at != null)
|
|
105
112
|
candidates.push({ row, enact: 'thaw' });
|
|
106
113
|
return;
|
|
@@ -13,8 +13,8 @@ export declare class StorageMaintenanceReconciler {
|
|
|
13
13
|
/** Milliseconds since meta/telemetry was last written. Inbox activity is
|
|
14
14
|
* deliberately excluded: children can keep writing a dead manager's inbox. */
|
|
15
15
|
private msSinceDiskActivity;
|
|
16
|
-
/** Reap only
|
|
17
|
-
*
|
|
16
|
+
/** Reap only physical placement residue of terminal nodes. The durable row,
|
|
17
|
+
* graph, and on-disk state remain available for a later manual revive. */
|
|
18
18
|
private reapDeadResidue;
|
|
19
19
|
/** Delete focus rows whose recorded tmux pane no longer exists. A failed tmux
|
|
20
20
|
* probe is "can't tell" and never authorizes deleting every viewport. */
|
|
@@ -66,12 +66,12 @@ export class StorageMaintenanceReconciler {
|
|
|
66
66
|
}
|
|
67
67
|
return latest === 0 ? Number.POSITIVE_INFINITY : now - latest;
|
|
68
68
|
}
|
|
69
|
-
/** Reap only
|
|
70
|
-
*
|
|
69
|
+
/** Reap only physical placement residue of terminal nodes. The durable row,
|
|
70
|
+
* graph, and on-disk state remain available for a later manual revive. */
|
|
71
71
|
reapDeadResidue(now) {
|
|
72
72
|
let rows;
|
|
73
73
|
try {
|
|
74
|
-
rows = listNodes({ status: ['dead'] });
|
|
74
|
+
rows = listNodes({ status: ['done', 'canceled', 'dead'] });
|
|
75
75
|
}
|
|
76
76
|
catch (err) {
|
|
77
77
|
operationIdContext.fresh(() => {
|
|
@@ -186,7 +186,7 @@ test('gh-451: finished does NOT shut down while a non-deferred inbox entry sits
|
|
|
186
186
|
assert.equal(getNode('mgr')?.intent ?? null, null, 'no idle-release intent while a non-deferred entry is unseen');
|
|
187
187
|
assert.equal(shutdown, false, 'pi stays alive so the in-process watcher can still deliver/commit');
|
|
188
188
|
});
|
|
189
|
-
test('
|
|
189
|
+
test('a deferred send to a TERMINAL node was raised on append, so it holds the settle instead of riding a later cycle', { timeout: 1_000 }, async () => {
|
|
190
190
|
createNode(node('root', { parent: null, lifecycle: 'resident' }));
|
|
191
191
|
createNode(node('mgr', { parent: 'root', lifecycle: 'terminal', mode: 'orchestrator' }));
|
|
192
192
|
createNode(node('worker', { parent: 'mgr', lifecycle: 'terminal', status: 'active' }));
|
|
@@ -199,9 +199,9 @@ test('gh-451: a DEFERRED-only unseen entry does not block idle-release (matches
|
|
|
199
199
|
let shutdown = false;
|
|
200
200
|
const ctx = { shutdown: () => { shutdown = true; } };
|
|
201
201
|
await settle(pi, stopEvent('still waiting on the worker'), ctx);
|
|
202
|
-
assert.equal(getNode('mgr')?.intent,
|
|
203
|
-
assert.equal(getNode('mgr')?.status, '
|
|
204
|
-
assert.equal(shutdown,
|
|
202
|
+
assert.equal(getNode('mgr')?.intent ?? null, null, 'a terminal node takes no reliable later cycle, so its raised entry blocks the release');
|
|
203
|
+
assert.equal(getNode('mgr')?.status, 'active', 'mgr stays active for the entry the watcher still owes it');
|
|
204
|
+
assert.equal(shutdown, false, 'pi stays alive so the in-process watcher can deliver it');
|
|
205
205
|
});
|
|
206
206
|
test('queued continuation finishes before natural-stop routing commits', { timeout: 1_000 }, async () => {
|
|
207
207
|
createNode(node('leaf', { parent: null, lifecycle: 'terminal' }));
|
|
@@ -23,8 +23,9 @@ interface SessionEntryLike {
|
|
|
23
23
|
}
|
|
24
24
|
interface SessionStartCtxLike {
|
|
25
25
|
sessionManager: {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/** Required by pi's `ReadonlySessionManager`: the active branch, which is
|
|
27
|
+
* the context the next turn actually sees. */
|
|
28
|
+
getBranch: (fromId?: string) => SessionEntryLike[];
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
interface CustomMessageLike {
|
|
@@ -37,35 +38,12 @@ interface CustomMessageLike {
|
|
|
37
38
|
nodeId: string;
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
|
-
/** The message handed to a message renderer. `content` is normally the string we
|
|
41
|
-
* sent, but pi types it as string-or-blocks, so we handle both. */
|
|
42
|
-
interface RenderedMessageLike {
|
|
43
|
-
customType: string;
|
|
44
|
-
content: string | Array<{
|
|
45
|
-
type: string;
|
|
46
|
-
text?: string;
|
|
47
|
-
}>;
|
|
48
|
-
}
|
|
49
|
-
/** Minimal structural match for pi-tui's `Component` (render + invalidate). A
|
|
50
|
-
* plain object of this shape is a valid child for pi's Container. */
|
|
51
|
-
interface ComponentLike {
|
|
52
|
-
render: (width: number) => string[];
|
|
53
|
-
invalidate: () => void;
|
|
54
|
-
}
|
|
55
|
-
/** Subset of pi's `Theme` we touch — `fg(color, text)` wraps text in ANSI. Used
|
|
56
|
-
* defensively (falls back to plain text if absent). */
|
|
57
|
-
interface ThemeLike {
|
|
58
|
-
fg?: (color: string, text: string) => string;
|
|
59
|
-
}
|
|
60
41
|
interface PiLike {
|
|
61
42
|
on: (event: 'session_start', handler: (event: unknown, ctx: SessionStartCtxLike) => void | Promise<void>) => void;
|
|
62
43
|
sendMessage: (message: CustomMessageLike, options?: {
|
|
63
44
|
deliverAs?: string;
|
|
64
45
|
triggerTurn?: boolean;
|
|
65
46
|
}) => void;
|
|
66
|
-
registerMessageRenderer: (customType: string, renderer: (message: RenderedMessageLike, options: {
|
|
67
|
-
expanded?: boolean;
|
|
68
|
-
}, theme: ThemeLike) => ComponentLike | undefined) => void;
|
|
69
47
|
}
|
|
70
48
|
/** Build the full bearings baseline for a conversation without inherited
|
|
71
49
|
* crouter context. Exported for testing. */
|
|
@@ -73,15 +51,6 @@ export declare function buildContextIntro(nodeId: string, target?: ExposureTarge
|
|
|
73
51
|
/** Build the compact node-scoped update for a fork whose copied branch already
|
|
74
52
|
* carries a full crouter baseline. Exported for testing. */
|
|
75
53
|
export declare function buildForkContextIntro(nodeId: string): Promise<string>;
|
|
76
|
-
/**
|
|
77
|
-
* Renderer for `crtr-context` messages. Collapsed (default) shows a one-line
|
|
78
|
-
* stub; expanded (Ctrl+O) shows the label + full body. Returns a plain object
|
|
79
|
-
* matching pi's structural `Component` interface — no pi-tui import. Exported for
|
|
80
|
-
* testing.
|
|
81
|
-
*/
|
|
82
|
-
export declare function renderContextMessage(message: RenderedMessageLike, options: {
|
|
83
|
-
expanded?: boolean;
|
|
84
|
-
}, theme: ThemeLike): ComponentLike;
|
|
85
54
|
/** Register new-context baseline assembly; a strict resume changes nothing. */
|
|
86
55
|
export declare function registerCanvasContextIntro(pi: PiLike): void;
|
|
87
56
|
export default registerCanvasContextIntro;
|
|
@@ -11,26 +11,16 @@
|
|
|
11
11
|
// stamp and receives a new baseline. The preference snapshot and persona
|
|
12
12
|
// acknowledgment persist only after Pi materializes that baseline.
|
|
13
13
|
//
|
|
14
|
-
// COLLAPSED BY DEFAULT: a `registerMessageRenderer` keyed to our customType
|
|
15
|
-
// renders the block as a single one-line stub; the full body only appears when
|
|
16
|
-
// the user expands tool output (Ctrl+O / `app.tools.expand`). pi drives this via
|
|
17
|
-
// `CustomMessageComponent.setExpanded(toolOutputExpanded)`, so the same toggle
|
|
18
|
-
// that expands tool results expands the bearings. The renderer returns a plain
|
|
19
|
-
// object satisfying pi's structural `Component` interface ({ render, invalidate })
|
|
20
|
-
// — no pi-tui class needed. The LLM always sees the full `content` regardless of
|
|
21
|
-
// how it renders; the renderer is display-only.
|
|
22
|
-
//
|
|
23
14
|
// Plain TS-with-types — no imports from @earendil-works/* so this compiles inside
|
|
24
15
|
// crouter's own tsc build without a dep on the pi packages.
|
|
25
16
|
import { envNodeId } from '../shared/env.js';
|
|
26
|
-
import { CONTEXT_INTRO_CUSTOM_TYPE, formatCard
|
|
17
|
+
import { CONTEXT_INTRO_CUSTOM_TYPE, formatCard } from '../shared/generated-context.js';
|
|
27
18
|
import { brokerExtensionState } from '../core/runtime/broker/daemon-ops.js';
|
|
28
19
|
import { buildContextBearingsFromState, buildForkBearingsFromState, buildProjectContextBlockForBroker, renderPreferencesFromState, } from '../core/runtime/broker-extension-render.js';
|
|
29
20
|
import { acknowledgePersonaBaseline } from '../core/runtime/broker-persona-guidance.js';
|
|
30
21
|
import { situationalContextEnvelope } from '../core/runtime/situational-context.js';
|
|
31
22
|
import { clearContextExposureState, cloneTranscriptExposureState, emptyContextExposureState, exposureTarget, freezePreferenceSnapshot, hasTranscriptExposure, loadContextExposureState, replaceContextExposureState, saveContextExposureState, sharedContextExposureState, } from '../core/substrate/injected-store.js';
|
|
32
23
|
import { clearSessionCache } from '../core/substrate/session-cache.js';
|
|
33
|
-
import { truncateToWidth } from './truncate.js';
|
|
34
24
|
/** The `customType` stamped on the injected session message. Used both to write
|
|
35
25
|
* the entry and to detect it on resume (the idempotency guard). Defined beside
|
|
36
26
|
* the block builder in `core/runtime/bearings.ts` because the warm-pool claim's
|
|
@@ -49,16 +39,7 @@ export async function buildForkContextIntro(nodeId) {
|
|
|
49
39
|
const state = await brokerExtensionState(nodeId);
|
|
50
40
|
return buildForkBearingsFromState(state, situationalContextEnvelope(nodeId) ?? '');
|
|
51
41
|
}
|
|
52
|
-
//
|
|
53
|
-
/** Pull the plain text out of a custom message's content (string or blocks). */
|
|
54
|
-
function messageText(message) {
|
|
55
|
-
if (typeof message.content === 'string')
|
|
56
|
-
return message.content;
|
|
57
|
-
return message.content
|
|
58
|
-
.filter((c) => c.type === 'text' && typeof c.text === 'string')
|
|
59
|
-
.map((c) => c.text)
|
|
60
|
-
.join('\n');
|
|
61
|
-
}
|
|
42
|
+
// Message extraction
|
|
62
43
|
/** Plain text of a session entry's content (string or text blocks), '' when
|
|
63
44
|
* absent. Used by the idempotency guard to tell OUR own injected bearings
|
|
64
45
|
* (which name our node id) from a SOURCE's bearings copied in by `--fork-from`. */
|
|
@@ -74,62 +55,9 @@ function entryText(e) {
|
|
|
74
55
|
}
|
|
75
56
|
return '';
|
|
76
57
|
}
|
|
77
|
-
function activeBranch(ctx) {
|
|
78
|
-
return typeof ctx.sessionManager.getBranch === 'function'
|
|
79
|
-
? ctx.sessionManager.getBranch()
|
|
80
|
-
: ctx.sessionManager.getEntries();
|
|
81
|
-
}
|
|
82
|
-
/** Hard-wrap a single logical line to `width` columns (content carries no ANSI).
|
|
83
|
-
* Code-point aware so wide-string slicing never splits a surrogate pair; the
|
|
84
|
-
* bearings prose is plain text, so code-point count == visible columns. */
|
|
85
|
-
function wrapLine(line, width) {
|
|
86
|
-
if (width <= 0)
|
|
87
|
-
return [''];
|
|
88
|
-
const chars = Array.from(line);
|
|
89
|
-
if (chars.length <= width)
|
|
90
|
-
return [line];
|
|
91
|
-
const out = [];
|
|
92
|
-
for (let i = 0; i < chars.length; i += width)
|
|
93
|
-
out.push(chars.slice(i, i + width).join(''));
|
|
94
|
-
return out;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Renderer for `crtr-context` messages. Collapsed (default) shows a one-line
|
|
98
|
-
* stub; expanded (Ctrl+O) shows the label + full body. Returns a plain object
|
|
99
|
-
* matching pi's structural `Component` interface — no pi-tui import. Exported for
|
|
100
|
-
* testing.
|
|
101
|
-
*/
|
|
102
|
-
export function renderContextMessage(message, options, theme) {
|
|
103
|
-
const expanded = options?.expanded === true;
|
|
104
|
-
const paint = (color, text) => typeof theme?.fg === 'function' ? theme.fg(color, text) : text;
|
|
105
|
-
return {
|
|
106
|
-
render(width) {
|
|
107
|
-
const w = typeof width === 'number' && width > 0 ? width : 80;
|
|
108
|
-
if (!expanded) {
|
|
109
|
-
// Truncate BEFORE painting so the ANSI wrapper never inflates the
|
|
110
|
-
// measured width; an over-wide line aborts pi's entire TUI render.
|
|
111
|
-
const stub = `[${CONTEXT_INTRO_CUSTOM_TYPE}] orienting bearings — ctrl+o to expand`;
|
|
112
|
-
return [paint('dim', truncateToWidth(stub, w))];
|
|
113
|
-
}
|
|
114
|
-
const lines = [paint('customMessageLabel', truncateToWidth(`[${CONTEXT_INTRO_CUSTOM_TYPE}]`, w)), ''];
|
|
115
|
-
const body = parseCard({ role: 'custom', customType: message.customType, content: messageText(message) })?.body ?? messageText(message);
|
|
116
|
-
for (const raw of body.split('\n')) {
|
|
117
|
-
for (const wrapped of wrapLine(raw, w))
|
|
118
|
-
lines.push(paint('customMessageText', wrapped));
|
|
119
|
-
}
|
|
120
|
-
return lines;
|
|
121
|
-
},
|
|
122
|
-
invalidate() {
|
|
123
|
-
/* stateless — nothing to clear */
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
58
|
// Extension
|
|
128
59
|
/** Register new-context baseline assembly; a strict resume changes nothing. */
|
|
129
60
|
export function registerCanvasContextIntro(pi) {
|
|
130
|
-
// Collapse the block to a one-liner until the user expands tool output (Ctrl+O).
|
|
131
|
-
// Harmless to register outside TUI mode (it's only consulted while rendering).
|
|
132
|
-
pi.registerMessageRenderer(CONTEXT_INTRO_CUSTOM_TYPE, renderContextMessage);
|
|
133
61
|
pi.on('session_start', async (event, ctx) => {
|
|
134
62
|
const nodeId = envNodeId();
|
|
135
63
|
if (nodeId === undefined || nodeId.trim() === '')
|
|
@@ -150,7 +78,7 @@ export function registerCanvasContextIntro(pi) {
|
|
|
150
78
|
}
|
|
151
79
|
// The active branch, rather than the whole session file, is the context the
|
|
152
80
|
// next turn sees. A new cycle therefore receives its own current baseline.
|
|
153
|
-
const contextEntries =
|
|
81
|
+
const contextEntries = ctx.sessionManager.getBranch().filter((e) => e.type === 'custom_message' && e.customType === CONTEXT_INTRO_CUSTOM_TYPE);
|
|
154
82
|
const ours = contextEntries.some((e) => e.details?.nodeId === nodeId || entryText(e).includes(nodeId));
|
|
155
83
|
if (ours)
|
|
156
84
|
return;
|
|
@@ -170,7 +98,7 @@ export function registerCanvasContextIntro(pi) {
|
|
|
170
98
|
display: true,
|
|
171
99
|
details: { nodeId },
|
|
172
100
|
});
|
|
173
|
-
const delivered =
|
|
101
|
+
const delivered = ctx.sessionManager.getBranch().some((entry) => entry.type === 'custom_message'
|
|
174
102
|
&& entry.customType === CONTEXT_INTRO_CUSTOM_TYPE
|
|
175
103
|
&& entry.details?.nodeId === nodeId
|
|
176
104
|
&& entryText(entry) === messageContent);
|
|
@@ -7,8 +7,9 @@ interface SessionEntryLike {
|
|
|
7
7
|
}
|
|
8
8
|
interface SessionStartCtxLike {
|
|
9
9
|
sessionManager: {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/** Required by pi's `ReadonlySessionManager`: the active branch, which is
|
|
11
|
+
* the context the next turn actually sees. */
|
|
12
|
+
getBranch: (fromId?: string) => SessionEntryLike[];
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
15
|
interface CustomMessageLike {
|
|
@@ -25,35 +26,13 @@ interface CustomMessageLike {
|
|
|
25
26
|
kind: 'review' | 'page_feedback';
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
|
-
interface RenderedMessageLike {
|
|
29
|
-
customType: string;
|
|
30
|
-
content: string | Array<{
|
|
31
|
-
type: string;
|
|
32
|
-
text?: string;
|
|
33
|
-
}>;
|
|
34
|
-
}
|
|
35
|
-
interface ComponentLike {
|
|
36
|
-
render: (width: number) => string[];
|
|
37
|
-
invalidate: () => void;
|
|
38
|
-
}
|
|
39
|
-
interface ThemeLike {
|
|
40
|
-
fg?: (color: string, text: string) => string;
|
|
41
|
-
}
|
|
42
29
|
interface PiLike {
|
|
43
30
|
on: (event: 'session_start', handler: (event: unknown, ctx: SessionStartCtxLike) => void | Promise<void>) => void;
|
|
44
31
|
sendMessage: (message: CustomMessageLike, options?: {
|
|
45
32
|
deliverAs?: string;
|
|
46
33
|
triggerTurn?: boolean;
|
|
47
34
|
}) => void;
|
|
48
|
-
registerMessageRenderer: (customType: string, renderer: (message: RenderedMessageLike, options: {
|
|
49
|
-
expanded?: boolean;
|
|
50
|
-
}, theme: ThemeLike) => ComponentLike | undefined) => void;
|
|
51
35
|
}
|
|
52
|
-
/** Render the marker as a single compact divider; its full content remains in
|
|
53
|
-
* the message list for the model and generated-context presenters. */
|
|
54
|
-
export declare function renderReviewBoundary(message: RenderedMessageLike, options: {
|
|
55
|
-
expanded?: boolean;
|
|
56
|
-
}, theme: ThemeLike): ComponentLike;
|
|
57
36
|
/**
|
|
58
37
|
* Register the review boundary marker. Companion nodes receive one boundary
|
|
59
38
|
* marker on session_start that separates inherited context from the visible
|
|
@@ -7,30 +7,7 @@
|
|
|
7
7
|
import { envNodeId } from '../shared/env.js';
|
|
8
8
|
import { brokerExtensionState } from '../core/runtime/broker/daemon-ops.js';
|
|
9
9
|
import { isReviewBoundary, REVIEW_BOUNDARY_CUSTOM_TYPE } from '../core/runtime/session-visibility.js';
|
|
10
|
-
import { formatCard
|
|
11
|
-
import { truncateToWidth } from './truncate.js';
|
|
12
|
-
// Collapsed-by-default rendering
|
|
13
|
-
/** Render the marker as a single compact divider; its full content remains in
|
|
14
|
-
* the message list for the model and generated-context presenters. */
|
|
15
|
-
export function renderReviewBoundary(message, options, theme) {
|
|
16
|
-
const paint = (color, text) => typeof theme?.fg === 'function' ? theme.fg(color, text) : text;
|
|
17
|
-
const text = typeof message.content === 'string'
|
|
18
|
-
? message.content
|
|
19
|
-
: message.content.filter((block) => block.type === 'text' && typeof block.text === 'string').map((block) => block.text).join('\n');
|
|
20
|
-
const body = parseCard({ role: 'custom', customType: message.customType, content: text })?.body ?? text;
|
|
21
|
-
return {
|
|
22
|
-
render(width) {
|
|
23
|
-
const w = typeof width === 'number' && width > 0 ? width : 80;
|
|
24
|
-
const divider = paint('dim', truncateToWidth('[review boundary] earlier conversation is not shown', w));
|
|
25
|
-
if (options?.expanded !== true)
|
|
26
|
-
return [divider];
|
|
27
|
-
return [divider, '', ...body.split('\n').map((line) => paint('customMessageText', truncateToWidth(line, w)))];
|
|
28
|
-
},
|
|
29
|
-
invalidate() {
|
|
30
|
-
/* stateless — nothing to clear */
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
}
|
|
10
|
+
import { formatCard } from '../shared/generated-context.js';
|
|
34
11
|
// Extension
|
|
35
12
|
/**
|
|
36
13
|
* Register the review boundary marker. Companion nodes receive one boundary
|
|
@@ -38,7 +15,6 @@ export function renderReviewBoundary(message, options, theme) {
|
|
|
38
15
|
* transcript. Ordinary nodes get nothing.
|
|
39
16
|
*/
|
|
40
17
|
export function registerCanvasReviewBoundary(pi) {
|
|
41
|
-
pi.registerMessageRenderer(REVIEW_BOUNDARY_CUSTOM_TYPE, renderReviewBoundary);
|
|
42
18
|
pi.on('session_start', async (_event, ctx) => {
|
|
43
19
|
const nodeId = envNodeId();
|
|
44
20
|
if (!nodeId || nodeId.trim() === '')
|
|
@@ -54,10 +30,9 @@ export function registerCanvasReviewBoundary(pi) {
|
|
|
54
30
|
return;
|
|
55
31
|
// The active branch, not the whole session file, is the context the next
|
|
56
32
|
// turn sees. Check for an existing boundary marker on this branch.
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const alreadyMarked = scan.some((entry) => isReviewBoundary(entry, node.review_binding?.review_id));
|
|
33
|
+
const alreadyMarked = ctx.sessionManager
|
|
34
|
+
.getBranch()
|
|
35
|
+
.some((entry) => isReviewBoundary(entry, node.review_binding?.review_id));
|
|
61
36
|
if (alreadyMarked)
|
|
62
37
|
return;
|
|
63
38
|
const binding = node.review_binding;
|
|
@@ -94,12 +94,7 @@ function lastAssistantMessage(messages) {
|
|
|
94
94
|
* Pi-owned fact; crtrd compares it with the node's daemon-owned review binding. */
|
|
95
95
|
function currentReviewBoundaryIds(ctx) {
|
|
96
96
|
try {
|
|
97
|
-
const
|
|
98
|
-
const entries = typeof manager?.getBranch === 'function'
|
|
99
|
-
? manager.getBranch()
|
|
100
|
-
: typeof manager?.getEntries === 'function'
|
|
101
|
-
? manager.getEntries()
|
|
102
|
-
: [];
|
|
97
|
+
const entries = ctx?.sessionManager?.getBranch();
|
|
103
98
|
if (!Array.isArray(entries))
|
|
104
99
|
return [];
|
|
105
100
|
const ids = new Set();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The entry marker keeps delivery-time rendering exclusive to birth notices. */
|
|
2
|
+
export declare const BIRTH_ANNOUNCEMENT_MARKER = "crtr.birth-announcement.v1";
|
|
3
|
+
export declare const BIRTH_ANNOUNCEMENT_AGE_TOKEN = "[[CRTR_BIRTH_ANNOUNCEMENT_AGE]]";
|
|
4
|
+
export declare function birthAnnouncementBody(child: {
|
|
5
|
+
name: string;
|
|
6
|
+
nodeId: string;
|
|
7
|
+
kind: string;
|
|
8
|
+
creator: string;
|
|
9
|
+
}): string;
|
|
10
|
+
export declare function birthAnnouncementAge(createdAt: string, now?: number): string;
|
|
11
|
+
/** Return a birth notice's body rendered at delivery, or null for every other entry. */
|
|
12
|
+
export declare function renderBirthAnnouncementBody(data: Record<string, unknown> | undefined, createdAt: string, now?: number): string | null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** The entry marker keeps delivery-time rendering exclusive to birth notices. */
|
|
2
|
+
export const BIRTH_ANNOUNCEMENT_MARKER = 'crtr.birth-announcement.v1';
|
|
3
|
+
export const BIRTH_ANNOUNCEMENT_AGE_TOKEN = '[[CRTR_BIRTH_ANNOUNCEMENT_AGE]]';
|
|
4
|
+
export function birthAnnouncementBody(child) {
|
|
5
|
+
return `${child.name} (${child.nodeId}, ${child.kind}) was created under you by ${child.creator} ${BIRTH_ANNOUNCEMENT_AGE_TOKEN} ago. For your awareness only — no response or action is needed, and no need to mention it unless asked. Treat its future reports as background you already know about; investigate only if something genuinely needs you.`;
|
|
6
|
+
}
|
|
7
|
+
export function birthAnnouncementAge(createdAt, now = Date.now()) {
|
|
8
|
+
const elapsed = Math.max(0, now - Date.parse(createdAt));
|
|
9
|
+
if (elapsed < 60_000)
|
|
10
|
+
return 'moments';
|
|
11
|
+
const minutes = Math.floor(elapsed / 60_000);
|
|
12
|
+
if (minutes < 60)
|
|
13
|
+
return `${minutes} minute${minutes === 1 ? '' : 's'}`;
|
|
14
|
+
const hours = Math.floor(elapsed / 3_600_000);
|
|
15
|
+
if (hours < 48)
|
|
16
|
+
return `${hours} hour${hours === 1 ? '' : 's'}`;
|
|
17
|
+
const days = Math.floor(elapsed / 86_400_000);
|
|
18
|
+
return `${days} day${days === 1 ? '' : 's'}`;
|
|
19
|
+
}
|
|
20
|
+
/** Return a birth notice's body rendered at delivery, or null for every other entry. */
|
|
21
|
+
export function renderBirthAnnouncementBody(data, createdAt, now = Date.now()) {
|
|
22
|
+
if (data?.['birth_announcement'] !== BIRTH_ANNOUNCEMENT_MARKER || typeof data['body'] !== 'string')
|
|
23
|
+
return null;
|
|
24
|
+
return data['body'].replace(BIRTH_ANNOUNCEMENT_AGE_TOKEN, birthAnnouncementAge(createdAt, now));
|
|
25
|
+
}
|
|
@@ -10,10 +10,14 @@ export declare const REVIEW_BOUNDARY_CUSTOM_TYPE = "crtr-review-boundary";
|
|
|
10
10
|
export declare const STALL_REPROMPT: string;
|
|
11
11
|
/** The daemon's parking mandate: the last turn of a conversation the unattended
|
|
12
12
|
* clock is concluding. Delivered live, wrapped in a `park` runtime card. It
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* governs both the node's durable inheritance and reader-facing output:
|
|
14
|
+
* one update for subscribers and history, then a goodbye that does not narrate
|
|
15
|
+
* the parking.
|
|
16
|
+
*
|
|
17
|
+
* The durable inheritance shares a CONTRACT with `node yield`'s pre-invocation
|
|
18
|
+
* guide (roadmap current, short and shrinking; context dir for in-progress
|
|
19
|
+
* material; memory only for gated permanent lessons) but not a string: yield is
|
|
20
|
+
* read by an agent choosing to refresh, this by an agent being told to conclude. */
|
|
17
21
|
export declare const PARK_SUMMARY_PROMPT: string;
|
|
18
22
|
/** Static recovery prompts shared by the broker producer and display classifier. */
|
|
19
23
|
export declare const AUTH_FAULT_RECOVERY_BODY = "Provider credentials were just updated (a new login landed). Your previous turn stopped on a provider authentication failure. Continue from where you left off and retry the work that failed.";
|