@caupulican/pi-adaptative 0.86.3 → 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 +32 -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 +21 -2
- 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 +749 -103
- 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
|
@@ -1,15 +1,45 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { workerAgentMailboxFile } from "../agent-paths.js";
|
|
4
|
+
import { parseBoundedStringArray } from "../orchestration/bounded-string-array.js";
|
|
5
|
+
import { MAX_ORCHESTRATION_COLLECTION_LENGTH, MAX_ORCHESTRATION_IDENTIFIER_LENGTH, } from "../orchestration/contracts.js";
|
|
4
6
|
import { withFileLockSync, writeFileAtomicSync } from "../util/atomic-file.js";
|
|
5
7
|
import { readBoundedTextFileSync } from "../util/bounded-file.js";
|
|
8
|
+
import { createReplaySafeMailboxBounder } from "./replay-safe-mailbox-bounds.js";
|
|
6
9
|
const MAX_MAILBOX_MESSAGES = 64;
|
|
7
10
|
const MAX_MAILBOX_MESSAGE_CHARS = 4_096;
|
|
8
|
-
const
|
|
11
|
+
const MAX_ORDINARY_MAILBOX_BYTES = 128 * 1024;
|
|
9
12
|
const MAX_MAILBOX_MESSAGE_ID_CHARS = 512;
|
|
10
13
|
const MAX_MAILBOX_TIMESTAMP_CHARS = 128;
|
|
11
14
|
const MAX_MAILBOX_IDENTITY_CHARS = 512;
|
|
12
|
-
const
|
|
15
|
+
const MAX_MAILBOX_IDEMPOTENCY_KEY_CHARS = 2_048;
|
|
16
|
+
const MAX_ORDINARY_RETAINED_MESSAGES = MAX_MAILBOX_MESSAGES * 2;
|
|
17
|
+
const MAX_MANDATORY_RETAINED_MESSAGES = MAX_MAILBOX_MESSAGES;
|
|
18
|
+
const MAX_MAILBOX_RETAINED_MESSAGES = MAX_ORDINARY_RETAINED_MESSAGES + MAX_MANDATORY_RETAINED_MESSAGES;
|
|
19
|
+
const MAX_REPLAY_EVIDENCE_SLOTS = MAX_ORDINARY_RETAINED_MESSAGES * 4;
|
|
20
|
+
const MAX_MAILBOX_REPLAY_RECEIPTS = MAX_REPLAY_EVIDENCE_SLOTS + MAX_ORDINARY_RETAINED_MESSAGES;
|
|
21
|
+
const MAX_MAILBOX_TRANSITION_TIMESTAMP = "9999-12-31T23:59:59.999Z";
|
|
22
|
+
const MAX_ENCODED_MANDATORY_MESSAGE_BYTES = Buffer.byteLength(JSON.stringify({
|
|
23
|
+
messageId: `worker-message-${"f".repeat(64)}`,
|
|
24
|
+
kind: "follow_up",
|
|
25
|
+
content: `c${"\0".repeat(MAX_MAILBOX_MESSAGE_CHARS - 1)}`,
|
|
26
|
+
senderAgentId: `s${"\0".repeat(MAX_MAILBOX_IDENTITY_CHARS - 1)}`,
|
|
27
|
+
threadId: `t${"\0".repeat(MAX_MAILBOX_IDENTITY_CHARS - 1)}`,
|
|
28
|
+
replyToMessageId: `r${"\0".repeat(MAX_MAILBOX_MESSAGE_ID_CHARS - 1)}`,
|
|
29
|
+
expectReply: true,
|
|
30
|
+
task: {
|
|
31
|
+
kind: "terminal_handoff",
|
|
32
|
+
sourceAttemptId: `a${"\0".repeat(MAX_MAILBOX_IDENTITY_CHARS - 1)}`,
|
|
33
|
+
},
|
|
34
|
+
createdAt: MAX_MAILBOX_TRANSITION_TIMESTAMP,
|
|
35
|
+
deliveredAt: MAX_MAILBOX_TRANSITION_TIMESTAMP,
|
|
36
|
+
}), "utf-8") + 1;
|
|
37
|
+
// The additive reserve keeps every previously valid 128 KiB ordinary mailbox migration-safe and
|
|
38
|
+
// covers one maximum accepted source acknowledgement transaction (locked by the saturation tests).
|
|
39
|
+
const MAX_MANDATORY_TRANSITION_RESERVE_BYTES = Math.max(48 * 1024, MAX_ENCODED_MANDATORY_MESSAGE_BYTES + 1_024);
|
|
40
|
+
const MAX_MAILBOX_BYTES = MAX_ORDINARY_MAILBOX_BYTES + MAX_MANDATORY_TRANSITION_RESERVE_BYTES;
|
|
41
|
+
const CONTENT_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
|
42
|
+
const assertWorkerAgentMailboxBounds = createReplaySafeMailboxBounder("Worker agent mailbox", MAX_MAILBOX_RETAINED_MESSAGES, MAX_MAILBOX_REPLAY_RECEIPTS, MAX_MAILBOX_BYTES);
|
|
13
43
|
function mailboxDigest(parentSessionId, agentId) {
|
|
14
44
|
return createHash("sha256")
|
|
15
45
|
.update("pi-worker-agent-mailbox-v1")
|
|
@@ -27,6 +57,92 @@ function assertIdentity(value, label) {
|
|
|
27
57
|
throw new TypeError(`Worker ${label} exceeds 512 characters.`);
|
|
28
58
|
return normalized;
|
|
29
59
|
}
|
|
60
|
+
/** Derive one target-fenced mailbox replay identity from a host-owned broadcast call identity. */
|
|
61
|
+
export function workerAgentBroadcastTargetIdempotencyKey(baseIdempotencyKey, agentId) {
|
|
62
|
+
const replayIdentity = baseIdempotencyKey.trim();
|
|
63
|
+
if (!replayIdentity || replayIdentity.length > MAX_MAILBOX_IDEMPOTENCY_KEY_CHARS) {
|
|
64
|
+
throw new TypeError("Worker broadcast idempotency key is invalid.");
|
|
65
|
+
}
|
|
66
|
+
const targetAgentId = assertIdentity(agentId, "agent id");
|
|
67
|
+
return `worker-broadcast-${createHash("sha256")
|
|
68
|
+
.update("pi-worker-broadcast-target-v1")
|
|
69
|
+
.update("\0")
|
|
70
|
+
.update(replayIdentity)
|
|
71
|
+
.update("\0")
|
|
72
|
+
.update(targetAgentId)
|
|
73
|
+
.digest("hex")}`;
|
|
74
|
+
}
|
|
75
|
+
/** Session-scoped replay identity. The coordinator fences one accepted id to exactly one target mailbox. */
|
|
76
|
+
export function workerAgentMessageId(parentSessionId, idempotencyKey) {
|
|
77
|
+
const sessionId = assertIdentity(parentSessionId, "parent session id");
|
|
78
|
+
const replayIdentity = idempotencyKey.trim();
|
|
79
|
+
if (!replayIdentity || replayIdentity.length > MAX_MAILBOX_IDEMPOTENCY_KEY_CHARS) {
|
|
80
|
+
throw new TypeError("Worker control idempotency key is invalid.");
|
|
81
|
+
}
|
|
82
|
+
return `worker-message-${createHash("sha256")
|
|
83
|
+
.update("pi-worker-agent-message-v2")
|
|
84
|
+
.update("\0")
|
|
85
|
+
.update(sessionId)
|
|
86
|
+
.update("\0")
|
|
87
|
+
.update(replayIdentity)
|
|
88
|
+
.digest("hex")}`;
|
|
89
|
+
}
|
|
90
|
+
function contentDigest(domain, content) {
|
|
91
|
+
return createHash("sha256").update(domain).update("\0").update(content).digest("hex");
|
|
92
|
+
}
|
|
93
|
+
function controlIntentDigest(input) {
|
|
94
|
+
return contentDigest("pi-worker-agent-control-intent-v1", JSON.stringify([
|
|
95
|
+
input.content,
|
|
96
|
+
input.senderAgentId ?? null,
|
|
97
|
+
input.threadId ?? null,
|
|
98
|
+
input.replyToMessageId ?? null,
|
|
99
|
+
input.expectReply === true,
|
|
100
|
+
input.task ?? null,
|
|
101
|
+
]));
|
|
102
|
+
}
|
|
103
|
+
function replyIntentDigest(content) {
|
|
104
|
+
return contentDigest("pi-worker-reply-content-v1", content);
|
|
105
|
+
}
|
|
106
|
+
function controlReplayReceipt(messageId, intentDigest, deliveredAt) {
|
|
107
|
+
return { kind: "control", messageId, intentDigest, ...(deliveredAt ? { deliveredAt } : {}) };
|
|
108
|
+
}
|
|
109
|
+
function controlReplayReceiptFor(receipts, messageId) {
|
|
110
|
+
return receipts.find((receipt) => receipt.kind === "control" && receipt.messageId === messageId);
|
|
111
|
+
}
|
|
112
|
+
function replyReplayReceiptFor(receipts, messageId) {
|
|
113
|
+
return receipts.find((receipt) => receipt.kind === "reply" && receipt.messageId === messageId);
|
|
114
|
+
}
|
|
115
|
+
function requiredMailboxTimestamp(value, label) {
|
|
116
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_MAILBOX_TIMESTAMP_CHARS) {
|
|
117
|
+
throw new Error(`Worker agent mailbox contains an invalid message ${label}.`);
|
|
118
|
+
}
|
|
119
|
+
const parsed = new Date(value);
|
|
120
|
+
if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== value) {
|
|
121
|
+
throw new Error(`Worker agent mailbox contains an invalid message ${label}.`);
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
function transitionTimestamp(floor) {
|
|
126
|
+
const current = new Date().toISOString();
|
|
127
|
+
return Date.parse(current) < Date.parse(floor) ? floor : current;
|
|
128
|
+
}
|
|
129
|
+
function parseReplyReceiptFields(receipt, label) {
|
|
130
|
+
if (typeof receipt.replyMessageId !== "string" ||
|
|
131
|
+
!receipt.replyMessageId ||
|
|
132
|
+
receipt.replyMessageId.length > MAX_MAILBOX_MESSAGE_ID_CHARS ||
|
|
133
|
+
typeof receipt.requestSenderId !== "string" ||
|
|
134
|
+
!receipt.requestSenderId ||
|
|
135
|
+
receipt.requestSenderId.length > MAX_MAILBOX_IDENTITY_CHARS ||
|
|
136
|
+
typeof receipt.contentDigest !== "string" ||
|
|
137
|
+
!CONTENT_DIGEST_PATTERN.test(receipt.contentDigest)) {
|
|
138
|
+
throw new Error(`Worker agent mailbox contains invalid ${label}.`);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
replyMessageId: receipt.replyMessageId,
|
|
142
|
+
requestSenderId: receipt.requestSenderId,
|
|
143
|
+
contentDigest: receipt.contentDigest,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
30
146
|
function parseState(raw, parentSessionId, agentId) {
|
|
31
147
|
let candidate;
|
|
32
148
|
try {
|
|
@@ -46,6 +162,42 @@ function parseState(raw, parentSessionId, agentId) {
|
|
|
46
162
|
throw new Error("Worker agent mailbox identity conflicts with the requested logical worker.");
|
|
47
163
|
}
|
|
48
164
|
const messages = state.messages.map((message) => {
|
|
165
|
+
let task;
|
|
166
|
+
let replyReceipt;
|
|
167
|
+
if (message?.task !== undefined) {
|
|
168
|
+
if (!message.task || typeof message.task !== "object" || Array.isArray(message.task)) {
|
|
169
|
+
throw new Error("Worker agent mailbox contains invalid task-bearing message metadata.");
|
|
170
|
+
}
|
|
171
|
+
const taskRecord = message.task;
|
|
172
|
+
if (taskRecord.kind === "agent_turn" &&
|
|
173
|
+
Object.keys(taskRecord).every((field) => field === "kind" || field === "dependsOnTaskIds")) {
|
|
174
|
+
const dependsOnTaskIds = normalizeWorkerAgentDependencyTaskIds(taskRecord.dependsOnTaskIds);
|
|
175
|
+
task = {
|
|
176
|
+
kind: "agent_turn",
|
|
177
|
+
...(dependsOnTaskIds.length > 0 ? { dependsOnTaskIds } : {}),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
else if (taskRecord.kind === "terminal_handoff" &&
|
|
181
|
+
typeof taskRecord.sourceAttemptId === "string" &&
|
|
182
|
+
taskRecord.sourceAttemptId.length > 0 &&
|
|
183
|
+
taskRecord.sourceAttemptId.length <= MAX_MAILBOX_IDENTITY_CHARS &&
|
|
184
|
+
Object.keys(taskRecord).every((field) => field === "kind" || field === "sourceAttemptId")) {
|
|
185
|
+
task = { kind: "terminal_handoff", sourceAttemptId: taskRecord.sourceAttemptId };
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
throw new Error("Worker agent mailbox contains invalid task-bearing message metadata.");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (message?.replyReceipt !== undefined) {
|
|
192
|
+
if (!message.replyReceipt || typeof message.replyReceipt !== "object" || Array.isArray(message.replyReceipt)) {
|
|
193
|
+
throw new Error("Worker agent mailbox contains invalid reply receipt metadata.");
|
|
194
|
+
}
|
|
195
|
+
const receipt = message.replyReceipt;
|
|
196
|
+
if (!Object.keys(receipt).every((field) => field === "replyMessageId" || field === "requestSenderId" || field === "contentDigest")) {
|
|
197
|
+
throw new Error("Worker agent mailbox contains invalid reply receipt metadata.");
|
|
198
|
+
}
|
|
199
|
+
replyReceipt = parseReplyReceiptFields(receipt, "reply receipt metadata");
|
|
200
|
+
}
|
|
49
201
|
if (!message ||
|
|
50
202
|
typeof message !== "object" ||
|
|
51
203
|
Array.isArray(message) ||
|
|
@@ -79,17 +231,192 @@ function parseState(raw, parentSessionId, agentId) {
|
|
|
79
231
|
(message.repliedAt !== undefined &&
|
|
80
232
|
(typeof message.repliedAt !== "string" ||
|
|
81
233
|
message.repliedAt.length === 0 ||
|
|
82
|
-
message.repliedAt.length > MAX_MAILBOX_TIMESTAMP_CHARS))
|
|
234
|
+
message.repliedAt.length > MAX_MAILBOX_TIMESTAMP_CHARS)) ||
|
|
235
|
+
(message.failedAt !== undefined &&
|
|
236
|
+
(typeof message.failedAt !== "string" ||
|
|
237
|
+
message.failedAt.length === 0 ||
|
|
238
|
+
message.failedAt.length > MAX_MAILBOX_TIMESTAMP_CHARS)) ||
|
|
239
|
+
(message.failureReason !== undefined &&
|
|
240
|
+
(typeof message.failureReason !== "string" ||
|
|
241
|
+
message.failureReason.length === 0 ||
|
|
242
|
+
message.failureReason.length > MAX_MAILBOX_IDENTITY_CHARS))) {
|
|
83
243
|
throw new Error("Worker agent mailbox contains an invalid message.");
|
|
84
244
|
}
|
|
85
|
-
|
|
245
|
+
const createdAt = requiredMailboxTimestamp(message.createdAt, "creation timestamp");
|
|
246
|
+
const deliveredAt = message.deliveredAt === undefined
|
|
247
|
+
? undefined
|
|
248
|
+
: requiredMailboxTimestamp(message.deliveredAt, "delivery timestamp");
|
|
249
|
+
const repliedAt = message.repliedAt === undefined ? undefined : requiredMailboxTimestamp(message.repliedAt, "reply timestamp");
|
|
250
|
+
const failedAt = message.failedAt === undefined ? undefined : requiredMailboxTimestamp(message.failedAt, "failure timestamp");
|
|
251
|
+
if (deliveredAt !== undefined && Date.parse(deliveredAt) < Date.parse(createdAt)) {
|
|
252
|
+
throw new Error("Worker agent mailbox message delivery timestamp predates creation.");
|
|
253
|
+
}
|
|
254
|
+
if (repliedAt !== undefined && deliveredAt === undefined) {
|
|
255
|
+
throw new Error("Worker agent mailbox message reply timestamp has no delivery timestamp.");
|
|
256
|
+
}
|
|
257
|
+
if (repliedAt !== undefined && deliveredAt !== undefined && Date.parse(repliedAt) < Date.parse(deliveredAt)) {
|
|
258
|
+
throw new Error("Worker agent mailbox message reply timestamp predates delivery.");
|
|
259
|
+
}
|
|
260
|
+
if (repliedAt !== undefined && message.expectReply !== true) {
|
|
261
|
+
throw new Error("Worker agent mailbox replied message did not expect a reply.");
|
|
262
|
+
}
|
|
263
|
+
if (failedAt !== undefined && Date.parse(failedAt) < Date.parse(createdAt)) {
|
|
264
|
+
throw new Error("Worker agent mailbox message failure timestamp predates creation.");
|
|
265
|
+
}
|
|
266
|
+
if ((failedAt === undefined) !== (message.failureReason === undefined) ||
|
|
267
|
+
(failedAt !== undefined && deliveredAt !== undefined)) {
|
|
268
|
+
throw new Error("Worker agent mailbox contains invalid task failure state.");
|
|
269
|
+
}
|
|
270
|
+
if (failedAt !== undefined && (task === undefined || repliedAt !== undefined || replyReceipt !== undefined)) {
|
|
271
|
+
throw new Error("Worker agent mailbox contains invalid task failure state.");
|
|
272
|
+
}
|
|
273
|
+
if ((repliedAt === undefined) !== (replyReceipt === undefined)) {
|
|
274
|
+
throw new Error("Worker agent mailbox replied message is missing its durable reply receipt.");
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
...message,
|
|
278
|
+
createdAt,
|
|
279
|
+
...(deliveredAt ? { deliveredAt } : {}),
|
|
280
|
+
...(repliedAt ? { repliedAt } : {}),
|
|
281
|
+
...(replyReceipt ? { replyReceipt } : {}),
|
|
282
|
+
...(failedAt ? { failedAt, failureReason: message.failureReason } : {}),
|
|
283
|
+
...(task ? { task } : {}),
|
|
284
|
+
};
|
|
86
285
|
});
|
|
87
|
-
|
|
88
|
-
|
|
286
|
+
if (new Set(messages.map((message) => message.messageId)).size !== messages.length) {
|
|
287
|
+
throw new Error("Worker agent mailbox contains duplicate message identities.");
|
|
288
|
+
}
|
|
289
|
+
const pendingMessages = messages.filter(isPendingMessage);
|
|
290
|
+
if (pendingMessages.filter((message) => !hasExternalReplayAuthority(message)).length > MAX_MAILBOX_MESSAGES)
|
|
89
291
|
throw new Error("Worker agent mailbox exceeds its pending-message bound.");
|
|
292
|
+
if (pendingMessages.filter(hasExternalReplayAuthority).length > MAX_MAILBOX_MESSAGES) {
|
|
293
|
+
throw new Error("Worker agent mailbox exceeds its mandatory pending-message bound.");
|
|
294
|
+
}
|
|
90
295
|
if (messages.length > MAX_MAILBOX_RETAINED_MESSAGES)
|
|
91
296
|
throw new Error("Worker agent mailbox exceeds its retained-message bound.");
|
|
92
|
-
|
|
297
|
+
if (messages.filter(usesMandatoryRetainedReserve).length > MAX_MANDATORY_RETAINED_MESSAGES) {
|
|
298
|
+
throw new Error("Worker agent mailbox exceeds its mandatory retained-message reserve.");
|
|
299
|
+
}
|
|
300
|
+
if (messages.filter((message) => !usesMandatoryRetainedReserve(message)).length > MAX_ORDINARY_RETAINED_MESSAGES) {
|
|
301
|
+
throw new Error("Worker agent mailbox exceeds its ordinary retained-message bound.");
|
|
302
|
+
}
|
|
303
|
+
if (state.replyAcknowledgements !== undefined && !Array.isArray(state.replyAcknowledgements)) {
|
|
304
|
+
throw new Error("Worker agent mailbox contains invalid reply acknowledgement state.");
|
|
305
|
+
}
|
|
306
|
+
const messagesById = new Map(messages.map((message) => [message.messageId, message]));
|
|
307
|
+
const acknowledgedMessageIds = new Set();
|
|
308
|
+
const acknowledgementIds = new Set();
|
|
309
|
+
const replyAcknowledgements = (state.replyAcknowledgements ?? []).map((acknowledgement) => {
|
|
310
|
+
if (!acknowledgement ||
|
|
311
|
+
typeof acknowledgement !== "object" ||
|
|
312
|
+
Array.isArray(acknowledgement) ||
|
|
313
|
+
typeof acknowledgement.messageId !== "string" ||
|
|
314
|
+
acknowledgement.messageId.length === 0 ||
|
|
315
|
+
acknowledgement.messageId.length > MAX_MAILBOX_MESSAGE_ID_CHARS ||
|
|
316
|
+
typeof acknowledgement.acknowledgementId !== "string" ||
|
|
317
|
+
acknowledgement.acknowledgementId.length === 0 ||
|
|
318
|
+
acknowledgement.acknowledgementId.length > MAX_MAILBOX_MESSAGE_ID_CHARS ||
|
|
319
|
+
typeof acknowledgement.replyContent !== "string" ||
|
|
320
|
+
acknowledgement.replyContent.length === 0 ||
|
|
321
|
+
acknowledgement.replyContent.length > MAX_MAILBOX_MESSAGE_CHARS ||
|
|
322
|
+
acknowledgement.replyContent.trim() !== acknowledgement.replyContent ||
|
|
323
|
+
acknowledgedMessageIds.has(acknowledgement.messageId) ||
|
|
324
|
+
acknowledgementIds.has(acknowledgement.acknowledgementId)) {
|
|
325
|
+
throw new Error("Worker agent mailbox contains invalid reply acknowledgement state.");
|
|
326
|
+
}
|
|
327
|
+
const message = messagesById.get(acknowledgement.messageId);
|
|
328
|
+
if (!message ||
|
|
329
|
+
message.deliveredAt === undefined ||
|
|
330
|
+
message.expectReply !== true ||
|
|
331
|
+
message.repliedAt === undefined ||
|
|
332
|
+
message.replyReceipt?.replyMessageId !== acknowledgement.acknowledgementId ||
|
|
333
|
+
message.replyReceipt.contentDigest !== replyIntentDigest(acknowledgement.replyContent)) {
|
|
334
|
+
throw new Error("Worker agent mailbox reply acknowledgement does not reference a retained request.");
|
|
335
|
+
}
|
|
336
|
+
acknowledgedMessageIds.add(acknowledgement.messageId);
|
|
337
|
+
acknowledgementIds.add(acknowledgement.acknowledgementId);
|
|
338
|
+
return {
|
|
339
|
+
messageId: acknowledgement.messageId,
|
|
340
|
+
acknowledgementId: acknowledgement.acknowledgementId,
|
|
341
|
+
replyContent: acknowledgement.replyContent,
|
|
342
|
+
};
|
|
343
|
+
});
|
|
344
|
+
if (state.replayReceipts !== undefined && !Array.isArray(state.replayReceipts)) {
|
|
345
|
+
throw new Error("Worker agent mailbox contains invalid replay receipt state.");
|
|
346
|
+
}
|
|
347
|
+
const replayIdentities = new Set();
|
|
348
|
+
const replyMessageIds = new Set();
|
|
349
|
+
const replayReceipts = (state.replayReceipts ?? []).map((receipt) => {
|
|
350
|
+
if (!receipt ||
|
|
351
|
+
typeof receipt !== "object" ||
|
|
352
|
+
Array.isArray(receipt) ||
|
|
353
|
+
typeof receipt.messageId !== "string" ||
|
|
354
|
+
!receipt.messageId ||
|
|
355
|
+
receipt.messageId.length > MAX_MAILBOX_MESSAGE_ID_CHARS) {
|
|
356
|
+
throw new Error("Worker agent mailbox contains invalid replay receipt state.");
|
|
357
|
+
}
|
|
358
|
+
const replayIdentity = `${String(receipt.kind)}:${receipt.messageId}`;
|
|
359
|
+
if (replayIdentities.has(replayIdentity)) {
|
|
360
|
+
throw new Error("Worker agent mailbox contains duplicate replay receipt identities.");
|
|
361
|
+
}
|
|
362
|
+
replayIdentities.add(replayIdentity);
|
|
363
|
+
if (receipt.kind === "control") {
|
|
364
|
+
if (typeof receipt.intentDigest !== "string" ||
|
|
365
|
+
!CONTENT_DIGEST_PATTERN.test(receipt.intentDigest) ||
|
|
366
|
+
!Object.keys(receipt).every((field) => field === "kind" || field === "messageId" || field === "intentDigest" || field === "deliveredAt")) {
|
|
367
|
+
throw new Error("Worker agent mailbox contains invalid control replay receipt state.");
|
|
368
|
+
}
|
|
369
|
+
const deliveredAt = receipt.deliveredAt === undefined
|
|
370
|
+
? undefined
|
|
371
|
+
: requiredMailboxTimestamp(receipt.deliveredAt, "replay delivery timestamp");
|
|
372
|
+
return controlReplayReceipt(receipt.messageId, receipt.intentDigest, deliveredAt);
|
|
373
|
+
}
|
|
374
|
+
if (receipt.kind !== "reply" ||
|
|
375
|
+
!Object.keys(receipt).every((field) => field === "kind" ||
|
|
376
|
+
field === "messageId" ||
|
|
377
|
+
field === "replyMessageId" ||
|
|
378
|
+
field === "requestSenderId" ||
|
|
379
|
+
field === "contentDigest")) {
|
|
380
|
+
throw new Error("Worker agent mailbox contains invalid reply replay receipt state.");
|
|
381
|
+
}
|
|
382
|
+
const replyReceipt = parseReplyReceiptFields(receipt, "reply replay receipt state");
|
|
383
|
+
if (replyMessageIds.has(replyReceipt.replyMessageId)) {
|
|
384
|
+
throw new Error("Worker agent mailbox contains duplicate reply replay identities.");
|
|
385
|
+
}
|
|
386
|
+
replyMessageIds.add(replyReceipt.replyMessageId);
|
|
387
|
+
return {
|
|
388
|
+
kind: "reply",
|
|
389
|
+
messageId: receipt.messageId,
|
|
390
|
+
...replyReceipt,
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
if (replayReceipts.length > MAX_MAILBOX_REPLAY_RECEIPTS) {
|
|
394
|
+
throw new Error("Worker agent mailbox exceeds its replay receipt bound.");
|
|
395
|
+
}
|
|
396
|
+
if (replayReceipts.filter((receipt) => receipt.kind === "control").length > MAX_REPLAY_EVIDENCE_SLOTS) {
|
|
397
|
+
throw new Error("Worker agent mailbox exceeds its control replay receipt bound.");
|
|
398
|
+
}
|
|
399
|
+
if (replayReceipts.length +
|
|
400
|
+
messages.filter((message) => message.expectReply === true && message.repliedAt === undefined).length >
|
|
401
|
+
MAX_MAILBOX_REPLAY_RECEIPTS) {
|
|
402
|
+
throw new Error("Worker agent mailbox exceeds its projected replay evidence bound.");
|
|
403
|
+
}
|
|
404
|
+
for (const message of messages) {
|
|
405
|
+
const controlReceipt = controlReplayReceiptFor(replayReceipts, message.messageId);
|
|
406
|
+
if (controlReceipt && controlReceipt.deliveredAt !== message.deliveredAt) {
|
|
407
|
+
throw new Error("Worker agent mailbox control delivery conflicts with its durable replay receipt.");
|
|
408
|
+
}
|
|
409
|
+
if (message.replyReceipt) {
|
|
410
|
+
const replyReceipt = replyReplayReceiptFor(replayReceipts, message.messageId);
|
|
411
|
+
if (!replyReceipt ||
|
|
412
|
+
replyReceipt.replyMessageId !== message.replyReceipt.replyMessageId ||
|
|
413
|
+
replyReceipt.requestSenderId !== message.replyReceipt.requestSenderId ||
|
|
414
|
+
replyReceipt.contentDigest !== message.replyReceipt.contentDigest) {
|
|
415
|
+
throw new Error("Worker agent mailbox reply conflicts with its durable replay receipt.");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return { version: 1, parentSessionId, agentId, messages, replyAcknowledgements, replayReceipts };
|
|
93
420
|
}
|
|
94
421
|
function normalizeOptionalIdentity(value, label) {
|
|
95
422
|
if (value === undefined)
|
|
@@ -100,17 +427,135 @@ function normalizeOptionalIdentity(value, label) {
|
|
|
100
427
|
}
|
|
101
428
|
return normalized;
|
|
102
429
|
}
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
430
|
+
export function normalizeWorkerAgentDependencyTaskIds(value) {
|
|
431
|
+
return parseBoundedStringArray(value === undefined ? [] : value, {
|
|
432
|
+
maxEntries: MAX_ORCHESTRATION_COLLECTION_LENGTH,
|
|
433
|
+
maxLength: MAX_ORCHESTRATION_IDENTIFIER_LENGTH,
|
|
434
|
+
trim: true,
|
|
435
|
+
invalidMessage: "Worker dependency task ids must contain bounded, non-empty strings.",
|
|
436
|
+
duplicateMessage: "Worker dependency task ids must contain unique strings.",
|
|
437
|
+
createError: (message) => new TypeError(message),
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
function normalizeTaskMetadata(task) {
|
|
441
|
+
if (task === undefined)
|
|
442
|
+
return undefined;
|
|
443
|
+
if (task.kind === "agent_turn") {
|
|
444
|
+
const dependsOnTaskIds = normalizeWorkerAgentDependencyTaskIds(task.dependsOnTaskIds);
|
|
445
|
+
return {
|
|
446
|
+
kind: "agent_turn",
|
|
447
|
+
...(dependsOnTaskIds.length > 0 ? { dependsOnTaskIds } : {}),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
const sourceAttemptId = normalizeOptionalIdentity(task.sourceAttemptId, "terminal source attempt id");
|
|
451
|
+
if (!sourceAttemptId)
|
|
452
|
+
throw new TypeError("Worker terminal source attempt id is invalid.");
|
|
453
|
+
return { kind: "terminal_handoff", sourceAttemptId };
|
|
454
|
+
}
|
|
455
|
+
function sameTaskMetadata(left, right) {
|
|
456
|
+
if (left?.kind !== right?.kind)
|
|
457
|
+
return false;
|
|
458
|
+
if (left?.kind === "agent_turn" && right?.kind === "agent_turn") {
|
|
459
|
+
const leftDependencies = left.dependsOnTaskIds ?? [];
|
|
460
|
+
const rightDependencies = right.dependsOnTaskIds ?? [];
|
|
461
|
+
return (leftDependencies.length === rightDependencies.length &&
|
|
462
|
+
leftDependencies.every((dependencyId, index) => dependencyId === rightDependencies[index]));
|
|
463
|
+
}
|
|
464
|
+
if (left?.kind === "terminal_handoff" && right?.kind === "terminal_handoff") {
|
|
465
|
+
return left.sourceAttemptId === right.sourceAttemptId;
|
|
466
|
+
}
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
function hasExternalReplayAuthority(message) {
|
|
470
|
+
return message.replyToMessageId !== undefined || message.task?.kind === "terminal_handoff";
|
|
471
|
+
}
|
|
472
|
+
function isPendingMessage(message) {
|
|
473
|
+
return message.deliveredAt === undefined && message.failedAt === undefined;
|
|
474
|
+
}
|
|
475
|
+
function usesMandatoryRetainedReserve(message) {
|
|
476
|
+
return isPendingMessage(message) && hasExternalReplayAuthority(message);
|
|
477
|
+
}
|
|
478
|
+
function projectedReplayEvidenceSlots(state) {
|
|
479
|
+
return (state.replayReceipts.length +
|
|
480
|
+
state.messages.filter((message) => message.expectReply === true && message.repliedAt === undefined).length);
|
|
481
|
+
}
|
|
482
|
+
function normalizeReplyAcknowledgement(messageId, acknowledgementId) {
|
|
483
|
+
const normalizedMessageId = messageId.trim();
|
|
484
|
+
if (!normalizedMessageId || normalizedMessageId.length > MAX_MAILBOX_MESSAGE_ID_CHARS) {
|
|
485
|
+
throw new TypeError("A worker control message id is invalid.");
|
|
486
|
+
}
|
|
487
|
+
const normalizedAcknowledgementId = acknowledgementId.trim();
|
|
488
|
+
if (!normalizedAcknowledgementId || normalizedAcknowledgementId.length > MAX_MAILBOX_MESSAGE_ID_CHARS) {
|
|
489
|
+
throw new TypeError("A worker reply acknowledgement id is invalid.");
|
|
490
|
+
}
|
|
491
|
+
return { messageId: normalizedMessageId, acknowledgementId: normalizedAcknowledgementId };
|
|
492
|
+
}
|
|
493
|
+
function normalizeReplyContent(replyContent) {
|
|
494
|
+
const normalized = replyContent.trim();
|
|
495
|
+
if (!normalized || normalized.length > MAX_MAILBOX_MESSAGE_CHARS) {
|
|
496
|
+
throw new TypeError(`Worker reply content must contain from 1 through ${MAX_MAILBOX_MESSAGE_CHARS.toLocaleString("en-US")} characters.`);
|
|
497
|
+
}
|
|
498
|
+
return normalized;
|
|
499
|
+
}
|
|
500
|
+
function pruneDeliveredHistory(parentSessionId, agentId, messages, replyAcknowledgements, replayReceipts, ordinaryAdmission) {
|
|
501
|
+
const protectedReplyIds = new Set(replyAcknowledgements.map((acknowledgement) => acknowledgement.messageId));
|
|
502
|
+
const protectedMessages = messages.filter((message) => isPendingMessage(message) ||
|
|
503
|
+
(message.expectReply === true && message.repliedAt === undefined) ||
|
|
504
|
+
protectedReplyIds.has(message.messageId));
|
|
505
|
+
const protectedMandatory = protectedMessages.filter(usesMandatoryRetainedReserve);
|
|
506
|
+
const protectedOrdinary = protectedMessages.filter((message) => !usesMandatoryRetainedReserve(message));
|
|
507
|
+
if (protectedMandatory.length > MAX_MANDATORY_RETAINED_MESSAGES) {
|
|
508
|
+
throw new Error("Worker agent mailbox mandatory retained-message reserve is exhausted.");
|
|
509
|
+
}
|
|
510
|
+
if (protectedOrdinary.length > MAX_ORDINARY_RETAINED_MESSAGES) {
|
|
511
|
+
throw new Error("Worker agent mailbox ordinary retained-message capacity is exhausted.");
|
|
512
|
+
}
|
|
513
|
+
const retainedIds = new Set(protectedMessages.map((message) => message.messageId));
|
|
514
|
+
const deliveredCandidates = messages.filter((message) => (message.deliveredAt !== undefined || message.failedAt !== undefined) && !retainedIds.has(message.messageId));
|
|
515
|
+
const remainingOrdinarySlots = MAX_ORDINARY_RETAINED_MESSAGES - protectedOrdinary.length;
|
|
516
|
+
const recentDelivered = remainingOrdinarySlots > 0 ? deliveredCandidates.slice(-remainingOrdinarySlots) : [];
|
|
517
|
+
let retained = [...protectedMessages, ...recentDelivered].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
518
|
+
const retainedReplayReceipts = [...replayReceipts];
|
|
519
|
+
while (true) {
|
|
520
|
+
const state = {
|
|
521
|
+
version: 1,
|
|
522
|
+
parentSessionId,
|
|
523
|
+
agentId,
|
|
524
|
+
messages: retained,
|
|
525
|
+
replyAcknowledgements: [...replyAcknowledgements],
|
|
526
|
+
replayReceipts: retainedReplayReceipts,
|
|
527
|
+
};
|
|
528
|
+
const encodedBytes = ordinaryAdmission ? ordinaryAdmissionEncodedBytes(state) : encodedStateBytes(state);
|
|
529
|
+
const encodedLimit = ordinaryAdmission ? MAX_ORDINARY_MAILBOX_BYTES : MAX_MAILBOX_BYTES;
|
|
530
|
+
if (encodedBytes <= encodedLimit)
|
|
531
|
+
break;
|
|
532
|
+
const oldestCompletedIndex = retained.findIndex((message) => !retainedIds.has(message.messageId));
|
|
533
|
+
if (oldestCompletedIndex >= 0) {
|
|
534
|
+
retained = retained.filter((_, index) => index !== oldestCompletedIndex);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
return { messages: retained, replayReceipts: retainedReplayReceipts };
|
|
111
540
|
}
|
|
112
541
|
function encodedStateBytes(state) {
|
|
113
|
-
return Buffer.byteLength(JSON.stringify(state)
|
|
542
|
+
return Buffer.byteLength(`${JSON.stringify(state)}\n`, "utf-8");
|
|
543
|
+
}
|
|
544
|
+
function ordinaryAdmissionEncodedBytes(state) {
|
|
545
|
+
const projectedDeliveryIds = new Set(state.messages
|
|
546
|
+
.filter((message) => message.deliveredAt === undefined &&
|
|
547
|
+
message.failedAt === undefined &&
|
|
548
|
+
!hasExternalReplayAuthority(message))
|
|
549
|
+
.map((message) => message.messageId));
|
|
550
|
+
return encodedStateBytes({
|
|
551
|
+
...state,
|
|
552
|
+
messages: state.messages.map((message) => projectedDeliveryIds.has(message.messageId)
|
|
553
|
+
? { ...message, deliveredAt: MAX_MAILBOX_TRANSITION_TIMESTAMP }
|
|
554
|
+
: message),
|
|
555
|
+
replayReceipts: state.replayReceipts.map((receipt) => receipt.kind === "control" && receipt.deliveredAt === undefined && projectedDeliveryIds.has(receipt.messageId)
|
|
556
|
+
? { ...receipt, deliveredAt: MAX_MAILBOX_TRANSITION_TIMESTAMP }
|
|
557
|
+
: receipt),
|
|
558
|
+
});
|
|
114
559
|
}
|
|
115
560
|
/**
|
|
116
561
|
* Bounded durable inbox for a single logical worker agent.
|
|
@@ -127,6 +572,13 @@ export class WorkerAgentMailbox {
|
|
|
127
572
|
this.file = workerAgentMailboxFile(options.agentDir, this.parentSessionId, mailboxDigest(this.parentSessionId, this.agentId));
|
|
128
573
|
}
|
|
129
574
|
enqueue(input) {
|
|
575
|
+
const accepted = this.enqueueWithReceipt(input);
|
|
576
|
+
if (accepted.status !== "retained")
|
|
577
|
+
throw new Error("Unkeyed worker control unexpectedly resolved as a replay.");
|
|
578
|
+
return accepted.message;
|
|
579
|
+
}
|
|
580
|
+
/** Enqueue with exact creation evidence for an idempotent surrounding acceptance flow. */
|
|
581
|
+
enqueueWithReceipt(input) {
|
|
130
582
|
const content = input.content.trim();
|
|
131
583
|
if (!content)
|
|
132
584
|
throw new TypeError("A worker control message is required.");
|
|
@@ -136,21 +588,15 @@ export class WorkerAgentMailbox {
|
|
|
136
588
|
const senderAgentId = normalizeOptionalIdentity(input.senderAgentId, "sender agent id");
|
|
137
589
|
const threadId = normalizeOptionalIdentity(input.threadId, "thread id");
|
|
138
590
|
const replyToMessageId = normalizeOptionalIdentity(input.replyToMessageId, "reply message id");
|
|
591
|
+
const task = normalizeTaskMetadata(input.task);
|
|
139
592
|
const idempotencyKey = input.idempotencyKey?.trim();
|
|
140
|
-
if (input.idempotencyKey !== undefined &&
|
|
593
|
+
if (input.idempotencyKey !== undefined &&
|
|
594
|
+
(!idempotencyKey || idempotencyKey.length > MAX_MAILBOX_IDEMPOTENCY_KEY_CHARS)) {
|
|
141
595
|
throw new TypeError("Worker control idempotency key is invalid.");
|
|
142
596
|
}
|
|
143
597
|
const message = {
|
|
144
598
|
messageId: idempotencyKey
|
|
145
|
-
?
|
|
146
|
-
.update("pi-worker-agent-message-v1")
|
|
147
|
-
.update("\0")
|
|
148
|
-
.update(this.parentSessionId)
|
|
149
|
-
.update("\0")
|
|
150
|
-
.update(this.agentId)
|
|
151
|
-
.update("\0")
|
|
152
|
-
.update(idempotencyKey)
|
|
153
|
-
.digest("hex")}`
|
|
599
|
+
? workerAgentMessageId(this.parentSessionId, idempotencyKey)
|
|
154
600
|
: `worker-message-${randomUUID()}`,
|
|
155
601
|
kind: input.kind,
|
|
156
602
|
content,
|
|
@@ -158,39 +604,93 @@ export class WorkerAgentMailbox {
|
|
|
158
604
|
...(threadId ? { threadId } : {}),
|
|
159
605
|
...(replyToMessageId ? { replyToMessageId } : {}),
|
|
160
606
|
...(input.expectReply === true ? { expectReply: true } : {}),
|
|
607
|
+
...(task ? { task } : {}),
|
|
161
608
|
createdAt: new Date().toISOString(),
|
|
162
609
|
};
|
|
610
|
+
const intentDigest = idempotencyKey ? controlIntentDigest(message) : undefined;
|
|
611
|
+
const externallyReplayOwned = hasExternalReplayAuthority(message);
|
|
163
612
|
let queued = message;
|
|
164
613
|
let created = false;
|
|
614
|
+
let completedReplay = false;
|
|
165
615
|
this.update((state) => {
|
|
166
616
|
const existing = state.messages.find((candidate) => candidate.messageId === message.messageId);
|
|
617
|
+
const replayReceipt = controlReplayReceiptFor(state.replayReceipts, message.messageId);
|
|
618
|
+
const addsControlReceipt = intentDigest !== undefined && !externallyReplayOwned && replayReceipt === undefined;
|
|
619
|
+
if (addsControlReceipt &&
|
|
620
|
+
state.replayReceipts.filter((receipt) => receipt.kind === "control").length >= MAX_REPLAY_EVIDENCE_SLOTS) {
|
|
621
|
+
throw new Error(`Worker agent mailbox control replay receipt capacity reached its ${MAX_REPLAY_EVIDENCE_SLOTS} entry limit.`);
|
|
622
|
+
}
|
|
167
623
|
if (existing) {
|
|
168
|
-
|
|
169
|
-
|
|
624
|
+
// `kind` is the delivery-mode projection at first acceptance. It is deliberately not
|
|
625
|
+
// part of task intent and is never rewritten when a retry observes a different live
|
|
626
|
+
// activity state. The durable `task` identity owns whether an idle turn is required.
|
|
627
|
+
const sameIntent = existing.content === message.content &&
|
|
170
628
|
existing.senderAgentId === message.senderAgentId &&
|
|
171
629
|
existing.threadId === message.threadId &&
|
|
172
630
|
existing.replyToMessageId === message.replyToMessageId &&
|
|
173
|
-
existing.expectReply === message.expectReply
|
|
631
|
+
existing.expectReply === message.expectReply &&
|
|
632
|
+
sameTaskMetadata(existing.task, message.task);
|
|
174
633
|
if (!sameIntent)
|
|
175
634
|
throw new Error("Worker control idempotency identity conflicts with an existing message.");
|
|
635
|
+
if (intentDigest && replayReceipt && replayReceipt.intentDigest !== intentDigest) {
|
|
636
|
+
throw new Error("Worker control idempotency identity conflicts with its durable replay receipt.");
|
|
637
|
+
}
|
|
176
638
|
queued = existing;
|
|
639
|
+
return intentDigest && !externallyReplayOwned && !replayReceipt
|
|
640
|
+
? {
|
|
641
|
+
...state,
|
|
642
|
+
replayReceipts: [
|
|
643
|
+
...state.replayReceipts,
|
|
644
|
+
controlReplayReceipt(message.messageId, intentDigest, existing.deliveredAt),
|
|
645
|
+
],
|
|
646
|
+
}
|
|
647
|
+
: state;
|
|
648
|
+
}
|
|
649
|
+
if (replayReceipt) {
|
|
650
|
+
if (!intentDigest || replayReceipt.intentDigest !== intentDigest) {
|
|
651
|
+
throw new Error("Worker control idempotency identity conflicts with its durable replay receipt.");
|
|
652
|
+
}
|
|
653
|
+
completedReplay = true;
|
|
177
654
|
return state;
|
|
178
655
|
}
|
|
179
|
-
|
|
180
|
-
|
|
656
|
+
const pending = state.messages.filter((candidate) => candidate.deliveredAt === undefined && candidate.failedAt === undefined);
|
|
657
|
+
if (pending.filter((candidate) => hasExternalReplayAuthority(candidate) === externallyReplayOwned).length >=
|
|
658
|
+
MAX_MAILBOX_MESSAGES) {
|
|
659
|
+
throw new Error(externallyReplayOwned
|
|
660
|
+
? `Worker agent mailbox reached its ${MAX_MAILBOX_MESSAGES} mandatory message limit.`
|
|
661
|
+
: `Worker agent mailbox reached its ${MAX_MAILBOX_MESSAGES} message limit.`);
|
|
181
662
|
}
|
|
182
663
|
created = true;
|
|
183
|
-
return {
|
|
184
|
-
|
|
664
|
+
return {
|
|
665
|
+
...state,
|
|
666
|
+
messages: [...state.messages, message],
|
|
667
|
+
...(intentDigest && !externallyReplayOwned
|
|
668
|
+
? {
|
|
669
|
+
replayReceipts: [
|
|
670
|
+
...state.replayReceipts,
|
|
671
|
+
controlReplayReceipt(message.messageId, intentDigest),
|
|
672
|
+
],
|
|
673
|
+
}
|
|
674
|
+
: {}),
|
|
675
|
+
};
|
|
676
|
+
}, !externallyReplayOwned, true);
|
|
185
677
|
if (created)
|
|
186
678
|
this.notify();
|
|
187
|
-
|
|
679
|
+
if (completedReplay)
|
|
680
|
+
return { status: "completed_replay", messageId: message.messageId, created: false };
|
|
681
|
+
return { status: "retained", messageId: queued.messageId, message: structuredClone(queued), created };
|
|
188
682
|
}
|
|
189
683
|
pending(kind) {
|
|
190
684
|
return this.read()
|
|
191
|
-
.messages.filter((message) => message.deliveredAt === undefined &&
|
|
685
|
+
.messages.filter((message) => message.deliveredAt === undefined &&
|
|
686
|
+
message.failedAt === undefined &&
|
|
687
|
+
(kind === undefined || message.kind === kind))
|
|
192
688
|
.map((message) => structuredClone(message));
|
|
193
689
|
}
|
|
690
|
+
/** Oldest-first executable intents that have not reached the durable transcript boundary. */
|
|
691
|
+
pendingTaskBearing() {
|
|
692
|
+
return this.pending().filter((message) => message.task !== undefined);
|
|
693
|
+
}
|
|
194
694
|
acknowledgeDelivered(messageId) {
|
|
195
695
|
this.markTimestamp(messageId, "deliveredAt");
|
|
196
696
|
}
|
|
@@ -206,8 +706,154 @@ export class WorkerAgentMailbox {
|
|
|
206
706
|
const message = this.read().messages.find((candidate) => candidate.messageId === normalized);
|
|
207
707
|
return message ? structuredClone(message) : undefined;
|
|
208
708
|
}
|
|
209
|
-
|
|
210
|
-
|
|
709
|
+
hasControlReplayReceipt(messageId) {
|
|
710
|
+
const normalized = messageId.trim();
|
|
711
|
+
if (!normalized)
|
|
712
|
+
throw new TypeError("A worker control message id is required.");
|
|
713
|
+
return controlReplayReceiptFor(this.read().replayReceipts, normalized) !== undefined;
|
|
714
|
+
}
|
|
715
|
+
hasDeliveredControlReceipt(messageId) {
|
|
716
|
+
const normalized = messageId.trim();
|
|
717
|
+
if (!normalized)
|
|
718
|
+
throw new TypeError("A worker control message id is required.");
|
|
719
|
+
const state = this.read();
|
|
720
|
+
const message = state.messages.find((candidate) => candidate.messageId === normalized);
|
|
721
|
+
return (message?.deliveredAt !== undefined ||
|
|
722
|
+
controlReplayReceiptFor(state.replayReceipts, normalized)?.deliveredAt !== undefined);
|
|
723
|
+
}
|
|
724
|
+
resolveCompletedReply(messageId, content) {
|
|
725
|
+
const normalized = messageId.trim();
|
|
726
|
+
if (!normalized)
|
|
727
|
+
throw new TypeError("A worker control message id is required.");
|
|
728
|
+
const normalizedContent = normalizeReplyContent(content);
|
|
729
|
+
const state = this.read();
|
|
730
|
+
const request = state.messages.find((candidate) => candidate.messageId === normalized);
|
|
731
|
+
const receipt = request?.replyReceipt ?? replyReplayReceiptFor(state.replayReceipts, normalized);
|
|
732
|
+
if (!receipt)
|
|
733
|
+
return undefined;
|
|
734
|
+
if (receipt.contentDigest !== replyIntentDigest(normalizedContent)) {
|
|
735
|
+
throw new Error("Worker reply identity conflicts with its durable source receipt.");
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
replyMessageId: receipt.replyMessageId,
|
|
739
|
+
requestSenderId: receipt.requestSenderId,
|
|
740
|
+
contentDigest: receipt.contentDigest,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
getReplyAcknowledgementId(messageId) {
|
|
744
|
+
const normalized = messageId.trim();
|
|
745
|
+
if (!normalized)
|
|
746
|
+
throw new TypeError("A worker control message id is required.");
|
|
747
|
+
return this.read().replyAcknowledgements.find((acknowledgement) => acknowledgement.messageId === normalized)
|
|
748
|
+
?.acknowledgementId;
|
|
749
|
+
}
|
|
750
|
+
listReplyAcknowledgements() {
|
|
751
|
+
return this.read().replyAcknowledgements.map((acknowledgement) => ({ ...acknowledgement }));
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Mark one request replied while retaining the exact target receipt until transcript consumption.
|
|
755
|
+
* The acknowledgement id is the durable target reply id, so a retry adopts a crash-left marker.
|
|
756
|
+
*/
|
|
757
|
+
beginReplyAcknowledgement(messageId, acknowledgementId, replyContent) {
|
|
758
|
+
const normalized = normalizeReplyAcknowledgement(messageId, acknowledgementId);
|
|
759
|
+
const normalizedContent = normalizeReplyContent(replyContent);
|
|
760
|
+
const contentDigest = replyIntentDigest(normalizedContent);
|
|
761
|
+
let acquired = false;
|
|
762
|
+
let changed = false;
|
|
763
|
+
this.update((state) => {
|
|
764
|
+
const request = state.messages.find((message) => message.messageId === normalized.messageId);
|
|
765
|
+
if (!request || request.deliveredAt === undefined || request.expectReply !== true)
|
|
766
|
+
return state;
|
|
767
|
+
if (!request.senderAgentId) {
|
|
768
|
+
throw new Error("Worker reply request has no routable requester.");
|
|
769
|
+
}
|
|
770
|
+
const receipt = {
|
|
771
|
+
replyMessageId: normalized.acknowledgementId,
|
|
772
|
+
requestSenderId: request.senderAgentId,
|
|
773
|
+
contentDigest,
|
|
774
|
+
};
|
|
775
|
+
const durableReceipt = replyReplayReceiptFor(state.replayReceipts, normalized.messageId);
|
|
776
|
+
if (durableReceipt &&
|
|
777
|
+
(durableReceipt.replyMessageId !== receipt.replyMessageId ||
|
|
778
|
+
durableReceipt.requestSenderId !== receipt.requestSenderId ||
|
|
779
|
+
durableReceipt.contentDigest !== receipt.contentDigest)) {
|
|
780
|
+
throw new Error("Worker reply identity conflicts with its durable replay receipt.");
|
|
781
|
+
}
|
|
782
|
+
const existing = state.replyAcknowledgements.find((acknowledgement) => acknowledgement.messageId === normalized.messageId ||
|
|
783
|
+
acknowledgement.acknowledgementId === normalized.acknowledgementId);
|
|
784
|
+
if (existing) {
|
|
785
|
+
if (existing.messageId !== normalized.messageId ||
|
|
786
|
+
existing.acknowledgementId !== normalized.acknowledgementId) {
|
|
787
|
+
throw new Error("Worker reply acknowledgement identity conflicts with an active transaction.");
|
|
788
|
+
}
|
|
789
|
+
if (request?.replyReceipt?.replyMessageId !== receipt.replyMessageId ||
|
|
790
|
+
request.replyReceipt.requestSenderId !== receipt.requestSenderId ||
|
|
791
|
+
request.replyReceipt.contentDigest !== receipt.contentDigest ||
|
|
792
|
+
existing.replyContent !== normalizedContent) {
|
|
793
|
+
throw new Error("Worker reply content conflicts with its active acknowledgement transaction.");
|
|
794
|
+
}
|
|
795
|
+
acquired = true;
|
|
796
|
+
return state;
|
|
797
|
+
}
|
|
798
|
+
if (request.repliedAt !== undefined)
|
|
799
|
+
return state;
|
|
800
|
+
acquired = true;
|
|
801
|
+
changed = true;
|
|
802
|
+
return {
|
|
803
|
+
...state,
|
|
804
|
+
messages: state.messages.map((message) => message.messageId === normalized.messageId
|
|
805
|
+
? {
|
|
806
|
+
...message,
|
|
807
|
+
repliedAt: transitionTimestamp(message.deliveredAt ?? message.createdAt),
|
|
808
|
+
replyReceipt: receipt,
|
|
809
|
+
}
|
|
810
|
+
: message),
|
|
811
|
+
replyAcknowledgements: [...state.replyAcknowledgements, { ...normalized, replyContent: normalizedContent }],
|
|
812
|
+
replayReceipts: durableReceipt
|
|
813
|
+
? state.replayReceipts
|
|
814
|
+
: [...state.replayReceipts, { kind: "reply", messageId: normalized.messageId, ...receipt }],
|
|
815
|
+
};
|
|
816
|
+
});
|
|
817
|
+
if (changed)
|
|
818
|
+
this.notify();
|
|
819
|
+
return acquired;
|
|
820
|
+
}
|
|
821
|
+
/** Dead-letter only an ordinary executable turn with no reply or terminal-delivery obligation. */
|
|
822
|
+
deadLetterOrdinaryTask(messageId, reason) {
|
|
823
|
+
const normalized = messageId.trim();
|
|
824
|
+
const failureReason = reason.trim();
|
|
825
|
+
if (!normalized)
|
|
826
|
+
throw new TypeError("A worker control message id is required.");
|
|
827
|
+
if (!failureReason || failureReason.length > MAX_MAILBOX_IDENTITY_CHARS) {
|
|
828
|
+
throw new TypeError("A worker task failure reason is invalid.");
|
|
829
|
+
}
|
|
830
|
+
let changed = false;
|
|
831
|
+
this.update((state) => ({
|
|
832
|
+
...state,
|
|
833
|
+
messages: state.messages.map((message) => {
|
|
834
|
+
if (message.messageId !== normalized ||
|
|
835
|
+
message.task?.kind !== "agent_turn" ||
|
|
836
|
+
message.replyToMessageId !== undefined ||
|
|
837
|
+
message.expectReply === true ||
|
|
838
|
+
message.deliveredAt !== undefined ||
|
|
839
|
+
message.failedAt !== undefined) {
|
|
840
|
+
return message;
|
|
841
|
+
}
|
|
842
|
+
changed = true;
|
|
843
|
+
return { ...message, failedAt: transitionTimestamp(message.createdAt), failureReason };
|
|
844
|
+
}),
|
|
845
|
+
}), true);
|
|
846
|
+
if (changed)
|
|
847
|
+
this.notify();
|
|
848
|
+
return changed;
|
|
849
|
+
}
|
|
850
|
+
/** Commit one exact reply acknowledgement and release its protected history slot. */
|
|
851
|
+
commitReplyAcknowledgement(messageId, acknowledgementId) {
|
|
852
|
+
return this.finishReplyAcknowledgement(messageId, acknowledgementId, false);
|
|
853
|
+
}
|
|
854
|
+
/** Roll back one exact reply acknowledgement without clearing a later or unrelated reply. */
|
|
855
|
+
rollbackReplyAcknowledgement(messageId, acknowledgementId) {
|
|
856
|
+
return this.finishReplyAcknowledgement(messageId, acknowledgementId, true);
|
|
211
857
|
}
|
|
212
858
|
markTimestamp(messageId, field) {
|
|
213
859
|
const normalized = messageId.trim();
|
|
@@ -215,13 +861,56 @@ export class WorkerAgentMailbox {
|
|
|
215
861
|
throw new TypeError("A worker control message id is required.");
|
|
216
862
|
let changed = false;
|
|
217
863
|
this.update((state) => {
|
|
864
|
+
let deliveredAt;
|
|
218
865
|
const messages = state.messages.map((message) => {
|
|
219
|
-
if (message.messageId !== normalized || message[field] !== undefined)
|
|
866
|
+
if (message.messageId !== normalized || message[field] !== undefined || message.failedAt !== undefined) {
|
|
220
867
|
return message;
|
|
868
|
+
}
|
|
221
869
|
changed = true;
|
|
222
|
-
|
|
870
|
+
deliveredAt = transitionTimestamp(message.createdAt);
|
|
871
|
+
return { ...message, [field]: deliveredAt };
|
|
223
872
|
});
|
|
224
|
-
|
|
873
|
+
if (!changed || !deliveredAt)
|
|
874
|
+
return state;
|
|
875
|
+
return {
|
|
876
|
+
...state,
|
|
877
|
+
messages,
|
|
878
|
+
replayReceipts: state.replayReceipts.map((receipt) => receipt.kind === "control" && receipt.messageId === normalized ? { ...receipt, deliveredAt } : receipt),
|
|
879
|
+
};
|
|
880
|
+
});
|
|
881
|
+
if (changed)
|
|
882
|
+
this.notify();
|
|
883
|
+
return changed;
|
|
884
|
+
}
|
|
885
|
+
finishReplyAcknowledgement(messageId, acknowledgementId, rollback) {
|
|
886
|
+
const normalized = normalizeReplyAcknowledgement(messageId, acknowledgementId);
|
|
887
|
+
let changed = false;
|
|
888
|
+
this.update((state) => {
|
|
889
|
+
const acknowledgement = state.replyAcknowledgements.find((candidate) => candidate.messageId === normalized.messageId &&
|
|
890
|
+
candidate.acknowledgementId === normalized.acknowledgementId);
|
|
891
|
+
if (!acknowledgement)
|
|
892
|
+
return state;
|
|
893
|
+
changed = true;
|
|
894
|
+
const messages = rollback
|
|
895
|
+
? state.messages.map((message) => {
|
|
896
|
+
if (message.messageId !== normalized.messageId || message.repliedAt === undefined)
|
|
897
|
+
return message;
|
|
898
|
+
const restored = { ...message };
|
|
899
|
+
delete restored.repliedAt;
|
|
900
|
+
delete restored.replyReceipt;
|
|
901
|
+
return restored;
|
|
902
|
+
})
|
|
903
|
+
: state.messages;
|
|
904
|
+
return {
|
|
905
|
+
...state,
|
|
906
|
+
messages,
|
|
907
|
+
replyAcknowledgements: state.replyAcknowledgements.filter((candidate) => candidate !== acknowledgement),
|
|
908
|
+
...(rollback
|
|
909
|
+
? {
|
|
910
|
+
replayReceipts: state.replayReceipts.filter((receipt) => receipt.kind !== "reply" || receipt.messageId !== normalized.messageId),
|
|
911
|
+
}
|
|
912
|
+
: {}),
|
|
913
|
+
};
|
|
225
914
|
});
|
|
226
915
|
if (changed)
|
|
227
916
|
this.notify();
|
|
@@ -232,24 +921,65 @@ export class WorkerAgentMailbox {
|
|
|
232
921
|
return () => this.listeners.delete(listener);
|
|
233
922
|
}
|
|
234
923
|
notify() {
|
|
235
|
-
for (const listener of this.listeners)
|
|
236
|
-
|
|
924
|
+
for (const listener of this.listeners) {
|
|
925
|
+
try {
|
|
926
|
+
listener();
|
|
927
|
+
}
|
|
928
|
+
catch {
|
|
929
|
+
// Mailbox subscribers are advisory; durable mutation results remain authoritative.
|
|
930
|
+
}
|
|
931
|
+
}
|
|
237
932
|
}
|
|
238
933
|
read() {
|
|
239
934
|
if (!existsSync(this.file)) {
|
|
240
|
-
return {
|
|
935
|
+
return {
|
|
936
|
+
version: 1,
|
|
937
|
+
parentSessionId: this.parentSessionId,
|
|
938
|
+
agentId: this.agentId,
|
|
939
|
+
messages: [],
|
|
940
|
+
replyAcknowledgements: [],
|
|
941
|
+
replayReceipts: [],
|
|
942
|
+
};
|
|
241
943
|
}
|
|
242
944
|
return parseState(readBoundedTextFileSync(this.file, MAX_MAILBOX_BYTES, "Worker agent mailbox durable size bound"), this.parentSessionId, this.agentId);
|
|
243
945
|
}
|
|
244
|
-
update(mutator) {
|
|
946
|
+
update(mutator, ordinaryAdmission = false, replayEvidenceAdmission = false) {
|
|
245
947
|
withFileLockSync(this.file, () => {
|
|
246
948
|
const state = this.read();
|
|
949
|
+
const previousOrdinaryBytes = ordinaryAdmissionEncodedBytes(state);
|
|
950
|
+
const previousReplayEvidenceSlots = projectedReplayEvidenceSlots(state);
|
|
247
951
|
const mutated = mutator(state);
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
952
|
+
const retained = pruneDeliveredHistory(this.parentSessionId, this.agentId, mutated.messages, mutated.replyAcknowledgements, mutated.replayReceipts, ordinaryAdmission);
|
|
953
|
+
const next = {
|
|
954
|
+
...mutated,
|
|
955
|
+
messages: retained.messages,
|
|
956
|
+
replayReceipts: retained.replayReceipts,
|
|
957
|
+
};
|
|
958
|
+
const addedReplayReceipt = mutated.replayReceipts.length > state.replayReceipts.length;
|
|
959
|
+
const bytesWithoutAddedReceipt = addedReplayReceipt
|
|
960
|
+
? encodedStateBytes({ ...next, replayReceipts: next.replayReceipts.slice(0, -1) })
|
|
961
|
+
: undefined;
|
|
962
|
+
const changed = JSON.stringify(next) !== JSON.stringify(state);
|
|
963
|
+
const nextOrdinaryBytes = ordinaryAdmissionEncodedBytes(next);
|
|
964
|
+
if (ordinaryAdmission &&
|
|
965
|
+
changed &&
|
|
966
|
+
nextOrdinaryBytes > MAX_ORDINARY_MAILBOX_BYTES &&
|
|
967
|
+
nextOrdinaryBytes > previousOrdinaryBytes) {
|
|
968
|
+
throw new Error(addedReplayReceipt
|
|
969
|
+
? "Worker agent mailbox replay receipt storage exhausted its mandatory control byte reserve."
|
|
970
|
+
: "Worker agent mailbox passive control storage exhausted its mandatory control byte reserve.");
|
|
971
|
+
}
|
|
972
|
+
const nextReplayEvidenceSlots = projectedReplayEvidenceSlots(next);
|
|
973
|
+
if (nextReplayEvidenceSlots > MAX_MAILBOX_REPLAY_RECEIPTS) {
|
|
974
|
+
throw new Error("Worker agent mailbox exceeds its projected replay evidence bound.");
|
|
975
|
+
}
|
|
976
|
+
if (replayEvidenceAdmission &&
|
|
977
|
+
nextReplayEvidenceSlots > MAX_REPLAY_EVIDENCE_SLOTS &&
|
|
978
|
+
nextReplayEvidenceSlots > previousReplayEvidenceSlots) {
|
|
979
|
+
throw new Error(`Worker agent mailbox replay evidence capacity reached its ${MAX_REPLAY_EVIDENCE_SLOTS} slot limit.`);
|
|
251
980
|
}
|
|
252
|
-
|
|
981
|
+
assertWorkerAgentMailboxBounds(next.messages.length, next.replayReceipts.length, state.replayReceipts.length, encodedStateBytes(next), bytesWithoutAddedReceipt);
|
|
982
|
+
if (changed)
|
|
253
983
|
writeFileAtomicSync(this.file, `${JSON.stringify(next)}\n`, { mode: 0o600 });
|
|
254
984
|
});
|
|
255
985
|
}
|