@sema-agent/core 7.2.0 → 7.3.0
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 +29 -0
- package/dist/agents/cross-session-envelope.d.ts +7 -0
- package/dist/agents/cross-session-envelope.js +4 -0
- package/dist/agents/list-agents-tool.d.ts +55 -0
- package/dist/agents/list-agents-tool.js +94 -0
- package/dist/agents/peer-admission.d.ts +17 -1
- package/dist/agents/peer-admission.js +19 -2
- package/dist/agents/peer-directory.d.ts +208 -0
- package/dist/agents/peer-directory.js +272 -0
- package/dist/agents/peer-session-drain.d.ts +159 -0
- package/dist/agents/peer-session-drain.js +245 -0
- package/dist/agents/send-message-tool.d.ts +31 -0
- package/dist/agents/send-message-tool.js +145 -4
- package/dist/agents/subagent-steps.d.ts +11 -0
- package/dist/agents/subagent-steps.js +27 -4
- package/dist/core/auto-mode-arming.d.ts +11 -0
- package/dist/core/auto-mode-arming.js +7 -1
- package/dist/core/auto-mode-prompt.d.ts +5 -0
- package/dist/core/auto-mode-prompt.js +2 -1
- package/dist/core/auto-mode-rebuild.d.ts +2 -1
- package/dist/core/auto-mode-rebuild.js +2 -0
- package/dist/core/checkpoint-store.d.ts +14 -0
- package/dist/core/checkpoint-store.js +4 -3
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +6 -0
- package/dist/core/mailbox-store.d.ts +89 -2
- package/dist/core/mailbox-store.js +77 -2
- package/dist/core/permission-rule-model.d.ts +9 -0
- package/dist/core/permission-rule-model.js +4 -1
- package/dist/core/runner/prepare-task.d.ts +20 -0
- package/dist/core/runner/prepare-task.js +137 -37
- package/dist/core/runner/runtask.js +3 -2
- package/dist/core/runner/tool-output-projection.js +1 -0
- package/dist/core/store-contracts/mailbox-store-contract.d.ts +23 -0
- package/dist/core/store-contracts/mailbox-store-contract.js +157 -1
- package/dist/core/task-notification.d.ts +38 -9
- package/dist/core/task-notification.js +8 -2
- package/dist/core/types.d.ts +132 -21
- package/dist/core/wiring-manifest.d.ts +21 -0
- package/dist/core/wiring-manifest.js +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +9 -3
- package/dist/stores/cc/mailbox-store.d.ts +1 -1
- package/dist/stores/cc/mailbox-store.js +13 -0
- package/dist/stores/file/adoption/marker.d.ts +1 -1
- package/dist/stores/file/mailbox-store.d.ts +57 -0
- package/dist/stores/file/mailbox-store.js +369 -18
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +109 -1
|
@@ -83,6 +83,8 @@ import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.j
|
|
|
83
83
|
import { createTaskOutputTool, createTaskStopTool, defaultTaskRegistry } from "../task-registry.js";
|
|
84
84
|
import { createMonitorTool } from "../../tools/monitor.js";
|
|
85
85
|
import { createWorktreeTools } from "../../tools/worktree.js";
|
|
86
|
+
import { announcePeerLaneMount, bindPeerLaneDrain, listAgentsMountable, mountListAgents, peerLaneSendMessageSeats } from "../../agents/peer-session-drain.js";
|
|
87
|
+
import { CROSS_SESSION_CLASSIFIER_RULE } from "../../agents/cross-session-envelope.js";
|
|
86
88
|
import { applyCompactionToReadFileState, isReadDedupStubResult, FULL_SHELL_CONTRACT_ID, HAND_TOOL_EFFECTS } from "../../tools/fs/index.js";
|
|
87
89
|
import { decodeTextBytes } from "../../tools/fs/encoding.js";
|
|
88
90
|
import { ASK_USER_QUESTION_TOOL_NAME, createAskUserQuestionTool, classifyQuestionOutcome, isLiveQuestionFace, validateAskQuestions, } from "../ask-question.js";
|
|
@@ -198,6 +200,20 @@ function announceToolModelGate(onNotice, modelId, gate) {
|
|
|
198
200
|
}
|
|
199
201
|
}
|
|
200
202
|
}
|
|
203
|
+
export const TOOL_MODEL_GATE_RESTORE_SENTENCE = "restore via TaskSpec.restoreGatedTools, SEMA_TOOL_MODEL_GATE=off, or RunnerDeps.toolModelGate: false";
|
|
204
|
+
function toolModelGateManifest(gate) {
|
|
205
|
+
let firstClass;
|
|
206
|
+
const removed = [];
|
|
207
|
+
for (const [gateClass, names] of gate.removedByClass) {
|
|
208
|
+
if (names.length === 0)
|
|
209
|
+
continue;
|
|
210
|
+
firstClass ??= gateClass;
|
|
211
|
+
for (const n of names)
|
|
212
|
+
if (!removed.includes(n))
|
|
213
|
+
removed.push(n);
|
|
214
|
+
}
|
|
215
|
+
return firstClass === undefined ? undefined : { class: firstClass, removed, restore: TOOL_MODEL_GATE_RESTORE_SENTENCE };
|
|
216
|
+
}
|
|
201
217
|
function announceDeclaredMcpContentClasses(args) {
|
|
202
218
|
for (const entry of args.entries ?? []) {
|
|
203
219
|
const declaredClass = entry.contentOrigin;
|
|
@@ -1161,14 +1177,101 @@ function refuseRequireExistingWithoutSession(spec) {
|
|
|
1161
1177
|
throw e;
|
|
1162
1178
|
}
|
|
1163
1179
|
}
|
|
1180
|
+
function screenAutoModeCap(caps, onError, sessionId) {
|
|
1181
|
+
if (caps === undefined)
|
|
1182
|
+
return { caps, faulted: false };
|
|
1183
|
+
if (typeof caps !== "object" || caps === null || Array.isArray(caps)) {
|
|
1184
|
+
onError?.(new Error(`runtimeCapsResolver returned a non-record value (${JSON.stringify(caps)}) — read as a resolver fault: every per-principal capability is DENIED for this run (auto mode is not armed; asks flow the original chain). Return a RuntimeCaps object or undefined.`), { phase: "config", sessionId });
|
|
1185
|
+
return { caps: { allowWorkflows: false, allowFork: false, autoMode: false }, faulted: true };
|
|
1186
|
+
}
|
|
1187
|
+
const record = caps;
|
|
1188
|
+
if (record.autoMode === undefined || typeof record.autoMode === "boolean")
|
|
1189
|
+
return { caps: record, faulted: false };
|
|
1190
|
+
onError?.(new Error(`runtimeCapsResolver returned a non-boolean autoMode (${JSON.stringify(record.autoMode)}) — read as a resolver fault: auto mode is DENIED for this run (the classifier is not armed; asks flow the original chain). Return true, false, or omit the key.`), { phase: "config", sessionId });
|
|
1191
|
+
return { caps: { ...record, autoMode: false }, faulted: true };
|
|
1192
|
+
}
|
|
1193
|
+
function mintPersistedArming(am, laneRule, classifierSystemPrompt, onError, sessionId) {
|
|
1194
|
+
const arming = autoModeArmingRecipeOf(laneRule ? { ...am, crossSessionMessagesRule: true } : am, {
|
|
1195
|
+
promptDigest: `apv1:${createHash("sha256").update(classifierSystemPrompt).digest("hex")}`,
|
|
1196
|
+
});
|
|
1197
|
+
if (arming === undefined) {
|
|
1198
|
+
onError?.(new Error("RunnerDeps.autoMode.persistArming is set but the auto-mode face did not canonicalize into an arming recipe " +
|
|
1199
|
+
"(a rule list carrying a non-string, or a non-finite/out-of-range timeoutMs / failureThreshold / window bound) — " +
|
|
1200
|
+
"NO arming recipe is recorded on this run's parked constraint chain, and a cross-process redemption keeps the " +
|
|
1201
|
+
"conservative behavior (the ancestor classifier answers unavailable and the inherited ask flows to a human)"), { phase: "config", sessionId });
|
|
1202
|
+
}
|
|
1203
|
+
return arming;
|
|
1204
|
+
}
|
|
1205
|
+
function autoModeLatchHealthy(d) {
|
|
1206
|
+
try {
|
|
1207
|
+
return d.breakerOpen() !== true && typeof d.consecutiveFailures === "function" && d.consecutiveFailures() === 0;
|
|
1208
|
+
}
|
|
1209
|
+
catch {
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
function refuseInvalidAutoModeSeat(spec) {
|
|
1214
|
+
if (spec.autoModeRequested !== undefined && spec.autoModeRequested !== true) {
|
|
1215
|
+
const e = new Error(`autoModeRequested must be the literal true when present (got ${JSON.stringify(spec.autoModeRequested)}) — omit it for a non-auto task; a non-boolean or false value is refused rather than read as either polarity.`);
|
|
1216
|
+
e.code = "config.auto_mode_requested_invalid";
|
|
1217
|
+
throw e;
|
|
1218
|
+
}
|
|
1219
|
+
return spec.autoModeRequested === true;
|
|
1220
|
+
}
|
|
1221
|
+
function announceDurableGateUnavailable(args) {
|
|
1222
|
+
if (!args.forceDurableGate || args.storeWired)
|
|
1223
|
+
return;
|
|
1224
|
+
const cause = args.taskStoreNull ? "task_store_null" : "no_deployment_store";
|
|
1225
|
+
const inheritedContentMandate = (args.parentConstraints ?? []).some((pc) => pc.contentMandate === true);
|
|
1226
|
+
const liveQuestionFace = args.liveQuestionFace && !inheritedContentMandate;
|
|
1227
|
+
deliverEngineNotice(args.onNotice, {
|
|
1228
|
+
code: "config.durable_gate_unavailable",
|
|
1229
|
+
message: `Durable approval gate mandated but unavailable: the runtime entitlement forceDurableGate is in force for this ` +
|
|
1230
|
+
`principal, but ${cause === "task_store_null" ? "this task disabled the checkpoint store (checkpointStore: null)" : "this deployment wired no checkpoint store"}, ` +
|
|
1231
|
+
`so no interactive ask of this task can be parked for a durable answer. Asks resolve on the live chain instead ` +
|
|
1232
|
+
`(${args.liveApprover ? "a live approver answers permission asks in-stream" : "permission asks are denied fail-closed — no live approver is wired"}; ` +
|
|
1233
|
+
`${liveQuestionFace ? "a live question face answers AskUserQuestion in-stream" : args.liveQuestionFace ? "AskUserQuestion is withheld from the live face by an inherited durable mandate and refused" : "AskUserQuestion has no live face and is refused"}) ` +
|
|
1234
|
+
`and NO durable approval record is written. ${cause === "task_store_null" ? "Run this task with its store, or withdraw the entitlement for principals whose runs are machine-started." : "Wire a checkpoint store on this deployment, or withdraw the entitlement for principals served here."}`,
|
|
1235
|
+
detail: {
|
|
1236
|
+
sessionId: args.sessionId,
|
|
1237
|
+
runId: args.runId,
|
|
1238
|
+
...(args.principal ? { principal: args.principal } : {}),
|
|
1239
|
+
cause,
|
|
1240
|
+
liveApprover: args.liveApprover,
|
|
1241
|
+
liveQuestionFace,
|
|
1242
|
+
},
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
function buildTier3ReviveSpawn(delegationForRevive, enrichSpecToolCtx) {
|
|
1246
|
+
if (delegationForRevive === undefined)
|
|
1247
|
+
return undefined;
|
|
1248
|
+
return async (req) => {
|
|
1249
|
+
const out = await delegationForRevive.execute({
|
|
1250
|
+
description: req.row.description ?? "revived teammate",
|
|
1251
|
+
prompt: req.prompt,
|
|
1252
|
+
run_in_background: true,
|
|
1253
|
+
...(req.row.agentType !== undefined ? { subagent_type: req.row.agentType } : {}),
|
|
1254
|
+
...(req.row.name !== undefined ? { name: req.row.name } : {}),
|
|
1255
|
+
...(req.row.model !== undefined ? { model: req.row.model } : {}),
|
|
1256
|
+
}, enrichSpecToolCtx({ toolCallId: `rv-${randomBytes(8).toString("hex")}`, reviveClaim: { row: req.row, rev: req.rev, ...(req.peerSeed !== undefined ? { peerSeed: req.peerSeed } : {}) } }));
|
|
1257
|
+
const o = (typeof out === "string" ? { content: out } : out);
|
|
1258
|
+
return {
|
|
1259
|
+
content: typeof o.content === "string" ? o.content : JSON.stringify(o.content),
|
|
1260
|
+
...(o.isError === true ? { isError: true } : {}),
|
|
1261
|
+
...(o.details !== undefined ? { details: o.details } : {}),
|
|
1262
|
+
};
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1164
1265
|
export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf, runIdSink) {
|
|
1165
1266
|
const runId = uuidv7();
|
|
1166
1267
|
if (runIdSink !== undefined)
|
|
1167
1268
|
runIdSink.runId = runId;
|
|
1168
1269
|
const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
|
|
1169
1270
|
spec = doors.spec;
|
|
1271
|
+
const autoModeSeat = refuseInvalidAutoModeSeat(spec);
|
|
1170
1272
|
const { toolFaceSnapshot, promptProfile, lockedPreflight, resolvedInteractionPosture, microCompactKnob, resolvedRole, model, thinking, compModel, fableMitigations, memoryDelegationEvidence, memoryProvenance, memoryCapturePolicy, usageWindows, brainCallGuardrailRef, brainCallGuardrailMs } = doors;
|
|
1171
1273
|
announceToolModelGate(deps.onNotice, model.id, doors.modelGate);
|
|
1274
|
+
const modelGateManifest = toolModelGateManifest(doors.modelGate);
|
|
1172
1275
|
const { toolEffects, egressTools, irreversibleTools, irreversibilityTier, axisExplicitNegatives, reversibilityProbes, ownToolNames } = prepareSafetyScan({ spec, deps });
|
|
1173
1276
|
let shellGatedBash = false;
|
|
1174
1277
|
let shellGatedMonitor = false;
|
|
@@ -1188,6 +1291,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1188
1291
|
const taskScope = internals?.registryScope ?? spec.principal ?? "default";
|
|
1189
1292
|
internals?.peerSelfRef?.addAxis("s", sessionId);
|
|
1190
1293
|
internals?.peerSelfRef?.addAxis("t", hostTaskId);
|
|
1294
|
+
const peerLaneActive = announcePeerLaneMount(deps, { sessionId, runId });
|
|
1295
|
+
const peerLaneRefs = {};
|
|
1296
|
+
const peerSendMessageBuiltIn = runnerSelf !== undefined && !(toolFaceSnapshot.exclude ?? []).includes(SEND_MESSAGE_TOOL_NAME) && !(spec.tools ?? []).some((t) => t.name === SEND_MESSAGE_TOOL_NAME);
|
|
1191
1297
|
const { reminderMark, reminderDisclosureCounts } = await adoptReminderMark(session, sessionId, resume?.seed.reminderMark, internals?.reminderMark, deps.onError);
|
|
1192
1298
|
const offloadScope = spec.principal ?? "default";
|
|
1193
1299
|
if (internals?.registryScope !== undefined && internals.registryScope !== offloadScope) {
|
|
@@ -1575,6 +1681,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1575
1681
|
? liveShellGate
|
|
1576
1682
|
: seedShellGate;
|
|
1577
1683
|
const inheritedParentConstraints = liveInheritedGate?.parentConstraints;
|
|
1684
|
+
const autoModeIntent = autoModeSeat || liveInheritedGate?.autoModeRequested === true || seedInheritedGate?.autoModeRequested === true;
|
|
1578
1685
|
const liveAdmittedOrg = liveInheritedGate?.admittedOrgScopes;
|
|
1579
1686
|
const seedAdmittedOrg = seedInheritedGate?.admittedOrgScopes;
|
|
1580
1687
|
const inheritedAdmittedOrgScopes = liveAdmittedOrg === undefined ? seedAdmittedOrg : seedAdmittedOrg === undefined ? liveAdmittedOrg : liveAdmittedOrg.filter((s) => seedAdmittedOrg.includes(s));
|
|
@@ -1687,6 +1794,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1687
1794
|
return {
|
|
1688
1795
|
...(ancestorRules.length > 0 ? { ancestorRules } : {}),
|
|
1689
1796
|
...(effectiveShellGate !== "off" ? { shellGate: effectiveShellGate } : {}),
|
|
1797
|
+
...(autoModeIntent ? { autoModeRequested: true } : {}),
|
|
1690
1798
|
...(parentConstraints.length > 0 ? { parentConstraints } : {}),
|
|
1691
1799
|
admittedOrgScopes: memoryAdmittedOrgScopesRef.current,
|
|
1692
1800
|
...(orgGovernedProvenance ? { orgAdmissionGoverned: true } : {}),
|
|
@@ -1827,9 +1935,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1827
1935
|
}
|
|
1828
1936
|
catch (err) {
|
|
1829
1937
|
deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "config", sessionId });
|
|
1830
|
-
runtimeCaps = { allowWorkflows: false, allowFork: false };
|
|
1938
|
+
runtimeCaps = { allowWorkflows: false, allowFork: false, autoMode: false };
|
|
1831
1939
|
runtimeCapsFaulted = true;
|
|
1832
1940
|
}
|
|
1941
|
+
const screened = screenAutoModeCap(runtimeCaps, deps.onError, sessionId);
|
|
1942
|
+
runtimeCaps = screened.caps;
|
|
1943
|
+
runtimeCapsFaulted ||= screened.faulted;
|
|
1833
1944
|
}
|
|
1834
1945
|
let complianceDenies = new Set();
|
|
1835
1946
|
let complianceDegraded = false;
|
|
@@ -1866,7 +1977,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1866
1977
|
}
|
|
1867
1978
|
agentForkDenial = forkGovernanceDenial(spec.enableFork, runtimeCaps?.allowFork);
|
|
1868
1979
|
observersActive = runtimeCaps?.allowObservers === true;
|
|
1869
|
-
if (
|
|
1980
|
+
if (autoModeIntent && deps.autoMode !== undefined && runtimeCaps?.autoMode !== false) {
|
|
1870
1981
|
const am = deps.autoMode;
|
|
1871
1982
|
let classifierModel;
|
|
1872
1983
|
try {
|
|
@@ -1878,7 +1989,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1878
1989
|
if (await derivedRouteFallsBack({ seat: "auto-mode-classifier", derived: classifierModel, primary: model, brain: deps.brain, getApiKeyAndHeaders: spec.getApiKeyAndHeaders, onNotice: deps.onNotice, sessionId, runId })) {
|
|
1879
1990
|
classifierModel = model;
|
|
1880
1991
|
}
|
|
1881
|
-
const
|
|
1992
|
+
const classifierLaneRule = peerLaneActive && peerSendMessageBuiltIn;
|
|
1993
|
+
const classifierSystemPrompt = buildAutoModePrompt(classifierLaneRule ? { ...am, crossSessionMessagesRule: CROSS_SESSION_CLASSIFIER_RULE } : am);
|
|
1882
1994
|
const classifierRuntime = brainToRuntime(deps.brain);
|
|
1883
1995
|
autoModeDecider = createAutoModeDecider({
|
|
1884
1996
|
...(am.timeoutMs !== undefined ? { timeoutMs: am.timeoutMs } : {}),
|
|
@@ -1901,17 +2013,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1901
2013
|
.join("");
|
|
1902
2014
|
},
|
|
1903
2015
|
});
|
|
1904
|
-
if (am.persistArming === true)
|
|
1905
|
-
autoModeArming =
|
|
1906
|
-
promptDigest: `apv1:${createHash("sha256").update(classifierSystemPrompt).digest("hex")}`,
|
|
1907
|
-
});
|
|
1908
|
-
if (autoModeArming === undefined) {
|
|
1909
|
-
deps.onError?.(new Error("RunnerDeps.autoMode.persistArming is set but the auto-mode face did not canonicalize into an arming recipe " +
|
|
1910
|
-
"(a rule list carrying a non-string, or a non-finite/out-of-range timeoutMs / failureThreshold / window bound) — " +
|
|
1911
|
-
"NO arming recipe is recorded on this run's parked constraint chain, and a cross-process redemption keeps the " +
|
|
1912
|
-
"conservative behavior (the ancestor classifier answers unavailable and the inherited ask flows to a human)"), { phase: "config", sessionId });
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
2016
|
+
if (am.persistArming === true)
|
|
2017
|
+
autoModeArming = mintPersistedArming(am, classifierLaneRule, classifierSystemPrompt, deps.onError, sessionId);
|
|
1915
2018
|
}
|
|
1916
2019
|
const deploymentWorkflowReady = runnerSelf !== undefined && isSelfOrchestrationActive(spec, deps);
|
|
1917
2020
|
const selfOrchestrationActive = deploymentWorkflowReady && runtimeCaps?.allowWorkflows !== false;
|
|
@@ -2207,7 +2310,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2207
2310
|
shellGatedBash = handsReadFace.shellGatedBash;
|
|
2208
2311
|
shellGatedMonitor = handsReadFace.shellGatedMonitor;
|
|
2209
2312
|
const delegationSurfaceActive = backgroundTaskToolsActive || workflowToolsActive;
|
|
2210
|
-
if (delegationSurfaceActive || internals?.parentNotify !== undefined) {
|
|
2313
|
+
if (delegationSurfaceActive || internals?.parentNotify !== undefined || peerLaneActive) {
|
|
2211
2314
|
if (delegationSurfaceActive) {
|
|
2212
2315
|
toolEffects.set("TaskOutput", "read");
|
|
2213
2316
|
toolEffects.set("TaskStop", "write");
|
|
@@ -2217,24 +2320,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2217
2320
|
toolEffects.set(SEND_MESSAGE_TOOL_NAME, "write");
|
|
2218
2321
|
axisExplicitNegatives.set(SEND_MESSAGE_TOOL_NAME, { ...axisExplicitNegatives.get(SEND_MESSAGE_TOOL_NAME), egress: false });
|
|
2219
2322
|
const delegationForRevive = (spec.tools ?? []).find((t) => t.agentListing !== undefined);
|
|
2220
|
-
const reviveSpawn =
|
|
2221
|
-
? async (req) => {
|
|
2222
|
-
const out = await delegationForRevive.execute({
|
|
2223
|
-
description: req.row.description ?? "revived teammate",
|
|
2224
|
-
prompt: req.prompt,
|
|
2225
|
-
run_in_background: true,
|
|
2226
|
-
...(req.row.agentType !== undefined ? { subagent_type: req.row.agentType } : {}),
|
|
2227
|
-
...(req.row.name !== undefined ? { name: req.row.name } : {}),
|
|
2228
|
-
...(req.row.model !== undefined ? { model: req.row.model } : {}),
|
|
2229
|
-
}, enrichSpecToolCtx({ toolCallId: `rv-${randomBytes(8).toString("hex")}`, reviveClaim: { row: req.row, rev: req.rev, ...(req.peerSeed !== undefined ? { peerSeed: req.peerSeed } : {}) } }));
|
|
2230
|
-
const o = (typeof out === "string" ? { content: out } : out);
|
|
2231
|
-
return {
|
|
2232
|
-
content: typeof o.content === "string" ? o.content : JSON.stringify(o.content),
|
|
2233
|
-
...(o.isError === true ? { isError: true } : {}),
|
|
2234
|
-
...(o.details !== undefined ? { details: o.details } : {}),
|
|
2235
|
-
};
|
|
2236
|
-
}
|
|
2237
|
-
: undefined;
|
|
2323
|
+
const reviveSpawn = deps.backgroundAgentStore !== undefined && deps.mailboxStore !== undefined ? buildTier3ReviveSpawn(delegationForRevive, enrichSpecToolCtx) : undefined;
|
|
2238
2324
|
tools.push(firstPartyOffload(createSendMessageTool({
|
|
2239
2325
|
runner: runnerSelf,
|
|
2240
2326
|
registry: defaultTaskRegistry,
|
|
@@ -2260,6 +2346,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2260
2346
|
...(deps.backgroundAgentStore !== undefined ? { agentStore: deps.backgroundAgentStore } : {}),
|
|
2261
2347
|
...(deps.mailboxStore !== undefined ? { mailbox: deps.mailboxStore } : {}),
|
|
2262
2348
|
...(reviveSpawn !== undefined ? { reviveSpawn } : {}),
|
|
2349
|
+
...(peerLaneActive ? peerLaneSendMessageSeats({ peerDirectory: deps.peerDirectory, sessionId, scope: taskScope, ...(internals?.explicitAgentName !== undefined ? { name: internals.explicitAgentName } : {}), refs: peerLaneRefs, listingMounted: listAgentsMountable({ exclude: toolFaceSnapshot.exclude, specTools: spec.tools ?? [] }) }) : {}),
|
|
2263
2350
|
onNotifyError: (f) => emitTrace(deps.tracer, () => ({ kind: "observer.notify_failed", version: 1, taskId: hostTaskId, site: f.site, message: f.error.message, ts: Date.now() })),
|
|
2264
2351
|
onTranscriptIntegrityGap: (handle, scope) => deliverEngineNotice(deps.onNotice, {
|
|
2265
2352
|
code: "delegation.transcript_integrity",
|
|
@@ -2285,6 +2372,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2285
2372
|
}
|
|
2286
2373
|
}
|
|
2287
2374
|
}
|
|
2375
|
+
const listAgents = peerLaneActive && peerSendMessageBuiltIn && listAgentsMountable({ exclude: toolFaceSnapshot.exclude, specTools: spec.tools ?? [] }) ? mountListAgents({ peerDirectory: deps.peerDirectory, sessionId, scope: taskScope, hostTaskId, ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}), ...(internals?.parentSessionId !== undefined ? { parentSessionId: internals.parentSessionId } : {}), registry: defaultTaskRegistry, ...(deps.rosterStore !== undefined ? { roster: deps.rosterStore } : {}), specTools: spec.tools ?? [], toolEffects }) : undefined;
|
|
2376
|
+
if (listAgents !== undefined)
|
|
2377
|
+
tools.push(firstPartyOffload(listAgents));
|
|
2288
2378
|
if (backgroundTaskToolsActive) {
|
|
2289
2379
|
toolEffects.set("Monitor", "write");
|
|
2290
2380
|
envHandToolNames.add("Monitor");
|
|
@@ -2331,10 +2421,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2331
2421
|
const liveQuestionFace = isLiveQuestionFace(onQuestion) ? onQuestion : undefined;
|
|
2332
2422
|
const contentAskBindings = new Map();
|
|
2333
2423
|
const CONTENT_ASK_BINDING_CAP = 32;
|
|
2424
|
+
const checkpointStore = resolveCheckpointStore(spec, deps);
|
|
2425
|
+
const questionParkStoreWired = checkpointStore !== undefined;
|
|
2426
|
+
const questionDurableMandateBinding = runtimeCaps?.forceDurableGate === true && questionParkStoreWired;
|
|
2334
2427
|
const contentAskRoutable = (toolCallId) => liveQuestionFace !== undefined &&
|
|
2335
2428
|
mountedQuestionTool !== undefined &&
|
|
2336
2429
|
tools.includes(mountedQuestionTool) &&
|
|
2337
|
-
|
|
2430
|
+
!questionDurableMandateBinding &&
|
|
2338
2431
|
!inheritedUnavailableAsks.has(toolCallId);
|
|
2339
2432
|
const lateStrandedAnswers = [];
|
|
2340
2433
|
const discloseStrandedAnswers = (records, why) => {
|
|
@@ -2361,8 +2454,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2361
2454
|
byDelivery.set(r.deliveryId, r);
|
|
2362
2455
|
return [...byDelivery.values()];
|
|
2363
2456
|
};
|
|
2364
|
-
const durableQuestionFace =
|
|
2365
|
-
(spec.durableApproval !== undefined || runtimeCaps?.forceDurableGate === true);
|
|
2457
|
+
const durableQuestionFace = questionParkStoreWired && (spec.durableApproval !== undefined || runtimeCaps?.forceDurableGate === true);
|
|
2366
2458
|
const mountedQuestionFace = liveQuestionFace !== undefined
|
|
2367
2459
|
? async (req, signal) => {
|
|
2368
2460
|
const bound = contentAskBindings.get(req.toolCallId);
|
|
@@ -4272,7 +4364,6 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4272
4364
|
}
|
|
4273
4365
|
const usageGovernance = buildUsageGovernance(usageWindows, deps, spec.principal, sessionId);
|
|
4274
4366
|
const platformSuspendArmed = durableSuspendInfraReady && (envLifetimeSuspendAt !== undefined || usageGovernance !== undefined);
|
|
4275
|
-
const checkpointStore = resolveCheckpointStore(spec, deps);
|
|
4276
4367
|
const durableApproval = spec.durableApproval ??
|
|
4277
4368
|
(runtimeCaps?.forceDurableGate ? { scope: spec.principal || DEFAULT_IRREVERSIBLE_SCOPE } : undefined);
|
|
4278
4369
|
const wiringQuestionSeam = resolveQuestionSeam(spec, deps);
|
|
@@ -4308,8 +4399,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4308
4399
|
complianceWired: deps.compliancePostureResolver !== undefined,
|
|
4309
4400
|
memoryAdmissionWired: deps.memoryScopeAdmission !== undefined,
|
|
4310
4401
|
retentionPolicyWired: deps.retentionPolicy !== undefined,
|
|
4402
|
+
...(modelGateManifest !== undefined ? { modelGate: modelGateManifest } : {}),
|
|
4311
4403
|
});
|
|
4404
|
+
peerLaneRefs.askEffective = wiringManifest.ask.effective;
|
|
4405
|
+
if (peerLaneActive && internals?.onTaskNotification !== undefined) {
|
|
4406
|
+
bindPeerLaneDrain(harness, { deps, sessionId, runId, scope: taskScope, inject: internals.onTaskNotification, ownTokens: () => internals?.peerSelfRef?.current.ownTokens ?? [], refs: peerLaneRefs });
|
|
4407
|
+
}
|
|
4312
4408
|
const parkLaneArmed = wiringManifest.parkLane.effective === true;
|
|
4409
|
+
announceDurableGateUnavailable({ onNotice: deps.onNotice, forceDurableGate: runtimeCaps?.forceDurableGate === true, storeWired: checkpointStore !== undefined, taskStoreNull: spec.checkpointStore === null, liveApprover: isLiveApproverSeat(frozenOnAsk), liveQuestionFace: liveQuestionFace !== undefined, parentConstraints: liveInheritedGate?.parentConstraints, sessionId, runId, principal: spec.principal });
|
|
4313
4410
|
const hookIdentity = mintHookInvocationIdentity({
|
|
4314
4411
|
sessionId,
|
|
4315
4412
|
taskId: hostTaskId,
|
|
@@ -4449,8 +4546,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4449
4546
|
const constraintDigest = (inheritedParentConstraints?.length ?? 0) > 0
|
|
4450
4547
|
? constraintChainDigest(constraintChain)
|
|
4451
4548
|
: seedInheritedGate?.constraintDigest;
|
|
4549
|
+
const autoModeIntentCarried = autoModeIntent && (autoModeDecider === undefined || autoModeLatchHealthy(autoModeDecider));
|
|
4452
4550
|
return inheritedAncestorRules !== undefined ||
|
|
4453
4551
|
inheritedShellGate !== undefined ||
|
|
4552
|
+
autoModeIntentCarried ||
|
|
4454
4553
|
inheritedAdmittedOrgScopes !== undefined ||
|
|
4455
4554
|
ownOrgVerdictRef.current !== undefined ||
|
|
4456
4555
|
orgGovernedProvenance ||
|
|
@@ -4458,6 +4557,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4458
4557
|
? {
|
|
4459
4558
|
...(inheritedAncestorRules !== undefined ? { ancestorRules: structuredClone(inheritedAncestorRules) } : {}),
|
|
4460
4559
|
...(inheritedShellGate !== undefined ? { shellGate: inheritedShellGate } : {}),
|
|
4560
|
+
...(autoModeIntentCarried ? { autoModeRequested: true } : {}),
|
|
4461
4561
|
...(inheritedAdmittedOrgScopes !== undefined ? { admittedOrgScopes: [...inheritedAdmittedOrgScopes] } : {}),
|
|
4462
4562
|
...(ownOrgVerdictRef.current !== undefined
|
|
4463
4563
|
? { ownAdmittedOrgScopes: [...ownOrgVerdictRef.current.scopes], ownAdmittedOrgWriteScope: ownOrgVerdictRef.current.writeScope }
|
|
@@ -21,6 +21,7 @@ import { ORG_ADJUDICATION_TIMEOUT_MS, settleOrgVerdictWithin } from "../permissi
|
|
|
21
21
|
import { emitTaskOutcome } from "../task-outcome.js";
|
|
22
22
|
import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
|
|
23
23
|
import { primaryActivityArg } from "../arg-summary.js";
|
|
24
|
+
import { redactThenCut } from "../../agents/subagent-steps.js";
|
|
24
25
|
import { resolveReasoning } from "../../brain/reasoning.js";
|
|
25
26
|
import { adjudicateDerivedRoute, authCarrierFingerprint, fallbackToPrimaryNotice, normalizeBaseUrl, sameRouteIdentity } from "../../brain/route-adjudicator.js";
|
|
26
27
|
import { readDegradation } from "../../brain/degrading.js";
|
|
@@ -773,7 +774,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
773
774
|
boundaryAttachmentBytes += Buffer.byteLength(a.body, "utf8");
|
|
774
775
|
rs.attach.attachmentsInjected += due.length;
|
|
775
776
|
for (const a of due) {
|
|
776
|
-
queue.push({ type: "steering_injected", source: a.source, preview: a.body
|
|
777
|
+
queue.push({ type: "steering_injected", source: a.source, preview: redactThenCut(a.body, 220), ...ident() });
|
|
777
778
|
}
|
|
778
779
|
}
|
|
779
780
|
}
|
|
@@ -3821,7 +3822,7 @@ export class Runner {
|
|
|
3821
3822
|
}
|
|
3822
3823
|
for (const f of firstFrames) {
|
|
3823
3824
|
f.commit();
|
|
3824
|
-
queue.push({ type: "steering_injected", source: f.source, preview: f.body
|
|
3825
|
+
queue.push({ type: "steering_injected", source: f.source, preview: redactThenCut(f.body, 220), ...ident() });
|
|
3825
3826
|
}
|
|
3826
3827
|
rs.attach.attachmentsInjected += firstFrames.length;
|
|
3827
3828
|
if (firstFrames.length > 0)
|
|
@@ -64,6 +64,7 @@ const CC_DETAIL_TYPES = new Set([
|
|
|
64
64
|
"task-stop", "tool-search", "repo-map", "fork", "enter-plan-mode", "exit-plan-mode",
|
|
65
65
|
"monitor-start", "path_not_in_root", "read_path_denied", "readonly_out_of_root", "bash_invalid_timeout",
|
|
66
66
|
"report-findings", "schedule-wakeup", "send-message", "agent-transcript", "a2a", "document",
|
|
67
|
+
"list-agents",
|
|
67
68
|
]);
|
|
68
69
|
export const structuredFrom = (result) => {
|
|
69
70
|
const details = result !== null && typeof result === "object" ? result.details : undefined;
|
|
@@ -46,6 +46,29 @@ export declare function mailboxAckOwnershipContract(mk: () => MailboxStore, runA
|
|
|
46
46
|
* 放宽」:核心层没有一条为了任何后端而弱化。第三方后端若有租户轴,应当也挂这一层。
|
|
47
47
|
*/
|
|
48
48
|
export declare function mailboxBundledOnlyContract(mk: () => MailboxStore, runAssertion?: ContractAssertionRunner): Promise<void>;
|
|
49
|
+
export interface MailboxCrossProcessContractHooks {
|
|
50
|
+
/** Open ONE fresh, empty storage location TWICE: `local` in this process and `peer` served by ANOTHER
|
|
51
|
+
* OS process over the same storage (the peer is a real second process — an in-process second
|
|
52
|
+
* instance proves nothing here). `dispose` closes both and may remove the storage. The kit calls it
|
|
53
|
+
* once per case. */
|
|
54
|
+
openPair: () => Promise<{
|
|
55
|
+
local: MailboxStore;
|
|
56
|
+
peer: MailboxStore;
|
|
57
|
+
dispose: () => Promise<void>;
|
|
58
|
+
}>;
|
|
59
|
+
runAssertion?: ContractAssertionRunner;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* T2 可选能力层(cross-process 分册,design/385 §1.2⑥ / G11)—— 声明 `crossProcessSafe: true` 的后端必须
|
|
63
|
+
* 过的全套判据。钉的是**整套**(跨进程 seq 单铸 + lease/ack 不交叉 + 一方的 housekeeping 不丢另一方的
|
|
64
|
+
* 已落盘 append + reap/drop 的 seq 语义跨进程成立),不是「有锁」:一把锁配一份陈旧缓存照样在锁内铸
|
|
65
|
+
* 重号、照样把别人的消息压缩掉——所以每一条都从**对方进程的视角**回读。
|
|
66
|
+
*
|
|
67
|
+
* 受众:文件后端(本仓捆绑)、SQL 后端(事务天然过)。进程内 Map 的参照实现与 CC inbox 适配器不声明,
|
|
68
|
+
* 也不挂——它们不是「没做到所以放宽」,而是根本不在跨进程车道上(挂载判据 `mailboxCrossProcessMountVerdict`
|
|
69
|
+
* 对未声明的后端响亮拒)。
|
|
70
|
+
*/
|
|
71
|
+
export declare function mailboxCrossProcessContract(hooks: MailboxCrossProcessContractHooks): Promise<void>;
|
|
49
72
|
export interface MailboxTombstonedRecipientContractHooks {
|
|
50
73
|
/** Put `(scope, handle)` into the deployment's PRE-DELETE state — whatever that is for this
|
|
51
74
|
* backend (a cascade marking the session row, a `deleting` column, a tombstone table). Core does
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { strict as assert } from "node:assert";
|
|
2
|
-
import { MAILBOX_TOMBSTONED_RECIPIENT_CODE } from "../mailbox-store.js";
|
|
2
|
+
import { MAILBOX_INVALID_PEER_META_CODE, MAILBOX_TOMBSTONED_RECIPIENT_CODE } from "../mailbox-store.js";
|
|
3
3
|
import { beginContract } from "./contract-harness.js";
|
|
4
4
|
export const MAILBOX_CONTRACT_SCOPE = "default";
|
|
5
5
|
const msg = (content, sentAt = 1000) => ({ content, sentAt });
|
|
@@ -112,6 +112,75 @@ export async function mailboxStoreContract(mk, runAssertion) {
|
|
|
112
112
|
const after = await s.claimLease(S, "a1", "rival", 60_000, 12_000);
|
|
113
113
|
assert.deepEqual(after?.messages.map((m) => m.seq), [1], "归还后同一条按原 seq 回可见");
|
|
114
114
|
});
|
|
115
|
+
run("peerMeta 回环:典型 typed 记录逐字节回读;缺席保持缺席(pre-385/foreign 记录语义);空对象也是「在场」", async (make) => {
|
|
116
|
+
const s = make();
|
|
117
|
+
const full = {
|
|
118
|
+
fromSession: "sess_1",
|
|
119
|
+
fromMode: "prompting",
|
|
120
|
+
senderKey: "k-1",
|
|
121
|
+
kind: "peer_message",
|
|
122
|
+
fromScope: "team/a b",
|
|
123
|
+
gateReceiptId: "gr-9",
|
|
124
|
+
};
|
|
125
|
+
await s.append(S, "a1", { content: "typed", sentAt: 1_000, peerMeta: full });
|
|
126
|
+
await s.append(S, "a1", { content: "plain", sentAt: 1_001 });
|
|
127
|
+
await s.append(S, "a1", { content: "empty-meta", sentAt: 1_002, peerMeta: {} });
|
|
128
|
+
const lease = await s.claimLease(S, "a1", "w1", 60_000, 10_000);
|
|
129
|
+
assert.deepEqual(lease.messages.map((m) => m.content), ["typed", "plain", "empty-meta"]);
|
|
130
|
+
assert.deepEqual(lease.messages[0].peerMeta, full, "六字段逐字节回读");
|
|
131
|
+
assert.equal("peerMeta" in lease.messages[1], false, "未带 peerMeta 的记录回读时该键必须缺席(不是 undefined 值)");
|
|
132
|
+
assert.deepEqual(lease.messages[2].peerMeta, {}, "空对象=在场但无字段,与缺席区分");
|
|
133
|
+
});
|
|
134
|
+
run("peerMeta 坏值响亮:garbage 以具名码拒 append,且零副作用(不入箱、不耗 seq、不动租约)", async (make) => {
|
|
135
|
+
const s = make();
|
|
136
|
+
const seqBefore = await s.append(S, "a1", { content: "anchor", sentAt: 1_000 });
|
|
137
|
+
const held = await s.claimLease(S, "a1", "holder", 60_000, 10_000);
|
|
138
|
+
assert.equal(held?.messages.length, 1);
|
|
139
|
+
const codeOf = (e) => e?.code;
|
|
140
|
+
class Meta {
|
|
141
|
+
fromSession = "s";
|
|
142
|
+
}
|
|
143
|
+
const hiddenKey = { fromSession: "s" };
|
|
144
|
+
Object.defineProperty(hiddenKey, "bogus", { value: 1, enumerable: false });
|
|
145
|
+
for (const [label, bad] of [
|
|
146
|
+
["fromMode 集外值", { fromMode: "root" }],
|
|
147
|
+
["kind 集外值", { kind: "gossip" }],
|
|
148
|
+
["未知键", { fromSession: "s", bogus: 1 }],
|
|
149
|
+
["空字符串", { senderKey: "" }],
|
|
150
|
+
["非字符串", { gateReceiptId: 7 }],
|
|
151
|
+
["数组", ["fromSession"]],
|
|
152
|
+
["null", null],
|
|
153
|
+
["字符串", "fromSession=x"],
|
|
154
|
+
["Date", new Date(0)],
|
|
155
|
+
["Map", new Map()],
|
|
156
|
+
["class 实例", new Meta()],
|
|
157
|
+
["symbol 键", { fromSession: "s", [Symbol("hidden")]: 1 }],
|
|
158
|
+
["不可枚举的未知键", hiddenKey],
|
|
159
|
+
]) {
|
|
160
|
+
let thrown;
|
|
161
|
+
try {
|
|
162
|
+
await s.append(S, "a1", { content: `bad:${label}`, sentAt: 2_000, peerMeta: bad });
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
thrown = e;
|
|
166
|
+
}
|
|
167
|
+
assert.notEqual(thrown, undefined, `${label}:必须拒`);
|
|
168
|
+
assert.equal(codeOf(thrown), MAILBOX_INVALID_PEER_META_CODE, `${label}:拒必须带码`);
|
|
169
|
+
}
|
|
170
|
+
assert.equal(await s.peekCount(S, "a1"), 1, "被拒的 append 不得入箱");
|
|
171
|
+
assert.equal(await s.claimLease(S, "a1", "rival", 60_000, 11_000), null, "被拒的 append 不得动活租约");
|
|
172
|
+
await s.ack(S, "a1", "holder", held.maxSeq);
|
|
173
|
+
assert.equal(await s.append(S, "a1", { content: "next", sentAt: 3_000 }), seqBefore + 1, "被拒的 append 不得消耗 seq");
|
|
174
|
+
});
|
|
175
|
+
run("peerMeta 不串引用:消费者改写租约批里的 peerMeta,不得改到下次投递的记录", async (make) => {
|
|
176
|
+
const s = make();
|
|
177
|
+
await s.append(S, "a1", { content: "m1", sentAt: 1_000, peerMeta: { fromMode: "bypass", fromSession: "sess_1" } });
|
|
178
|
+
const first = await s.claimLease(S, "a1", "w1", 1_000, 10_000);
|
|
179
|
+
first.messages[0].peerMeta.fromSession = "forged";
|
|
180
|
+
await s.releaseLease(S, "a1", "w1");
|
|
181
|
+
const again = await s.claimLease(S, "a1", "w2", 1_000, 20_000);
|
|
182
|
+
assert.deepEqual(again.messages[0].peerMeta, { fromMode: "bypass", fromSession: "sess_1" }, "重投的记录必须是原样");
|
|
183
|
+
});
|
|
115
184
|
await settle();
|
|
116
185
|
}
|
|
117
186
|
export async function mailboxAckOwnershipContract(mk, runAssertion) {
|
|
@@ -192,6 +261,93 @@ export async function mailboxBundledOnlyContract(mk, runAssertion) {
|
|
|
192
261
|
});
|
|
193
262
|
await settle();
|
|
194
263
|
}
|
|
264
|
+
export async function mailboxCrossProcessContract(hooks) {
|
|
265
|
+
const { run: runRaw, settle } = beginContract(hooks.runAssertion);
|
|
266
|
+
const run = (name, fn) => runRaw(name, async () => {
|
|
267
|
+
const pair = await hooks.openPair();
|
|
268
|
+
try {
|
|
269
|
+
await fn(pair.local, pair.peer);
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
await pair.dispose();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
const S = MAILBOX_CONTRACT_SCOPE;
|
|
276
|
+
const H = "session.shared";
|
|
277
|
+
const range = (n) => Array.from({ length: n }, (_, i) => i + 1);
|
|
278
|
+
run("跨进程 seq 单铸:两个进程交错 append N+N ⇒ seq 恰为 1..2N(无重复无空洞),每条消息在任一方视角都可见", async (local, peer) => {
|
|
279
|
+
const N = 40;
|
|
280
|
+
const seqs = [];
|
|
281
|
+
for (let i = 0; i < N; i++) {
|
|
282
|
+
const [a, b] = await Promise.all([local.append(S, H, msg(`local-${i}`, 1_000 + i)), peer.append(S, H, msg(`peer-${i}`, 2_000 + i))]);
|
|
283
|
+
seqs.push(a, b);
|
|
284
|
+
}
|
|
285
|
+
assert.deepEqual([...seqs].sort((x, y) => x - y), range(2 * N), "两方拿到的回执合起来必须恰是 1..2N");
|
|
286
|
+
assert.equal(await peer.peekCount(S, H), 2 * N, "对方进程的观察面必须看到本方的 append");
|
|
287
|
+
assert.equal(await local.peekCount(S, H), 2 * N);
|
|
288
|
+
const lease = await local.claimLease(S, H, "cycle-1", 60_000, 10_000);
|
|
289
|
+
assert.deepEqual(lease.messages.map((m) => m.seq), range(2 * N), "本方 claim 必须按 seq 序取到全部 2N 条");
|
|
290
|
+
assert.equal(lease.maxSeq, 2 * N);
|
|
291
|
+
const contents = new Set(lease.messages.map((m) => m.content));
|
|
292
|
+
for (let i = 0; i < N; i++) {
|
|
293
|
+
assert.ok(contents.has(`local-${i}`), `local-${i} 不得丢`);
|
|
294
|
+
assert.ok(contents.has(`peer-${i}`), `peer-${i} 不得丢`);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
run("lease/ack 不跨进程交叉:一方的活租约 fence 住另一方的 claim;非持有方的 ack 是 no-op;持有方的 ack 对另一方立即可见;高水位跨对方的 ack 存活", async (local, peer) => {
|
|
298
|
+
await local.append(S, H, msg("m1", 1));
|
|
299
|
+
await peer.append(S, H, msg("m2", 2));
|
|
300
|
+
const held = await peer.claimLease(S, H, "cycle-peer", 60_000, 10_000);
|
|
301
|
+
assert.deepEqual(held.messages.map((m) => m.seq), [1, 2]);
|
|
302
|
+
assert.equal(await local.claimLease(S, H, "cycle-local", 60_000, 11_000), null, "对方进程的活租约必须 fence 住本方");
|
|
303
|
+
await local.ack(S, H, "cycle-local", held.maxSeq);
|
|
304
|
+
assert.equal(await peer.peekCount(S, H), 2, "非持有方的 ack 不得动持有方手上的批");
|
|
305
|
+
await peer.ack(S, H, "cycle-peer", held.maxSeq);
|
|
306
|
+
assert.equal(await local.peekCount(S, H), 0, "持有方的 ack 必须被本方看见");
|
|
307
|
+
assert.equal(await local.claimLease(S, H, "cycle-local", 60_000, 12_000), null);
|
|
308
|
+
assert.equal(await local.append(S, H, msg("m3", 3)), 3, "seq 高水位必须跨对方进程的 ack 存活(永不复用)");
|
|
309
|
+
await peer.append(S, H, msg("m4", 4));
|
|
310
|
+
const held2 = await peer.claimLease(S, H, "cycle-peer-2", 1_000, 20_000);
|
|
311
|
+
assert.deepEqual(held2.messages.map((m) => m.seq), [3, 4]);
|
|
312
|
+
const takeover = await local.claimLease(S, H, "cycle-local-2", 60_000, 30_000);
|
|
313
|
+
assert.deepEqual(takeover.messages.map((m) => m.seq), [3, 4], "过期租约的接管跨进程成立,seq 原样");
|
|
314
|
+
});
|
|
315
|
+
run("一方的 housekeeping 不丢另一方的 append:两进程各自反复压缩/重写期间交错写入 120 条,任一方视角都完整", async (local, peer) => {
|
|
316
|
+
const N = 60;
|
|
317
|
+
for (let i = 0; i < N; i++) {
|
|
318
|
+
await Promise.all([local.append(S, H, msg(`L${i}`, 1_000 + i)), peer.append(S, H, msg(`P${i}`, 2_000 + i))]);
|
|
319
|
+
}
|
|
320
|
+
const fromPeer = await peer.claimLease(S, H, "cycle-peer", 60_000, 10_000);
|
|
321
|
+
assert.deepEqual(fromPeer.messages.map((m) => m.seq), range(2 * N), "对方视角:seq 1..2N 全在");
|
|
322
|
+
await peer.releaseLease(S, H, "cycle-peer");
|
|
323
|
+
const fromLocal = await local.claimLease(S, H, "cycle-local", 60_000, 11_000);
|
|
324
|
+
assert.deepEqual(fromLocal.messages.map((m) => m.seq), range(2 * N), "本方视角:seq 1..2N 全在");
|
|
325
|
+
const contents = fromLocal.messages.map((m) => m.content).sort();
|
|
326
|
+
assert.deepEqual(contents, [...range(N).map((i) => `L${i - 1}`), ...range(N).map((i) => `P${i - 1}`)].sort());
|
|
327
|
+
await local.ack(S, H, "cycle-local", fromLocal.maxSeq);
|
|
328
|
+
assert.equal(await peer.peekCount(S, H), 0, "本方的 ack 对方可见");
|
|
329
|
+
});
|
|
330
|
+
run("reap 看得见对方进程的旧消息,哪怕本方缓存里这个箱是空的(已加载的空缓存不得替对方的旧消息说「没有」)", async (local, peer) => {
|
|
331
|
+
await local.append(S, H, msg("mine", 1_000));
|
|
332
|
+
const held = await local.claimLease(S, H, "cycle-local", 60_000, 1_000);
|
|
333
|
+
await local.ack(S, H, "cycle-local", held.maxSeq);
|
|
334
|
+
await peer.append(S, H, msg("old-from-peer", 2_000));
|
|
335
|
+
assert.equal(await local.reap(S, 100_000, { maxAgeMs: 50_000 }), 1, "对方写的过期消息必须被本方 reap 看见并老化");
|
|
336
|
+
assert.equal(await peer.peekCount(S, H), 0);
|
|
337
|
+
assert.equal(await peer.append(S, H, msg("after", 200_000)), 3, "高水位跨 reap 存活(对方视角)");
|
|
338
|
+
});
|
|
339
|
+
run("reap 与 drop 的 seq 语义跨进程:一方 reap 后另一方继续高水位;一方 drop 后另一方从 1 重铸", async (local, peer) => {
|
|
340
|
+
await peer.append(S, H, msg("old", 1_000));
|
|
341
|
+
assert.equal(await local.reap(S, 100_000, { maxAgeMs: 50_000 }), 1, "本方 reap 必须看见对方写的旧消息");
|
|
342
|
+
assert.equal(await peer.peekCount(S, H), 0);
|
|
343
|
+
assert.equal(await peer.append(S, H, msg("after-reap", 200_000)), 2, "reap 清箱但保高水位(RB-90 跨进程)");
|
|
344
|
+
await local.drop(S, H);
|
|
345
|
+
assert.equal(await peer.peekCount(S, H), 0, "本方 drop 对方可见");
|
|
346
|
+
assert.equal(await peer.append(S, H, msg("after-drop", 300_000)), 1, "drop 结束箱的一生:对方从 1 重铸");
|
|
347
|
+
assert.equal(await local.peekCount(S, H), 1);
|
|
348
|
+
});
|
|
349
|
+
await settle();
|
|
350
|
+
}
|
|
195
351
|
export async function mailboxTombstonedRecipientContract(mk, hooks) {
|
|
196
352
|
const { run: runRaw, settle } = beginContract(hooks.runAssertion);
|
|
197
353
|
const run = (name, fn) => runRaw(name, () => withStores(mk, fn));
|