@caupulican/pi-adaptative 0.86.2 → 0.86.5
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/CHANGELOG.md +40 -0
- package/dist/core/agent-paths.d.ts +13 -0
- package/dist/core/agent-paths.d.ts.map +1 -1
- package/dist/core/agent-paths.js +23 -0
- package/dist/core/agent-paths.js.map +1 -1
- package/dist/core/autonomy/bounded-completion.d.ts +7 -0
- package/dist/core/autonomy/bounded-completion.d.ts.map +1 -1
- package/dist/core/autonomy/bounded-completion.js +12 -4
- package/dist/core/autonomy/bounded-completion.js.map +1 -1
- package/dist/core/autonomy/lane-tool-surface.d.ts +2 -0
- package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -1
- package/dist/core/autonomy/lane-tool-surface.js +6 -3
- package/dist/core/autonomy/lane-tool-surface.js.map +1 -1
- package/dist/core/autonomy/subagent-prompt.js +1 -1
- package/dist/core/autonomy/subagent-prompt.js.map +1 -1
- package/dist/core/background-lane-controller.d.ts +25 -2
- package/dist/core/background-lane-controller.d.ts.map +1 -1
- package/dist/core/background-lane-controller.js +67 -0
- package/dist/core/background-lane-controller.js.map +1 -1
- package/dist/core/delegation/replay-safe-mailbox-bounds.d.ts +4 -0
- package/dist/core/delegation/replay-safe-mailbox-bounds.d.ts.map +1 -0
- package/dist/core/delegation/replay-safe-mailbox-bounds.js +20 -0
- package/dist/core/delegation/replay-safe-mailbox-bounds.js.map +1 -0
- package/dist/core/delegation/sanitized-context-fork.d.ts +25 -0
- package/dist/core/delegation/sanitized-context-fork.d.ts.map +1 -0
- package/dist/core/delegation/sanitized-context-fork.js +299 -0
- package/dist/core/delegation/sanitized-context-fork.js.map +1 -0
- package/dist/core/delegation/session-root-mailbox.d.ts +75 -0
- package/dist/core/delegation/session-root-mailbox.d.ts.map +1 -0
- package/dist/core/delegation/session-root-mailbox.js +627 -0
- package/dist/core/delegation/session-root-mailbox.js.map +1 -0
- package/dist/core/delegation/worker-agent-control-coordinator.d.ts +102 -9
- package/dist/core/delegation/worker-agent-control-coordinator.d.ts.map +1 -1
- package/dist/core/delegation/worker-agent-control-coordinator.js +1307 -125
- package/dist/core/delegation/worker-agent-control-coordinator.js.map +1 -1
- package/dist/core/delegation/worker-agent-control.d.ts +183 -8
- package/dist/core/delegation/worker-agent-control.d.ts.map +1 -1
- package/dist/core/delegation/worker-agent-control.js +778 -48
- package/dist/core/delegation/worker-agent-control.js.map +1 -1
- package/dist/core/delegation/worker-attempt-executor.d.ts +9 -0
- package/dist/core/delegation/worker-attempt-executor.d.ts.map +1 -1
- package/dist/core/delegation/worker-attempt-executor.js +479 -214
- package/dist/core/delegation/worker-attempt-executor.js.map +1 -1
- package/dist/core/delegation/worker-authority-resolver.d.ts +9 -0
- package/dist/core/delegation/worker-authority-resolver.d.ts.map +1 -1
- package/dist/core/delegation/worker-authority-resolver.js +22 -14
- package/dist/core/delegation/worker-authority-resolver.js.map +1 -1
- package/dist/core/delegation/worker-context-fork-store.d.ts +71 -0
- package/dist/core/delegation/worker-context-fork-store.d.ts.map +1 -0
- package/dist/core/delegation/worker-context-fork-store.js +437 -0
- package/dist/core/delegation/worker-context-fork-store.js.map +1 -0
- package/dist/core/delegation/worker-context-inheritance-policy.d.ts +18 -0
- package/dist/core/delegation/worker-context-inheritance-policy.d.ts.map +1 -0
- package/dist/core/delegation/worker-context-inheritance-policy.js +28 -0
- package/dist/core/delegation/worker-context-inheritance-policy.js.map +1 -0
- package/dist/core/delegation/worker-conversation-revision.d.ts +31 -0
- package/dist/core/delegation/worker-conversation-revision.d.ts.map +1 -0
- package/dist/core/delegation/worker-conversation-revision.js +203 -0
- package/dist/core/delegation/worker-conversation-revision.js.map +1 -0
- package/dist/core/delegation/worker-conversation-store.d.ts +130 -18
- package/dist/core/delegation/worker-conversation-store.d.ts.map +1 -1
- package/dist/core/delegation/worker-conversation-store.js +1263 -79
- package/dist/core/delegation/worker-conversation-store.js.map +1 -1
- package/dist/core/delegation/worker-delegation-controller.d.ts +29 -2
- package/dist/core/delegation/worker-delegation-controller.d.ts.map +1 -1
- package/dist/core/delegation/worker-delegation-controller.js +625 -111
- package/dist/core/delegation/worker-delegation-controller.js.map +1 -1
- package/dist/core/delegation/worker-delegation-request.d.ts +2 -0
- package/dist/core/delegation/worker-delegation-request.d.ts.map +1 -1
- package/dist/core/delegation/worker-delegation-request.js.map +1 -1
- package/dist/core/delegation/worker-dispatch-scheduler.d.ts +20 -1
- package/dist/core/delegation/worker-dispatch-scheduler.d.ts.map +1 -1
- package/dist/core/delegation/worker-dispatch-scheduler.js +210 -37
- package/dist/core/delegation/worker-dispatch-scheduler.js.map +1 -1
- package/dist/core/delegation/worker-fleet-limits.d.ts +38 -0
- package/dist/core/delegation/worker-fleet-limits.d.ts.map +1 -0
- package/dist/core/delegation/worker-fleet-limits.js +113 -0
- package/dist/core/delegation/worker-fleet-limits.js.map +1 -0
- package/dist/core/delegation/worker-lane-projection.d.ts +1 -0
- package/dist/core/delegation/worker-lane-projection.d.ts.map +1 -1
- package/dist/core/delegation/worker-lane-projection.js +6 -2
- package/dist/core/delegation/worker-lane-projection.js.map +1 -1
- package/dist/core/delegation/worker-lifecycle.d.ts +20 -3
- package/dist/core/delegation/worker-lifecycle.d.ts.map +1 -1
- package/dist/core/delegation/worker-lifecycle.js +42 -18
- package/dist/core/delegation/worker-lifecycle.js.map +1 -1
- package/dist/core/delegation/worker-notification-coordinator.d.ts +4 -0
- package/dist/core/delegation/worker-notification-coordinator.d.ts.map +1 -1
- package/dist/core/delegation/worker-notification-coordinator.js +45 -4
- package/dist/core/delegation/worker-notification-coordinator.js.map +1 -1
- package/dist/core/delegation/worker-provider-turn-protocol.d.ts +50 -0
- package/dist/core/delegation/worker-provider-turn-protocol.d.ts.map +1 -0
- package/dist/core/delegation/worker-provider-turn-protocol.js +185 -0
- package/dist/core/delegation/worker-provider-turn-protocol.js.map +1 -0
- package/dist/core/delegation/worker-recovery-coordinator.d.ts +45 -4
- package/dist/core/delegation/worker-recovery-coordinator.d.ts.map +1 -1
- package/dist/core/delegation/worker-recovery-coordinator.js +223 -17
- package/dist/core/delegation/worker-recovery-coordinator.js.map +1 -1
- package/dist/core/delegation/worker-retry-policy.d.ts +36 -0
- package/dist/core/delegation/worker-retry-policy.d.ts.map +1 -0
- package/dist/core/delegation/worker-retry-policy.js +41 -0
- package/dist/core/delegation/worker-retry-policy.js.map +1 -0
- package/dist/core/delegation/worker-runner.d.ts +3 -1
- package/dist/core/delegation/worker-runner.d.ts.map +1 -1
- package/dist/core/delegation/worker-runner.js +4 -2
- package/dist/core/delegation/worker-runner.js.map +1 -1
- package/dist/core/delegation/worker-task-view.d.ts +43 -0
- package/dist/core/delegation/worker-task-view.d.ts.map +1 -0
- package/dist/core/delegation/worker-task-view.js +194 -0
- package/dist/core/delegation/worker-task-view.js.map +1 -0
- package/dist/core/delegation/worker-terminal-handoff-coordinator.d.ts +55 -0
- package/dist/core/delegation/worker-terminal-handoff-coordinator.d.ts.map +1 -0
- package/dist/core/delegation/worker-terminal-handoff-coordinator.js +179 -0
- package/dist/core/delegation/worker-terminal-handoff-coordinator.js.map +1 -0
- package/dist/core/delegation/worker-tree-budget-coordinator.d.ts +2 -2
- package/dist/core/delegation/worker-tree-budget-coordinator.d.ts.map +1 -1
- package/dist/core/delegation/worker-tree-budget-coordinator.js +12 -46
- package/dist/core/delegation/worker-tree-budget-coordinator.js.map +1 -1
- package/dist/core/keybindings.d.ts +10 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +2 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/orchestration/attempt-ordering.d.ts +9 -0
- package/dist/core/orchestration/attempt-ordering.d.ts.map +1 -0
- package/dist/core/orchestration/attempt-ordering.js +29 -0
- package/dist/core/orchestration/attempt-ordering.js.map +1 -0
- package/dist/core/orchestration/capability-gateway.d.ts +19 -0
- package/dist/core/orchestration/capability-gateway.d.ts.map +1 -1
- package/dist/core/orchestration/capability-gateway.js +19 -2
- package/dist/core/orchestration/capability-gateway.js.map +1 -1
- package/dist/core/orchestration/contracts.d.ts +43 -3
- package/dist/core/orchestration/contracts.d.ts.map +1 -1
- package/dist/core/orchestration/contracts.js +24 -0
- package/dist/core/orchestration/contracts.js.map +1 -1
- package/dist/core/orchestration/delegation-ledger.d.ts +8 -3
- package/dist/core/orchestration/delegation-ledger.d.ts.map +1 -1
- package/dist/core/orchestration/delegation-ledger.js +148 -55
- package/dist/core/orchestration/delegation-ledger.js.map +1 -1
- package/dist/core/orchestration/event-store.d.ts +6 -3
- package/dist/core/orchestration/event-store.d.ts.map +1 -1
- package/dist/core/orchestration/event-store.js +228 -30
- package/dist/core/orchestration/event-store.js.map +1 -1
- package/dist/core/orchestration/task-runtime-codecs.d.ts +31 -0
- package/dist/core/orchestration/task-runtime-codecs.d.ts.map +1 -0
- package/dist/core/orchestration/task-runtime-codecs.js +1471 -0
- package/dist/core/orchestration/task-runtime-codecs.js.map +1 -0
- package/dist/core/orchestration/task-runtime-projection.d.ts +30 -0
- package/dist/core/orchestration/task-runtime-projection.d.ts.map +1 -0
- package/dist/core/orchestration/task-runtime-projection.js +340 -0
- package/dist/core/orchestration/task-runtime-projection.js.map +1 -0
- package/dist/core/orchestration/task-runtime-reducer.d.ts +38 -0
- package/dist/core/orchestration/task-runtime-reducer.d.ts.map +1 -0
- package/dist/core/orchestration/task-runtime-reducer.js +1270 -0
- package/dist/core/orchestration/task-runtime-reducer.js.map +1 -0
- package/dist/core/orchestration/task-runtime-state.d.ts +142 -0
- package/dist/core/orchestration/task-runtime-state.d.ts.map +1 -0
- package/dist/core/orchestration/task-runtime-state.js +14 -0
- package/dist/core/orchestration/task-runtime-state.js.map +1 -0
- package/dist/core/orchestration/task-runtime.d.ts +44 -98
- package/dist/core/orchestration/task-runtime.d.ts.map +1 -1
- package/dist/core/orchestration/task-runtime.js +357 -1113
- package/dist/core/orchestration/task-runtime.js.map +1 -1
- package/dist/core/orchestration/worker-context-fork-reference.d.ts +20 -0
- package/dist/core/orchestration/worker-context-fork-reference.d.ts.map +1 -0
- package/dist/core/orchestration/worker-context-fork-reference.js +53 -0
- package/dist/core/orchestration/worker-context-fork-reference.js.map +1 -0
- package/dist/core/research/research-runner.d.ts +2 -0
- package/dist/core/research/research-runner.d.ts.map +1 -1
- package/dist/core/research/research-runner.js +1 -0
- package/dist/core/research/research-runner.js.map +1 -1
- package/dist/core/runtime-builder.d.ts.map +1 -1
- package/dist/core/runtime-builder.js +8 -0
- package/dist/core/runtime-builder.js.map +1 -1
- package/dist/core/secrets/credential-project.d.ts.map +1 -1
- package/dist/core/secrets/credential-project.js +24 -2
- package/dist/core/secrets/credential-project.js.map +1 -1
- package/dist/core/security/secret-text.d.ts +3 -0
- package/dist/core/security/secret-text.d.ts.map +1 -1
- package/dist/core/security/secret-text.js +15 -0
- package/dist/core/security/secret-text.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +160 -8
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/delegate.d.ts +31 -4
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +751 -98
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/activity-lane.d.ts +5 -0
- package/dist/modes/interactive/components/activity-lane.d.ts.map +1 -1
- package/dist/modes/interactive/components/activity-lane.js +133 -49
- package/dist/modes/interactive/components/activity-lane.js.map +1 -1
- package/dist/modes/interactive/components/agents-overlay.d.ts +40 -0
- package/dist/modes/interactive/components/agents-overlay.d.ts.map +1 -0
- package/dist/modes/interactive/components/agents-overlay.js +153 -0
- package/dist/modes/interactive/components/agents-overlay.js.map +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +18 -13
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-event-controller.d.ts +1 -1
- package/dist/modes/interactive/interactive-event-controller.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-event-controller.js +6 -3
- package/dist/modes/interactive/interactive-event-controller.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +5 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +51 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/key-handlers.d.ts +1 -0
- package/dist/modes/interactive/key-handlers.d.ts.map +1 -1
- package/dist/modes/interactive/key-handlers.js +1 -0
- package/dist/modes/interactive/key-handlers.js.map +1 -1
- package/docs/harness-architecture.md +61 -17
- package/docs/settings.md +17 -9
- package/docs/worker-profiles.md +6 -2
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { sessionRootMailboxFile } from "../agent-paths.js";
|
|
4
|
+
import { withFileLockSync, writeFileAtomicSync } from "../util/atomic-file.js";
|
|
5
|
+
import { readBoundedTextFileSync } from "../util/bounded-file.js";
|
|
6
|
+
import { createReplaySafeMailboxBounder } from "./replay-safe-mailbox-bounds.js";
|
|
7
|
+
const MAX_MANDATORY_REPLIES = 64;
|
|
8
|
+
const MAX_RETAINED_REPLIES = 128;
|
|
9
|
+
const MAX_REPLAY_RECEIPTS = MAX_RETAINED_REPLIES * 4;
|
|
10
|
+
const MAX_REPLY_CONTENT_CHARS = 4_096;
|
|
11
|
+
const MAX_REPLY_ENCODED_BYTES = 15 * 1024;
|
|
12
|
+
const MAX_IDENTITY_CHARS = 512;
|
|
13
|
+
const MAX_MESSAGE_ID_CHARS = 512;
|
|
14
|
+
const MAX_ACK_TOKEN_CHARS = 128;
|
|
15
|
+
const MAX_TIMESTAMP_CHARS = 128;
|
|
16
|
+
const MAX_DEFAULT_REPLAY_RECEIPTS = MAX_REPLAY_RECEIPTS - MAX_MANDATORY_REPLIES;
|
|
17
|
+
const MAX_DEFAULT_MAILBOX_BYTES = 128 * 1024;
|
|
18
|
+
const MAX_TRANSITION_TIMESTAMP = "9999-12-31T23:59:59.999Z";
|
|
19
|
+
const MAX_ENCODED_SOURCE_OWNED_RECEIPT_BYTES = Buffer.byteLength(JSON.stringify({
|
|
20
|
+
messageId: `session-root-reply-${"f".repeat(64)}`,
|
|
21
|
+
sourceAgentId: `s${"\0".repeat(MAX_IDENTITY_CHARS - 1)}`,
|
|
22
|
+
requestMessageId: `r${"\0".repeat(MAX_MESSAGE_ID_CHARS - 1)}`,
|
|
23
|
+
threadId: `t${"\0".repeat(MAX_IDENTITY_CHARS - 1)}`,
|
|
24
|
+
contentDigest: "f".repeat(64),
|
|
25
|
+
sourceOwned: true,
|
|
26
|
+
}), "utf-8") + 1;
|
|
27
|
+
const MAX_SOURCE_OWNED_RESERVE_BYTES = Math.ceil((MAX_REPLY_ENCODED_BYTES + MAX_ENCODED_SOURCE_OWNED_RECEIPT_BYTES + 1_024) / 1_024) * 1_024;
|
|
28
|
+
const MAX_MAILBOX_BYTES = MAX_DEFAULT_MAILBOX_BYTES + MAX_SOURCE_OWNED_RESERVE_BYTES;
|
|
29
|
+
const MAX_WAIT_TIMEOUT_MS = 300_000;
|
|
30
|
+
const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
|
|
31
|
+
const DEFAULT_PAGE_SIZE = 16;
|
|
32
|
+
const RANDOM_UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
33
|
+
const listenersByMailboxFile = new Map();
|
|
34
|
+
const assertSessionRootMailboxBounds = createReplaySafeMailboxBounder("Session root mailbox", MAX_RETAINED_REPLIES, MAX_REPLAY_RECEIPTS, MAX_MAILBOX_BYTES);
|
|
35
|
+
function requiredIdentity(value, label) {
|
|
36
|
+
if (typeof value !== "string")
|
|
37
|
+
throw new TypeError(`Session root reply ${label} is invalid.`);
|
|
38
|
+
const normalized = value.trim();
|
|
39
|
+
if (!normalized || normalized.length > MAX_IDENTITY_CHARS) {
|
|
40
|
+
throw new TypeError(`Session root reply ${label} is invalid.`);
|
|
41
|
+
}
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
function optionalIdentity(value, label) {
|
|
45
|
+
if (value === undefined)
|
|
46
|
+
return undefined;
|
|
47
|
+
return requiredIdentity(value, label);
|
|
48
|
+
}
|
|
49
|
+
function requiredContent(value) {
|
|
50
|
+
if (typeof value !== "string")
|
|
51
|
+
throw new TypeError("Session root reply content is invalid.");
|
|
52
|
+
const normalized = value.trim();
|
|
53
|
+
if (!normalized || normalized.length > MAX_REPLY_CONTENT_CHARS) {
|
|
54
|
+
throw new TypeError(`Session root reply content must contain from 1 through ${MAX_REPLY_CONTENT_CHARS.toLocaleString("en-US")} characters.`);
|
|
55
|
+
}
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
58
|
+
function requiredBoundedString(value, maxChars, label) {
|
|
59
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxChars) {
|
|
60
|
+
throw new Error(`Session root mailbox contains an invalid reply ${label}.`);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
function requiredTimestamp(value, label) {
|
|
65
|
+
const timestamp = requiredBoundedString(value, MAX_TIMESTAMP_CHARS, label);
|
|
66
|
+
const parsed = new Date(timestamp);
|
|
67
|
+
if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== timestamp) {
|
|
68
|
+
throw new Error(`Session root mailbox contains an invalid reply ${label}.`);
|
|
69
|
+
}
|
|
70
|
+
return timestamp;
|
|
71
|
+
}
|
|
72
|
+
function optionalTimestamp(value, label) {
|
|
73
|
+
return value === undefined ? undefined : requiredTimestamp(value, label);
|
|
74
|
+
}
|
|
75
|
+
function requiredAckToken(value) {
|
|
76
|
+
const token = requiredBoundedString(value, MAX_ACK_TOKEN_CHARS, "acknowledgement token");
|
|
77
|
+
if (!RANDOM_UUID_V4_PATTERN.test(token)) {
|
|
78
|
+
throw new Error("Session root mailbox contains an invalid reply acknowledgement token.");
|
|
79
|
+
}
|
|
80
|
+
return token;
|
|
81
|
+
}
|
|
82
|
+
function inputAckToken(value) {
|
|
83
|
+
if (typeof value !== "string" || value.length > MAX_ACK_TOKEN_CHARS || !RANDOM_UUID_V4_PATTERN.test(value)) {
|
|
84
|
+
throw new TypeError("Session root reply acknowledgement token is invalid.");
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
function transitionTimestamp(createdAt) {
|
|
89
|
+
const current = new Date().toISOString();
|
|
90
|
+
return current < createdAt ? createdAt : current;
|
|
91
|
+
}
|
|
92
|
+
function identityDigest(domain, identities) {
|
|
93
|
+
const hash = createHash("sha256").update(domain);
|
|
94
|
+
for (const identity of identities)
|
|
95
|
+
hash.update("\0").update(identity);
|
|
96
|
+
return hash.digest("hex");
|
|
97
|
+
}
|
|
98
|
+
/** Opaque host-owned sender address for root-originated reply-expected worker messages. */
|
|
99
|
+
export function sessionRootAddress(parentSessionId) {
|
|
100
|
+
const normalized = requiredIdentity(parentSessionId, "parent session id");
|
|
101
|
+
return `session-root-${identityDigest("pi-session-root-address-v1", [normalized])}`;
|
|
102
|
+
}
|
|
103
|
+
export function sessionRootReplyMessageId(parentSessionId, sourceAgentId, requestMessageId) {
|
|
104
|
+
return `session-root-reply-${identityDigest("pi-session-root-reply-v1", [
|
|
105
|
+
requiredIdentity(parentSessionId, "parent session id"),
|
|
106
|
+
requiredIdentity(sourceAgentId, "source agent id"),
|
|
107
|
+
requiredIdentity(requestMessageId, "request message id"),
|
|
108
|
+
])}`;
|
|
109
|
+
}
|
|
110
|
+
function replyContentDigest(content) {
|
|
111
|
+
return identityDigest("pi-session-root-reply-content-v1", [content]);
|
|
112
|
+
}
|
|
113
|
+
function replayReceipt(reply, sourceOwned = false) {
|
|
114
|
+
return {
|
|
115
|
+
messageId: reply.messageId,
|
|
116
|
+
sourceAgentId: reply.sourceAgentId,
|
|
117
|
+
requestMessageId: reply.requestMessageId,
|
|
118
|
+
...(reply.threadId ? { threadId: reply.threadId } : {}),
|
|
119
|
+
contentDigest: replyContentDigest(reply.content),
|
|
120
|
+
...(sourceOwned ? { sourceOwned: true } : {}),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function sameReplayIntent(receipt, reply) {
|
|
124
|
+
return (receipt.messageId === reply.messageId &&
|
|
125
|
+
receipt.sourceAgentId === reply.sourceAgentId &&
|
|
126
|
+
receipt.requestMessageId === reply.requestMessageId &&
|
|
127
|
+
receipt.threadId === reply.threadId &&
|
|
128
|
+
receipt.contentDigest === replyContentDigest(reply.content));
|
|
129
|
+
}
|
|
130
|
+
function isMandatory(reply) {
|
|
131
|
+
return reply.sourceReconciledAt === undefined || reply.acknowledgedAt === undefined;
|
|
132
|
+
}
|
|
133
|
+
function pruneRetainedReplies(replies) {
|
|
134
|
+
const mandatory = replies.filter(isMandatory);
|
|
135
|
+
if (mandatory.length > MAX_MANDATORY_REPLIES) {
|
|
136
|
+
throw new Error(`Session root mailbox reached its ${MAX_MANDATORY_REPLIES} mandatory reply limit.`);
|
|
137
|
+
}
|
|
138
|
+
const remainingSlots = MAX_RETAINED_REPLIES - mandatory.length;
|
|
139
|
+
const completed = replies.filter((reply) => !isMandatory(reply));
|
|
140
|
+
const retainedCompletedIds = new Set(completed.slice(-remainingSlots).map((reply) => reply.messageId));
|
|
141
|
+
return replies.filter((reply) => isMandatory(reply) || retainedCompletedIds.has(reply.messageId));
|
|
142
|
+
}
|
|
143
|
+
function encodedStateBytes(state) {
|
|
144
|
+
return Buffer.byteLength(`${JSON.stringify(state)}\n`, "utf-8");
|
|
145
|
+
}
|
|
146
|
+
function lifecycleProjectedEncodedBytes(state) {
|
|
147
|
+
return encodedStateBytes({
|
|
148
|
+
...state,
|
|
149
|
+
replies: state.replies.map((reply) => ({
|
|
150
|
+
...reply,
|
|
151
|
+
sourceReconciledAt: reply.sourceReconciledAt ?? MAX_TRANSITION_TIMESTAMP,
|
|
152
|
+
acknowledgedAt: reply.acknowledgedAt ?? MAX_TRANSITION_TIMESTAMP,
|
|
153
|
+
})),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function assertReplyEncodedByteBound(reply) {
|
|
157
|
+
// Reserve both lifecycle fields before accepting content so every durable reply can still fit as
|
|
158
|
+
// one whole entry in the delegate inbox's 16 KiB result envelope after later transitions.
|
|
159
|
+
const transitioned = {
|
|
160
|
+
...reply,
|
|
161
|
+
sourceReconciledAt: reply.sourceReconciledAt ?? reply.createdAt,
|
|
162
|
+
acknowledgedAt: reply.acknowledgedAt ?? reply.createdAt,
|
|
163
|
+
};
|
|
164
|
+
if (Buffer.byteLength(JSON.stringify(transitioned), "utf-8") > MAX_REPLY_ENCODED_BYTES) {
|
|
165
|
+
throw new Error(`Session root reply exceeds its ${MAX_REPLY_ENCODED_BYTES}-byte encoded byte bound.`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function createSessionRootReply(parentSessionId, input) {
|
|
169
|
+
const sourceAgentId = requiredIdentity(input.sourceAgentId, "source agent id");
|
|
170
|
+
const requestMessageId = requiredIdentity(input.requestMessageId, "request message id");
|
|
171
|
+
const threadId = optionalIdentity(input.threadId, "thread id");
|
|
172
|
+
const content = requiredContent(input.content);
|
|
173
|
+
const reply = {
|
|
174
|
+
messageId: sessionRootReplyMessageId(parentSessionId, sourceAgentId, requestMessageId),
|
|
175
|
+
sourceAgentId,
|
|
176
|
+
requestMessageId,
|
|
177
|
+
...(threadId ? { threadId } : {}),
|
|
178
|
+
content,
|
|
179
|
+
createdAt: new Date().toISOString(),
|
|
180
|
+
ackToken: randomUUID(),
|
|
181
|
+
};
|
|
182
|
+
assertReplyEncodedByteBound(reply);
|
|
183
|
+
return reply;
|
|
184
|
+
}
|
|
185
|
+
function parseReply(value, parentSessionId) {
|
|
186
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
187
|
+
throw new Error("Session root mailbox contains an invalid reply.");
|
|
188
|
+
}
|
|
189
|
+
const candidate = value;
|
|
190
|
+
const sourceAgentId = requiredBoundedString(candidate.sourceAgentId, MAX_IDENTITY_CHARS, "source agent id");
|
|
191
|
+
const requestMessageId = requiredBoundedString(candidate.requestMessageId, MAX_MESSAGE_ID_CHARS, "request message id");
|
|
192
|
+
const messageId = requiredBoundedString(candidate.messageId, MAX_MESSAGE_ID_CHARS, "message id");
|
|
193
|
+
const threadId = candidate.threadId === undefined
|
|
194
|
+
? undefined
|
|
195
|
+
: requiredBoundedString(candidate.threadId, MAX_IDENTITY_CHARS, "thread id");
|
|
196
|
+
const content = requiredBoundedString(candidate.content, MAX_REPLY_CONTENT_CHARS, "content");
|
|
197
|
+
const createdAt = requiredTimestamp(candidate.createdAt, "creation timestamp");
|
|
198
|
+
const ackToken = requiredAckToken(candidate.ackToken);
|
|
199
|
+
const sourceReconciledAt = optionalTimestamp(candidate.sourceReconciledAt, "source reconciliation timestamp");
|
|
200
|
+
const acknowledgedAt = optionalTimestamp(candidate.acknowledgedAt, "acknowledgement timestamp");
|
|
201
|
+
if (sourceReconciledAt !== undefined && sourceReconciledAt < createdAt) {
|
|
202
|
+
throw new Error("Session root mailbox reply source reconciliation timestamp predates creation.");
|
|
203
|
+
}
|
|
204
|
+
if (acknowledgedAt !== undefined && acknowledgedAt < createdAt) {
|
|
205
|
+
throw new Error("Session root mailbox reply acknowledgement timestamp predates creation.");
|
|
206
|
+
}
|
|
207
|
+
if (sourceAgentId.trim() !== sourceAgentId ||
|
|
208
|
+
requestMessageId.trim() !== requestMessageId ||
|
|
209
|
+
messageId !== sessionRootReplyMessageId(parentSessionId, sourceAgentId, requestMessageId) ||
|
|
210
|
+
(threadId !== undefined && threadId.trim() !== threadId) ||
|
|
211
|
+
content.trim() !== content) {
|
|
212
|
+
throw new Error("Session root mailbox contains an invalid reply.");
|
|
213
|
+
}
|
|
214
|
+
const reply = {
|
|
215
|
+
messageId,
|
|
216
|
+
sourceAgentId,
|
|
217
|
+
requestMessageId,
|
|
218
|
+
...(threadId ? { threadId } : {}),
|
|
219
|
+
content,
|
|
220
|
+
createdAt,
|
|
221
|
+
ackToken,
|
|
222
|
+
...(sourceReconciledAt ? { sourceReconciledAt } : {}),
|
|
223
|
+
...(acknowledgedAt ? { acknowledgedAt } : {}),
|
|
224
|
+
};
|
|
225
|
+
assertReplyEncodedByteBound(reply);
|
|
226
|
+
return reply;
|
|
227
|
+
}
|
|
228
|
+
function parseState(raw, parentSessionId) {
|
|
229
|
+
let value;
|
|
230
|
+
try {
|
|
231
|
+
value = JSON.parse(raw);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
throw new Error("Session root mailbox is invalid JSON.");
|
|
235
|
+
}
|
|
236
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
237
|
+
throw new Error("Session root mailbox is invalid.");
|
|
238
|
+
}
|
|
239
|
+
const candidate = value;
|
|
240
|
+
if (candidate.version !== 1 || candidate.parentSessionId !== parentSessionId || !Array.isArray(candidate.replies)) {
|
|
241
|
+
throw new Error("Session root mailbox identity conflicts with the requested foreground session.");
|
|
242
|
+
}
|
|
243
|
+
const replies = candidate.replies.map((reply) => parseReply(reply, parentSessionId));
|
|
244
|
+
const ids = new Set(replies.map((reply) => reply.messageId));
|
|
245
|
+
if (ids.size !== replies.length)
|
|
246
|
+
throw new Error("Session root mailbox contains duplicate reply identities.");
|
|
247
|
+
const acknowledgementTokens = new Set(replies.map((reply) => reply.ackToken));
|
|
248
|
+
if (acknowledgementTokens.size !== replies.length) {
|
|
249
|
+
throw new Error("Session root mailbox contains duplicate acknowledgement tokens.");
|
|
250
|
+
}
|
|
251
|
+
if (replies.length > MAX_RETAINED_REPLIES) {
|
|
252
|
+
throw new Error(`Session root mailbox exceeds its ${MAX_RETAINED_REPLIES} retained reply limit.`);
|
|
253
|
+
}
|
|
254
|
+
if (replies.filter(isMandatory).length > MAX_MANDATORY_REPLIES) {
|
|
255
|
+
throw new Error(`Session root mailbox exceeds its ${MAX_MANDATORY_REPLIES} mandatory reply limit.`);
|
|
256
|
+
}
|
|
257
|
+
if (candidate.replayReceipts !== undefined && !Array.isArray(candidate.replayReceipts)) {
|
|
258
|
+
throw new Error("Session root mailbox contains invalid replay receipt state.");
|
|
259
|
+
}
|
|
260
|
+
const receiptIds = new Set();
|
|
261
|
+
const replayReceipts = (candidate.replayReceipts ?? []).map((value) => {
|
|
262
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
263
|
+
throw new Error("Session root mailbox contains invalid replay receipt state.");
|
|
264
|
+
}
|
|
265
|
+
const receipt = value;
|
|
266
|
+
const messageId = requiredBoundedString(receipt.messageId, MAX_MESSAGE_ID_CHARS, "replay message id");
|
|
267
|
+
const sourceAgentId = requiredBoundedString(receipt.sourceAgentId, MAX_IDENTITY_CHARS, "replay source agent id");
|
|
268
|
+
const requestMessageId = requiredBoundedString(receipt.requestMessageId, MAX_MESSAGE_ID_CHARS, "replay request message id");
|
|
269
|
+
const threadId = receipt.threadId === undefined
|
|
270
|
+
? undefined
|
|
271
|
+
: requiredBoundedString(receipt.threadId, MAX_IDENTITY_CHARS, "replay thread id");
|
|
272
|
+
if (messageId !== sessionRootReplyMessageId(parentSessionId, sourceAgentId, requestMessageId) ||
|
|
273
|
+
typeof receipt.contentDigest !== "string" ||
|
|
274
|
+
!/^[a-f0-9]{64}$/.test(receipt.contentDigest) ||
|
|
275
|
+
(receipt.sourceOwned !== undefined && receipt.sourceOwned !== true) ||
|
|
276
|
+
receiptIds.has(messageId)) {
|
|
277
|
+
throw new Error("Session root mailbox contains invalid replay receipt state.");
|
|
278
|
+
}
|
|
279
|
+
receiptIds.add(messageId);
|
|
280
|
+
return {
|
|
281
|
+
messageId,
|
|
282
|
+
sourceAgentId,
|
|
283
|
+
requestMessageId,
|
|
284
|
+
...(threadId ? { threadId } : {}),
|
|
285
|
+
contentDigest: receipt.contentDigest,
|
|
286
|
+
...(receipt.sourceOwned ? { sourceOwned: true } : {}),
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
if (replayReceipts.length > MAX_REPLAY_RECEIPTS) {
|
|
290
|
+
throw new Error("Session root mailbox exceeds its replay receipt bound.");
|
|
291
|
+
}
|
|
292
|
+
if (replayReceipts.filter((receipt) => !receipt.sourceOwned).length > MAX_DEFAULT_REPLAY_RECEIPTS) {
|
|
293
|
+
throw new Error("Session root mailbox exceeds its default replay receipt reserve.");
|
|
294
|
+
}
|
|
295
|
+
for (const reply of replies) {
|
|
296
|
+
const receipt = replayReceipts.find((candidate) => candidate.messageId === reply.messageId);
|
|
297
|
+
if (receipt && !sameReplayIntent(receipt, reply)) {
|
|
298
|
+
throw new Error("Session root mailbox reply conflicts with its durable replay receipt.");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return { version: 1, parentSessionId, replies, replayReceipts };
|
|
302
|
+
}
|
|
303
|
+
function normalizeQuery(query) {
|
|
304
|
+
const maxMessages = query.maxMessages ?? DEFAULT_PAGE_SIZE;
|
|
305
|
+
if (!Number.isSafeInteger(maxMessages) || maxMessages < 1 || maxMessages > MAX_MANDATORY_REPLIES) {
|
|
306
|
+
throw new TypeError(`Session root reply page size must be from 1 through ${MAX_MANDATORY_REPLIES}.`);
|
|
307
|
+
}
|
|
308
|
+
const sourceAgentId = optionalIdentity(query.sourceAgentId, "source agent id");
|
|
309
|
+
const requestMessageId = optionalIdentity(query.requestMessageId, "request message id");
|
|
310
|
+
return {
|
|
311
|
+
maxMessages,
|
|
312
|
+
...(sourceAgentId ? { sourceAgentId } : {}),
|
|
313
|
+
...(requestMessageId ? { requestMessageId } : {}),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function abortReason(signal) {
|
|
317
|
+
return signal.reason ?? new Error("Session root reply wait was aborted.");
|
|
318
|
+
}
|
|
319
|
+
/** Bounded durable inbox for accepted worker replies addressed to one foreground session root. */
|
|
320
|
+
export class SessionRootMailbox {
|
|
321
|
+
constructor(options) {
|
|
322
|
+
this.parentSessionId = requiredIdentity(options.parentSessionId, "parent session id");
|
|
323
|
+
this.file = sessionRootMailboxFile(options.agentDir, this.parentSessionId);
|
|
324
|
+
}
|
|
325
|
+
/** Validate and size one canonical reply without mutating durable inbox state. */
|
|
326
|
+
assertReplyInput(input) {
|
|
327
|
+
createSessionRootReply(this.parentSessionId, input);
|
|
328
|
+
}
|
|
329
|
+
enqueueReply(input) {
|
|
330
|
+
return this.enqueueReplyWithAuthority(input, false);
|
|
331
|
+
}
|
|
332
|
+
/** Enqueue a reply whose permanent replay authority is the source worker's durable receipt. */
|
|
333
|
+
enqueueSourceOwnedReply(input) {
|
|
334
|
+
return this.enqueueReplyWithAuthority(input, true);
|
|
335
|
+
}
|
|
336
|
+
enqueueReplyWithAuthority(input, sourceOwned) {
|
|
337
|
+
const reply = createSessionRootReply(this.parentSessionId, input);
|
|
338
|
+
const { sourceAgentId, requestMessageId, threadId, content } = reply;
|
|
339
|
+
let accepted = reply;
|
|
340
|
+
let created = false;
|
|
341
|
+
let completedReplay = false;
|
|
342
|
+
this.update((state) => {
|
|
343
|
+
const existing = state.replies.find((candidate) => candidate.messageId === reply.messageId);
|
|
344
|
+
const receipt = state.replayReceipts.find((candidate) => candidate.messageId === reply.messageId);
|
|
345
|
+
if (existing) {
|
|
346
|
+
if (existing.sourceAgentId !== sourceAgentId ||
|
|
347
|
+
existing.requestMessageId !== requestMessageId ||
|
|
348
|
+
existing.threadId !== threadId ||
|
|
349
|
+
existing.content !== content) {
|
|
350
|
+
throw new Error("Session root reply identity conflicts with an existing reply.");
|
|
351
|
+
}
|
|
352
|
+
if (receipt && !sameReplayIntent(receipt, reply)) {
|
|
353
|
+
throw new Error("Session root reply identity conflicts with its durable replay receipt.");
|
|
354
|
+
}
|
|
355
|
+
accepted = existing;
|
|
356
|
+
return receipt
|
|
357
|
+
? state
|
|
358
|
+
: { ...state, replayReceipts: [...state.replayReceipts, replayReceipt(existing, sourceOwned)] };
|
|
359
|
+
}
|
|
360
|
+
if (receipt) {
|
|
361
|
+
if (!sameReplayIntent(receipt, reply)) {
|
|
362
|
+
throw new Error("Session root reply identity conflicts with its durable replay receipt.");
|
|
363
|
+
}
|
|
364
|
+
completedReplay = true;
|
|
365
|
+
return state;
|
|
366
|
+
}
|
|
367
|
+
if (state.replies.filter(isMandatory).length >= MAX_MANDATORY_REPLIES) {
|
|
368
|
+
throw new Error(`Session root mailbox reached its ${MAX_MANDATORY_REPLIES} mandatory reply limit.`);
|
|
369
|
+
}
|
|
370
|
+
if (!sourceOwned &&
|
|
371
|
+
state.replayReceipts.filter((candidate) => !candidate.sourceOwned).length >= MAX_DEFAULT_REPLAY_RECEIPTS) {
|
|
372
|
+
throw new Error(`Session root mailbox default replay receipt capacity reached its ${MAX_DEFAULT_REPLAY_RECEIPTS} entry limit.`);
|
|
373
|
+
}
|
|
374
|
+
if (state.replies.some((candidate) => candidate.ackToken === reply.ackToken)) {
|
|
375
|
+
throw new Error("Session root mailbox could not allocate a unique acknowledgement token.");
|
|
376
|
+
}
|
|
377
|
+
created = true;
|
|
378
|
+
return {
|
|
379
|
+
...state,
|
|
380
|
+
replies: [...state.replies, reply],
|
|
381
|
+
replayReceipts: [...state.replayReceipts, replayReceipt(reply, sourceOwned)],
|
|
382
|
+
};
|
|
383
|
+
}, !sourceOwned);
|
|
384
|
+
if (created)
|
|
385
|
+
this.notify();
|
|
386
|
+
if (completedReplay)
|
|
387
|
+
return { status: "completed_replay", messageId: reply.messageId, created: false };
|
|
388
|
+
return {
|
|
389
|
+
status: "retained",
|
|
390
|
+
messageId: accepted.messageId,
|
|
391
|
+
reply: structuredClone(accepted),
|
|
392
|
+
created,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
pendingReplies(query = {}) {
|
|
396
|
+
const normalized = normalizeQuery(query);
|
|
397
|
+
return this.read()
|
|
398
|
+
.replies.filter((reply) => reply.acknowledgedAt === undefined &&
|
|
399
|
+
(normalized.sourceAgentId === undefined || reply.sourceAgentId === normalized.sourceAgentId) &&
|
|
400
|
+
(normalized.requestMessageId === undefined || reply.requestMessageId === normalized.requestMessageId))
|
|
401
|
+
.slice(0, normalized.maxMessages)
|
|
402
|
+
.map((reply) => structuredClone(reply));
|
|
403
|
+
}
|
|
404
|
+
retainedReplies() {
|
|
405
|
+
return this.read().replies.map((reply) => structuredClone(reply));
|
|
406
|
+
}
|
|
407
|
+
getReply(messageId) {
|
|
408
|
+
const normalized = requiredIdentity(messageId, "message id");
|
|
409
|
+
const reply = this.read().replies.find((candidate) => candidate.messageId === normalized);
|
|
410
|
+
return reply ? structuredClone(reply) : undefined;
|
|
411
|
+
}
|
|
412
|
+
markSourceReconciled(messageId) {
|
|
413
|
+
const normalized = requiredIdentity(messageId, "message id");
|
|
414
|
+
let accepted = false;
|
|
415
|
+
let changed = false;
|
|
416
|
+
this.update((state) => {
|
|
417
|
+
const existing = state.replies.find((reply) => reply.messageId === normalized);
|
|
418
|
+
if (!existing)
|
|
419
|
+
return state;
|
|
420
|
+
accepted = true;
|
|
421
|
+
if (existing.sourceReconciledAt !== undefined)
|
|
422
|
+
return state;
|
|
423
|
+
changed = true;
|
|
424
|
+
return {
|
|
425
|
+
...state,
|
|
426
|
+
replies: state.replies.map((reply) => reply.messageId === normalized
|
|
427
|
+
? { ...reply, sourceReconciledAt: transitionTimestamp(reply.createdAt) }
|
|
428
|
+
: reply),
|
|
429
|
+
};
|
|
430
|
+
});
|
|
431
|
+
if (changed)
|
|
432
|
+
this.notify();
|
|
433
|
+
return accepted;
|
|
434
|
+
}
|
|
435
|
+
releaseSourceReplayReceipt(messageId) {
|
|
436
|
+
const normalized = requiredIdentity(messageId, "message id");
|
|
437
|
+
let released = false;
|
|
438
|
+
this.update((state) => ({
|
|
439
|
+
...state,
|
|
440
|
+
replayReceipts: state.replayReceipts.filter((receipt) => {
|
|
441
|
+
if (receipt.messageId !== normalized || !receipt.sourceOwned)
|
|
442
|
+
return true;
|
|
443
|
+
released = true;
|
|
444
|
+
return false;
|
|
445
|
+
}),
|
|
446
|
+
}));
|
|
447
|
+
if (released)
|
|
448
|
+
this.notify();
|
|
449
|
+
return released;
|
|
450
|
+
}
|
|
451
|
+
acknowledge(messageId, ackToken) {
|
|
452
|
+
const normalizedMessageId = requiredIdentity(messageId, "message id");
|
|
453
|
+
const normalizedToken = inputAckToken(ackToken);
|
|
454
|
+
let accepted = false;
|
|
455
|
+
let changed = false;
|
|
456
|
+
this.update((state) => {
|
|
457
|
+
const existing = state.replies.find((reply) => reply.messageId === normalizedMessageId);
|
|
458
|
+
if (!existing)
|
|
459
|
+
return state;
|
|
460
|
+
if (existing.ackToken !== normalizedToken) {
|
|
461
|
+
throw new Error("Session root reply acknowledgement token does not match.");
|
|
462
|
+
}
|
|
463
|
+
accepted = true;
|
|
464
|
+
if (existing.acknowledgedAt !== undefined)
|
|
465
|
+
return state;
|
|
466
|
+
changed = true;
|
|
467
|
+
return {
|
|
468
|
+
...state,
|
|
469
|
+
replies: state.replies.map((reply) => reply.messageId === normalizedMessageId
|
|
470
|
+
? { ...reply, acknowledgedAt: transitionTimestamp(reply.createdAt) }
|
|
471
|
+
: reply),
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
if (changed)
|
|
475
|
+
this.notify();
|
|
476
|
+
return accepted;
|
|
477
|
+
}
|
|
478
|
+
async waitForReplies(options = {}) {
|
|
479
|
+
const query = normalizeQuery(options);
|
|
480
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
481
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > MAX_WAIT_TIMEOUT_MS) {
|
|
482
|
+
throw new TypeError(`Session root reply wait timeout must be from 0 through ${MAX_WAIT_TIMEOUT_MS}.`);
|
|
483
|
+
}
|
|
484
|
+
if (options.signal?.aborted)
|
|
485
|
+
throw abortReason(options.signal);
|
|
486
|
+
const currentReplies = () => this.pendingReplies(query);
|
|
487
|
+
const immediate = currentReplies();
|
|
488
|
+
if (immediate.length > 0)
|
|
489
|
+
return { replies: immediate, timedOut: false };
|
|
490
|
+
return new Promise((resolve, reject) => {
|
|
491
|
+
let settled = false;
|
|
492
|
+
let unsubscribe = () => undefined;
|
|
493
|
+
let timeout;
|
|
494
|
+
const signal = options.signal;
|
|
495
|
+
const cleanup = () => {
|
|
496
|
+
unsubscribe();
|
|
497
|
+
if (timeout)
|
|
498
|
+
clearTimeout(timeout);
|
|
499
|
+
if (signal)
|
|
500
|
+
signal.removeEventListener("abort", onAbort);
|
|
501
|
+
};
|
|
502
|
+
const settleFromPredicate = () => {
|
|
503
|
+
if (settled)
|
|
504
|
+
return;
|
|
505
|
+
let replies;
|
|
506
|
+
try {
|
|
507
|
+
replies = currentReplies();
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
settled = true;
|
|
511
|
+
cleanup();
|
|
512
|
+
reject(error);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (replies.length === 0)
|
|
516
|
+
return;
|
|
517
|
+
settled = true;
|
|
518
|
+
cleanup();
|
|
519
|
+
resolve({ replies, timedOut: false });
|
|
520
|
+
};
|
|
521
|
+
const onAbort = () => {
|
|
522
|
+
if (settled)
|
|
523
|
+
return;
|
|
524
|
+
settled = true;
|
|
525
|
+
cleanup();
|
|
526
|
+
reject(abortReason(signal));
|
|
527
|
+
};
|
|
528
|
+
unsubscribe = this.subscribe(settleFromPredicate);
|
|
529
|
+
if (settled) {
|
|
530
|
+
unsubscribe();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (signal) {
|
|
534
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
535
|
+
if (signal.aborted) {
|
|
536
|
+
onAbort();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
timeout = setTimeout(() => {
|
|
541
|
+
if (settled)
|
|
542
|
+
return;
|
|
543
|
+
settled = true;
|
|
544
|
+
cleanup();
|
|
545
|
+
resolve({ replies: [], timedOut: true });
|
|
546
|
+
}, timeoutMs);
|
|
547
|
+
if (typeof timeout === "object" && timeout && "unref" in timeout)
|
|
548
|
+
timeout.unref();
|
|
549
|
+
settleFromPredicate();
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Subscribes to process-local mutations for every mailbox instance backed by this file.
|
|
554
|
+
* Callers must unsubscribe; durable cross-process delivery remains the control owner's responsibility.
|
|
555
|
+
*/
|
|
556
|
+
subscribe(listener) {
|
|
557
|
+
const listeners = listenersByMailboxFile.get(this.file) ?? new Set();
|
|
558
|
+
listenersByMailboxFile.set(this.file, listeners);
|
|
559
|
+
listeners.add(listener);
|
|
560
|
+
let subscribed = true;
|
|
561
|
+
return () => {
|
|
562
|
+
if (!subscribed)
|
|
563
|
+
return;
|
|
564
|
+
subscribed = false;
|
|
565
|
+
listeners.delete(listener);
|
|
566
|
+
if (listeners.size === 0 && listenersByMailboxFile.get(this.file) === listeners) {
|
|
567
|
+
listenersByMailboxFile.delete(this.file);
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
notify() {
|
|
572
|
+
const listeners = listenersByMailboxFile.get(this.file);
|
|
573
|
+
if (!listeners)
|
|
574
|
+
return;
|
|
575
|
+
for (const listener of [...listeners]) {
|
|
576
|
+
try {
|
|
577
|
+
listener();
|
|
578
|
+
}
|
|
579
|
+
catch {
|
|
580
|
+
// Subscribers are advisory; the durable reply mutation remains authoritative.
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
read() {
|
|
585
|
+
if (!existsSync(this.file)) {
|
|
586
|
+
return { version: 1, parentSessionId: this.parentSessionId, replies: [], replayReceipts: [] };
|
|
587
|
+
}
|
|
588
|
+
return parseState(readBoundedTextFileSync(this.file, MAX_MAILBOX_BYTES, "Session root mailbox durable size bound"), this.parentSessionId);
|
|
589
|
+
}
|
|
590
|
+
update(mutator, defaultAdmission = false) {
|
|
591
|
+
withFileLockSync(this.file, () => {
|
|
592
|
+
const state = this.read();
|
|
593
|
+
const previousDefaultBytes = lifecycleProjectedEncodedBytes(state);
|
|
594
|
+
const mutated = mutator(state);
|
|
595
|
+
let next = {
|
|
596
|
+
...mutated,
|
|
597
|
+
replies: pruneRetainedReplies(mutated.replies),
|
|
598
|
+
replayReceipts: [...mutated.replayReceipts],
|
|
599
|
+
};
|
|
600
|
+
while (lifecycleProjectedEncodedBytes(next) > (defaultAdmission ? MAX_DEFAULT_MAILBOX_BYTES : MAX_MAILBOX_BYTES)) {
|
|
601
|
+
const oldestCompletedIndex = next.replies.findIndex((reply) => !isMandatory(reply));
|
|
602
|
+
if (oldestCompletedIndex >= 0) {
|
|
603
|
+
next = { ...next, replies: next.replies.filter((_, index) => index !== oldestCompletedIndex) };
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
for (const reply of next.replies)
|
|
609
|
+
assertReplyEncodedByteBound(reply);
|
|
610
|
+
const addedReplayReceipt = mutated.replayReceipts.length > state.replayReceipts.length;
|
|
611
|
+
const bytesWithoutAddedReceipt = addedReplayReceipt
|
|
612
|
+
? lifecycleProjectedEncodedBytes({ ...next, replayReceipts: next.replayReceipts.slice(0, -1) })
|
|
613
|
+
: undefined;
|
|
614
|
+
const nextDefaultBytes = lifecycleProjectedEncodedBytes(next);
|
|
615
|
+
if (defaultAdmission &&
|
|
616
|
+
nextDefaultBytes > MAX_DEFAULT_MAILBOX_BYTES &&
|
|
617
|
+
nextDefaultBytes > previousDefaultBytes) {
|
|
618
|
+
throw new Error("Session root mailbox default replay receipt storage reserve is exhausted.");
|
|
619
|
+
}
|
|
620
|
+
assertSessionRootMailboxBounds(next.replies.length, next.replayReceipts.length, state.replayReceipts.length, nextDefaultBytes, bytesWithoutAddedReceipt);
|
|
621
|
+
if (JSON.stringify(next) !== JSON.stringify(state)) {
|
|
622
|
+
writeFileAtomicSync(this.file, `${JSON.stringify(next)}\n`, { mode: 0o600 });
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
//# sourceMappingURL=session-root-mailbox.js.map
|