@sema-agent/core 7.3.0 → 7.4.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 +49 -0
- package/dist/agents/peer-admission.d.ts +18 -3
- package/dist/agents/peer-admission.js +79 -4
- package/dist/agents/peer-held-queue.d.ts +101 -0
- package/dist/agents/peer-held-queue.js +229 -0
- package/dist/agents/peer-idle.d.ts +109 -0
- package/dist/agents/peer-idle.js +240 -0
- package/dist/agents/peer-notice-route.d.ts +33 -0
- package/dist/agents/peer-notice-route.js +46 -0
- package/dist/agents/peer-notices.d.ts +103 -0
- package/dist/agents/peer-notices.js +206 -0
- package/dist/agents/peer-session-drain.d.ts +39 -4
- package/dist/agents/peer-session-drain.js +248 -42
- package/dist/agents/send-message-tool.d.ts +8 -1
- package/dist/agents/send-message-tool.js +96 -30
- package/dist/agents/subagent.js +1 -0
- package/dist/brain/status-sink.d.ts +10 -0
- package/dist/brain/status-sink.js +13 -4
- package/dist/brain/stream-engine.d.ts +11 -0
- package/dist/brain/stream-engine.js +39 -3
- package/dist/core/arg-summary.d.ts +13 -3
- package/dist/core/arg-summary.js +138 -7
- package/dist/core/auto-mode-defaults.d.ts +11 -0
- package/dist/core/auto-mode-defaults.js +2 -0
- package/dist/core/auto-mode.d.ts +59 -0
- package/dist/core/auto-mode.js +57 -1
- package/dist/core/checkpoint-store.js +2 -2
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +8 -0
- package/dist/core/hooks.d.ts +30 -0
- package/dist/core/hooks.js +43 -8
- package/dist/core/mailbox-store.d.ts +33 -1
- package/dist/core/mailbox-store.js +42 -2
- package/dist/core/runner/assemble-result.d.ts +5 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/denial-limit-arms.d.ts +149 -0
- package/dist/core/runner/denial-limit-arms.js +91 -0
- package/dist/core/runner/edited-files-ledger.d.ts +33 -0
- package/dist/core/runner/edited-files-ledger.js +14 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +5 -0
- package/dist/core/runner/prepare-hands-readface.js +1 -0
- package/dist/core/runner/prepare-task.d.ts +62 -1
- package/dist/core/runner/prepare-task.js +135 -89
- package/dist/core/runner/runtask.js +12 -0
- package/dist/core/sensitive-path-policy.d.ts +27 -6
- package/dist/core/sensitive-path-policy.js +57 -2
- package/dist/core/task-notification.d.ts +24 -2
- package/dist/core/task-notification.js +6 -1
- package/dist/core/tool-policy.d.ts +55 -4
- package/dist/core/tool-policy.js +28 -5
- package/dist/core/tools.js +1 -0
- package/dist/core/types.d.ts +251 -15
- package/dist/core/wiring-manifest.d.ts +41 -5
- package/dist/core/wiring-manifest.js +8 -0
- package/dist/engine/harness/agent-harness.d.ts +1 -0
- package/dist/engine/harness/agent-harness.js +3 -0
- package/dist/engine/harness/types.d.ts +3 -0
- package/dist/engine/loop/agent-loop.d.ts +7 -0
- package/dist/engine/loop/agent-loop.js +79 -0
- package/dist/engine/loop/types.d.ts +42 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.js +10 -4
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/orchestration/workflow.js +7 -3
- package/dist/tools/fs/fs-write.d.ts +4 -4
- package/dist/tools/fs/fs-write.js +99 -14
- package/dist/tools/fs/index.d.ts +7 -1
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/fs/safety.d.ts +29 -8
- package/dist/tools/fs/safety.js +11 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +181 -1
|
@@ -8,6 +8,8 @@ import { type PeerDirectory } from "./peer-directory.js";
|
|
|
8
8
|
import { createListAgentsTool } from "./list-agents-tool.js";
|
|
9
9
|
import type { AskEffective } from "../core/wiring-manifest.js";
|
|
10
10
|
import type { SendMessageToolOptions } from "./send-message-tool.js";
|
|
11
|
+
import { type PeerClock, type PeerHeldQueue } from "./peer-held-queue.js";
|
|
12
|
+
import { type PeerIdleMachine } from "./peer-idle.js";
|
|
11
13
|
/** design/385 §1.2④ — the drain lease TTL. Same sizing law as the tier-3 revive lease: well under the
|
|
12
14
|
* store's stale-row floor, long enough to cover one turn (a longer turn renews on the same owner at
|
|
13
15
|
* its next boundary; an expired lease re-serves the batch — at-least-once). */
|
|
@@ -34,6 +36,8 @@ export declare function judgePeerLaneMount(deps: {
|
|
|
34
36
|
peerDirectory?: PeerDirectory;
|
|
35
37
|
mailboxStore?: MailboxStore;
|
|
36
38
|
}, sessionId?: string): PeerLaneMountVerdict | undefined;
|
|
39
|
+
/** The disposition arms of a `peer.inbound_disposition` notice (closed set; `detail.disposition`). */
|
|
40
|
+
export type PeerInboundDisposition = "refused" | "held" | "admission_refused" | "notice_unrouted" | "notice_user_only";
|
|
37
41
|
export interface PeerSessionDrainOptions {
|
|
38
42
|
mailbox: MailboxStore;
|
|
39
43
|
/** This session (the box owner) and this leg's cycle id (the lease owner). */
|
|
@@ -53,6 +57,17 @@ export interface PeerSessionDrainOptions {
|
|
|
53
57
|
settingLayers: () => CrossSessionInboundSettingLayers | undefined;
|
|
54
58
|
/** The recipient's own permission-mode class, read at every drain (the manifest's ask fold). */
|
|
55
59
|
selfModeClass: () => PermissionModeClass | "unknown";
|
|
60
|
+
/** This session's display name (its directory row's name), for receipts and idle notices it authors. */
|
|
61
|
+
selfName?: () => string | undefined;
|
|
62
|
+
/** Awaited ONCE before the first round (a directory lookup that fills `selfName`, say); a rejection is swallowed. */
|
|
63
|
+
warmup?: () => Promise<unknown>;
|
|
64
|
+
/** The deployment's `dialogExpiry` (raw; resolved at bind — an out-of-vocabulary value is announced
|
|
65
|
+
* once through `onError` and the default applies). Absent ⇒ the default (5 m). */
|
|
66
|
+
dialogExpiry?: () => unknown;
|
|
67
|
+
/** The last turn's text, for the idle notice's one-line detail (CC `getLastTurnText`). */
|
|
68
|
+
lastTurnText?: () => string | undefined;
|
|
69
|
+
/** Whether the run is parked on a pending ask / review at its end (a parked run is `blocked`, not idle). */
|
|
70
|
+
parked?: () => boolean;
|
|
56
71
|
onNotice?: (notice: EngineNotice) => void;
|
|
57
72
|
onError?: (error: unknown, ctx: {
|
|
58
73
|
phase: "degraded";
|
|
@@ -60,12 +75,22 @@ export interface PeerSessionDrainOptions {
|
|
|
60
75
|
classification: string;
|
|
61
76
|
}) => void;
|
|
62
77
|
leaseTtlMs?: number;
|
|
78
|
+
/** Tests: a virtual clock for the held deadlines and idle timers (honored when this process first sees the session). */
|
|
79
|
+
clock?: PeerClock;
|
|
63
80
|
}
|
|
64
81
|
export interface PeerSessionDrain {
|
|
65
82
|
/** One drain round (serialized: a boundary never overlaps a still-running round). */
|
|
66
83
|
drain(): Promise<void>;
|
|
67
84
|
/** Run end: ack the last batch and release the lease so the session's next cycle claims at once. */
|
|
68
85
|
finish(): Promise<void>;
|
|
86
|
+
/** The run began working (cancels a pending idle fire). */
|
|
87
|
+
busy(): void;
|
|
88
|
+
/** The run ended with nothing queued and no pending ask: the one-shot idle fire is scheduled (§5.2). */
|
|
89
|
+
idle(finishedAt: number): void;
|
|
90
|
+
/** This session's held queue (process-level; the host review face reads/settles through it). */
|
|
91
|
+
readonly held: PeerHeldQueue;
|
|
92
|
+
/** This session's idle machine (process-level). */
|
|
93
|
+
readonly idleMachine: PeerIdleMachine;
|
|
69
94
|
}
|
|
70
95
|
export declare function createPeerSessionDrain(opts: PeerSessionDrainOptions): PeerSessionDrain;
|
|
71
96
|
/** The lane's late-bound facts: the ask fold resolves after the manifest derivation, the mount runs
|
|
@@ -134,26 +159,36 @@ export interface PeerDrainHarness {
|
|
|
134
159
|
on(type: "turn_boundary" | "before_agent_start", handler: () => Promise<undefined>): unknown;
|
|
135
160
|
subscribe(listener: (event: {
|
|
136
161
|
type: string;
|
|
162
|
+
nextTurnCount?: number;
|
|
163
|
+
undrainedSteerCount?: number;
|
|
164
|
+
undrainedFollowUpCount?: number;
|
|
137
165
|
}) => void | Promise<void>): unknown;
|
|
138
166
|
}
|
|
139
167
|
/**
|
|
140
168
|
* Bind the drain to a leg: the run's own injector at the `next` tier, run open (the loop's first
|
|
141
169
|
* steering drain follows the `before_agent_start` hook) + every turn boundary, and the run-end
|
|
142
|
-
* ack/release on `agent_end`.
|
|
170
|
+
* ack/release on `agent_end`. Slice 4: the harness's `settled` account (the frame after `agent_end`)
|
|
171
|
+
* is the idle judgment's seat — "finished its turn with nothing queued" = no next-turn frames, no
|
|
172
|
+
* undrained user inputs, and (through `opts.parked`) no pending ask/review; the held queue's own
|
|
173
|
+
* emptiness is the machine's to check.
|
|
143
174
|
*/
|
|
144
175
|
export declare function bindPeerSessionDrain(harness: PeerDrainHarness, opts: PeerSessionDrainOptions): PeerSessionDrain;
|
|
145
176
|
/**
|
|
146
177
|
* The leg-side spelling of {@link bindPeerSessionDrain}: the deployment seats the drain reads
|
|
147
|
-
* (`mailboxStore` / `peerAdmission` / `crossSessionInbound` / the two
|
|
148
|
-
* recipient's mode class folds off the lane refs — one binding call
|
|
178
|
+
* (`mailboxStore` / `peerAdmission` / `crossSessionInbound` / `crossSessionDialogExpiry` / the two
|
|
179
|
+
* sinks) come straight off deps, the recipient's mode class folds off the lane refs — one binding call
|
|
180
|
+
* per leg, no per-seat plumbing.
|
|
149
181
|
*/
|
|
150
182
|
export declare function bindPeerLaneDrain(harness: PeerDrainHarness, args: {
|
|
151
|
-
deps: Pick<RunnerDeps, "mailboxStore" | "peerAdmission" | "crossSessionInbound" | "onNotice" | "onError">;
|
|
183
|
+
deps: Pick<RunnerDeps, "mailboxStore" | "peerDirectory" | "peerAdmission" | "crossSessionInbound" | "crossSessionDialogExpiry" | "onNotice" | "onError">;
|
|
152
184
|
sessionId: string;
|
|
153
185
|
runId: string;
|
|
154
186
|
scope: string;
|
|
155
187
|
inject: PeerSessionDrainOptions["inject"];
|
|
156
188
|
ownTokens: () => readonly string[];
|
|
157
189
|
refs: PeerLaneRefs;
|
|
190
|
+
selfName?: () => string | undefined;
|
|
191
|
+
parked?: () => boolean;
|
|
192
|
+
lastTurnText?: () => string | undefined;
|
|
158
193
|
}): PeerSessionDrain;
|
|
159
194
|
export {};
|
|
@@ -7,6 +7,10 @@ import { isPeerSessionId, PEER_SESSION_ID_GRAMMAR, peerSessionBoxHandle } from "
|
|
|
7
7
|
import { inlineUntrusted } from "../core/untrusted-text.js";
|
|
8
8
|
import { createListAgentsTool, LIST_AGENTS_TOOL_ALIAS, LIST_AGENTS_TOOL_NAME } from "./list-agents-tool.js";
|
|
9
9
|
import { foldPermissionModeClass } from "./cross-session-judge.js";
|
|
10
|
+
import { peerHeldQueueFor } from "./peer-held-queue.js";
|
|
11
|
+
import { peerIdleMachineFor } from "./peer-idle.js";
|
|
12
|
+
import { describePeerIdleSubscription, isPeerDeliveryReceiptState, isPeerIdleNoticeKind, peerIdleNoticeLabel, renderCrossSessionDeliveryNotice, renderCrossSessionDroppedNotice, renderCrossSessionIdleNotice, resolveCrossSessionDialogExpiry } from "./peer-notices.js";
|
|
13
|
+
import { routePeerDeliveryReceipt, routePeerDroppedReceipt, routePeerIdleNotice } from "./peer-notice-route.js";
|
|
10
14
|
export const PEER_DRAIN_LEASE_TTL_MS = 5 * 60_000;
|
|
11
15
|
export const PEER_SESSION_ID_UNGRAMMATICAL_CODE = "peer.session_id_ungrammatical";
|
|
12
16
|
export function judgePeerLaneMount(deps, sessionId) {
|
|
@@ -30,17 +34,122 @@ export function judgePeerLaneMount(deps, sessionId) {
|
|
|
30
34
|
export function createPeerSessionDrain(opts) {
|
|
31
35
|
const { mailbox, sessionId, runId, scope } = opts;
|
|
32
36
|
const boxHandle = peerSessionBoxHandle(sessionId);
|
|
37
|
+
const storedSessionAddress = (fromSession) => fromSession !== undefined && isPeerSessionId(fromSession) ? peerSessionBoxHandle(fromSession) : undefined;
|
|
33
38
|
const drainOwner = `drain:${runId}`;
|
|
34
39
|
const recipientKey = peerAxisToken(scope, "s", sessionId);
|
|
35
40
|
const ttl = opts.leaseTtlMs ?? PEER_DRAIN_LEASE_TTL_MS;
|
|
36
41
|
let settledUpTo;
|
|
37
42
|
const disclosedHeld = new Set();
|
|
38
43
|
let inFlight = Promise.resolve();
|
|
39
|
-
const
|
|
44
|
+
const held = peerHeldQueueFor(scope, sessionId, opts.clock !== undefined ? { clock: opts.clock } : {});
|
|
45
|
+
const idleMachine = peerIdleMachineFor(scope, sessionId, opts.clock !== undefined ? { clock: opts.clock } : {});
|
|
46
|
+
const disclose = (disposition, cause, m, heldNote) => deliverEngineNotice(opts.onNotice, {
|
|
40
47
|
code: "peer.inbound_disposition",
|
|
41
|
-
message: `cross-session message seq ${m.seq} in this session's box was ${disposition === "held"
|
|
48
|
+
message: `cross-session message seq ${m.seq} in this session's box was ${disposition === "held"
|
|
49
|
+
? `HELD${heldNote !== undefined ? ` (${heldNote})` : " (it entered this session's held queue, acked from the box; it is re-judged at every boundary, released or refused with a receipt to the sender, and expires with a receipt under dialogExpiry)"}`
|
|
50
|
+
: disposition === "refused"
|
|
51
|
+
? "refused by this session's crossSessionInbound setting"
|
|
52
|
+
: disposition === "notice_unrouted"
|
|
53
|
+
? "settled WITHOUT delivery (a notice-kind record the notice face cannot render)"
|
|
54
|
+
: disposition === "notice_user_only"
|
|
55
|
+
? "surfaced to the user only (a notice whose sender this session's parity would hold; the model did not read it)"
|
|
56
|
+
: "refused by the inbound admission gate"}: ${cause}`,
|
|
42
57
|
detail: { disposition, cause, seq: m.seq, box: boxHandle, ...(m.peerMeta?.fromSession !== undefined ? { fromSession: m.peerMeta.fromSession } : {}), sessionId, runId },
|
|
43
58
|
});
|
|
59
|
+
const readFacts = () => ({
|
|
60
|
+
setting: resolveCrossSessionInboundSetting(opts.settingLayers() ?? {}),
|
|
61
|
+
selfModeClass: opts.selfModeClass(),
|
|
62
|
+
});
|
|
63
|
+
const judgeMeta = (facts, meta) => judgeCrossSessionInbound({ setting: facts.setting, selfModeClass: facts.selfModeClass, sender: { ...(meta?.fromMode !== undefined ? { fromMode: meta.fromMode } : {}), selfSent: meta?.fromSession !== undefined && meta.fromSession.toLowerCase() === sessionId.toLowerCase() } });
|
|
64
|
+
const route = () => ({
|
|
65
|
+
mailbox,
|
|
66
|
+
scope,
|
|
67
|
+
self: { sessionId, ...(opts.selfName?.() !== undefined ? { name: opts.selfName() } : {}), modeClass: opts.selfModeClass },
|
|
68
|
+
...(opts.onError !== undefined ? { onError: opts.onError } : {}),
|
|
69
|
+
});
|
|
70
|
+
const buildPeerMessagePayload = (m) => {
|
|
71
|
+
const meta = m.peerMeta;
|
|
72
|
+
const fields = {
|
|
73
|
+
from: encodePeerAddress(storedSessionAddress(meta?.fromSession) ?? (m.from ?? "unknown")),
|
|
74
|
+
...(meta?.fromSession !== undefined ? { fromSession: meta.fromSession } : {}),
|
|
75
|
+
...(m.from !== undefined ? { fromName: m.from } : {}),
|
|
76
|
+
...(meta?.fromMode !== undefined ? { fromMode: meta.fromMode } : {}),
|
|
77
|
+
...(meta?.fromScope !== undefined ? { fromScope: meta.fromScope } : {}),
|
|
78
|
+
};
|
|
79
|
+
buildCrossSessionEnvelope(fields, m.content);
|
|
80
|
+
return {
|
|
81
|
+
task_id: boxHandle,
|
|
82
|
+
task_type: "background_agent",
|
|
83
|
+
status: "event",
|
|
84
|
+
summary: `cross-session message from ${m.from ?? fields.from}`,
|
|
85
|
+
result: m.content,
|
|
86
|
+
seq: m.seq,
|
|
87
|
+
peer: { hopChain: m.hopChain !== undefined ? [...m.hopChain] : [] },
|
|
88
|
+
crossSessionMessage: { ...fields, body: m.content },
|
|
89
|
+
_sema_provenance: { kind: "cross_session_message", from: fields.from, taskId: boxHandle, seq: m.seq, ...(meta !== undefined ? { peerMeta: { ...meta } } : {}) },
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
const buildNoticePayload = (m, kind, text) => ({
|
|
93
|
+
task_id: boxHandle,
|
|
94
|
+
task_type: "background_agent",
|
|
95
|
+
status: "event",
|
|
96
|
+
summary: kind === "delivery_notice" ? "cross-session delivery notice" : "cross-session idle notice",
|
|
97
|
+
result: text,
|
|
98
|
+
seq: m.seq,
|
|
99
|
+
crossSessionNotice: { kind, text },
|
|
100
|
+
_sema_provenance: { kind: "cross_session_notice", from: encodePeerAddress(storedSessionAddress(m.peerMeta?.fromSession) ?? (m.from ?? "unknown")), taskId: boxHandle, seq: m.seq, ...(m.peerMeta !== undefined ? { peerMeta: { ...m.peerMeta } } : {}) },
|
|
101
|
+
});
|
|
102
|
+
const receipt = (entry, state) => {
|
|
103
|
+
const to = entry.peerMeta?.fromSession;
|
|
104
|
+
if (to === undefined)
|
|
105
|
+
return;
|
|
106
|
+
if (state === "dropped")
|
|
107
|
+
void routePeerDroppedReceipt(route(), to, entry.seq);
|
|
108
|
+
else
|
|
109
|
+
void routePeerDeliveryReceipt(route(), to, state, entry.seq);
|
|
110
|
+
};
|
|
111
|
+
const heldSink = (live) => ({
|
|
112
|
+
judge: (entry) => judgeMeta(readFacts(), entry.peerMeta),
|
|
113
|
+
...(live ? { inject: (entry) => opts.inject(buildPeerMessagePayload(entry), { priority: "next" }) } : {}),
|
|
114
|
+
receipt,
|
|
115
|
+
onHeld: (entry, count, cause, reannounce) => {
|
|
116
|
+
if (!reannounce && disclosedHeld.has(entry.seq))
|
|
117
|
+
return;
|
|
118
|
+
disclosedHeld.add(entry.seq);
|
|
119
|
+
disclose("held", cause, entry, reannounce ? `still held; its cause moved to ${cause} (${count} held)` : undefined);
|
|
120
|
+
},
|
|
121
|
+
onSettled: (entry, settlement, reason, count) => deliverEngineNotice(opts.onNotice, {
|
|
122
|
+
code: "peer.held_settled",
|
|
123
|
+
message: `held cross-session message seq ${entry.seq} left this session's held queue: ${settlement} (${reason}); ${count} still held${settlement === "expired" && reason === "evicted" ? " — the held queue was full, so the oldest entry was expired to make room" : ""}. The sender ${entry.peerMeta?.fromSession !== undefined ? "was sent a receipt" : "has no reply address and was not told"}.`,
|
|
124
|
+
detail: { settlement, reason, seq: entry.seq, box: boxHandle, ...(entry.peerMeta?.fromSession !== undefined ? { fromSession: entry.peerMeta.fromSession } : {}), heldCount: count, sessionId, runId },
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
let dialogExpiryMs = resolveCrossSessionDialogExpiry(undefined).ms;
|
|
128
|
+
let dialogExpiryAnnounced = false;
|
|
129
|
+
const bindHeld = (live) => {
|
|
130
|
+
try {
|
|
131
|
+
const resolved = resolveCrossSessionDialogExpiry(opts.dialogExpiry?.());
|
|
132
|
+
dialogExpiryMs = resolved.ms;
|
|
133
|
+
if (resolved.invalid && !dialogExpiryAnnounced) {
|
|
134
|
+
dialogExpiryAnnounced = true;
|
|
135
|
+
opts.onError?.(new Error(`crossSessionDialogExpiry holds a value outside its vocabulary (60s|5m|10m|never); the default ${resolved.value} is in force for held cross-session messages`), { phase: "degraded", sessionId, classification: "peer-dialog-expiry" });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-dialog-expiry" });
|
|
140
|
+
}
|
|
141
|
+
held.configure({ sink: heldSink(live), dialogExpiryMs });
|
|
142
|
+
};
|
|
143
|
+
bindHeld(true);
|
|
144
|
+
idleMachine.target.configure({
|
|
145
|
+
notify: (sub, notice) => void routePeerIdleNotice(route(), sub.requester, notice),
|
|
146
|
+
judge: (sub) => judgeMeta(readFacts(), { ...(sub.fromMode !== undefined ? { fromMode: sub.fromMode } : {}), ...(sub.selfSent ? { fromSession: sessionId } : {}) }),
|
|
147
|
+
heldCount: () => held.size(),
|
|
148
|
+
...(opts.lastTurnText !== undefined ? { lastTurnText: opts.lastTurnText } : {}),
|
|
149
|
+
});
|
|
150
|
+
idleMachine.requester.configure({
|
|
151
|
+
expired: (o) => void routePeerIdleNotice(route(), sessionId, { kind: "expired", label: o.label }),
|
|
152
|
+
});
|
|
44
153
|
const ackSettled = async () => {
|
|
45
154
|
if (settledUpTo === undefined)
|
|
46
155
|
return;
|
|
@@ -53,7 +162,76 @@ export function createPeerSessionDrain(opts) {
|
|
|
53
162
|
opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-ack" });
|
|
54
163
|
}
|
|
55
164
|
};
|
|
165
|
+
const settleNoticeRecord = (facts, m, meta) => {
|
|
166
|
+
const notice = meta.notice;
|
|
167
|
+
const kind = meta.kind;
|
|
168
|
+
const stateFits = notice !== undefined && (kind === "idle_notice" ? isPeerIdleNoticeKind(notice.state) : isPeerDeliveryReceiptState(notice.state));
|
|
169
|
+
if (!stateFits) {
|
|
170
|
+
disclose("notice_unrouted", `kind=${kind}${notice === undefined ? " without a typed notice" : ` with state=${inlineUntrusted(notice.state, 32)}`}`, m);
|
|
171
|
+
settledUpTo = m.seq;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
let text;
|
|
175
|
+
if (kind === "idle_notice") {
|
|
176
|
+
const idleKind = notice.state;
|
|
177
|
+
const from = meta.fromSession;
|
|
178
|
+
const selfAuthored = from !== undefined && from.toLowerCase() === sessionId.toLowerCase();
|
|
179
|
+
const settled = from !== undefined && !selfAuthored ? idleMachine.requester.settle(from, idleKind) : { cleared: 0 };
|
|
180
|
+
const label = (notice.target !== undefined ? peerIdleNoticeLabel(notice.target) : undefined) ?? settled.label ?? peerIdleNoticeLabel(m.from ?? storedSessionAddress(from) ?? "") ?? "(unnamed session)";
|
|
181
|
+
text = renderCrossSessionIdleNotice({ kind: idleKind, label, ...(notice.finishedAt !== undefined ? { finishedAt: notice.finishedAt } : {}), ...(notice.detail !== undefined ? { detail: peerIdleNoticeLabel(notice.detail) ?? "" } : {}) });
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
const state = notice.state;
|
|
185
|
+
const recipients = [notice.recipient ?? m.from ?? ""];
|
|
186
|
+
text = state === "dropped" ? renderCrossSessionDroppedNotice(1, recipients, [notice.dropReason]) : renderCrossSessionDeliveryNotice(state, 1, recipients);
|
|
187
|
+
}
|
|
188
|
+
const judged = judgeMeta(facts, meta);
|
|
189
|
+
if (judged.verdict === "refuse") {
|
|
190
|
+
disclose("refused", `${judged.cause} (a ${kind})`, m);
|
|
191
|
+
}
|
|
192
|
+
else if (judged.verdict === "hold") {
|
|
193
|
+
disclose("notice_user_only", `${judged.cause}: ${text}`, m);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
opts.inject(buildNoticePayload(m, kind, text), { priority: "next" });
|
|
197
|
+
}
|
|
198
|
+
settledUpTo = m.seq;
|
|
199
|
+
};
|
|
200
|
+
const settleSubscription = (facts, m, meta) => {
|
|
201
|
+
const requester = meta.fromSession;
|
|
202
|
+
if (requester === undefined) {
|
|
203
|
+
disclose("notice_unrouted", "kind=idle_subscription without a reply address (fromSession)", m);
|
|
204
|
+
settledUpTo = m.seq;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const sub = { requester, ...(m.from !== undefined ? { requesterName: m.from } : {}), ...(meta.fromMode !== undefined ? { fromMode: meta.fromMode } : {}), selfSent: requester.toLowerCase() === sessionId.toLowerCase(), requestedAt: m.sentAt };
|
|
208
|
+
const announce = (outcome, live) => deliverEngineNotice(opts.onNotice, {
|
|
209
|
+
code: "peer.idle_subscription",
|
|
210
|
+
message: outcome === "recorded" || outcome === "refreshed" ? `${describePeerIdleSubscription(encodePeerAddress(peerSessionBoxHandle(requester)))} (${outcome}; ${live} live)` : `A peer session at ${encodePeerAddress(peerSessionBoxHandle(requester))} asked to be told when this session is next idle and was answered "unavailable": ${outcome === "full" ? "this session's subscription table is full" : `this session's crossSessionInbound setting is "refuse"`}.`,
|
|
211
|
+
detail: { outcome, fromSession: requester, live, seq: m.seq, box: boxHandle, sessionId, runId },
|
|
212
|
+
});
|
|
213
|
+
if (facts.setting.value === "refuse") {
|
|
214
|
+
void routePeerIdleNotice(route(), requester, { kind: "unavailable" });
|
|
215
|
+
announce("refused", idleMachine.target.size());
|
|
216
|
+
settledUpTo = m.seq;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const outcome = idleMachine.target.subscribe(sub);
|
|
220
|
+
if (outcome === "full")
|
|
221
|
+
void routePeerIdleNotice(route(), requester, { kind: "unavailable" });
|
|
222
|
+
announce(outcome, idleMachine.target.size());
|
|
223
|
+
settledUpTo = m.seq;
|
|
224
|
+
};
|
|
225
|
+
let warmedUp = opts.warmup === undefined;
|
|
56
226
|
const drainOnce = async () => {
|
|
227
|
+
if (!warmedUp) {
|
|
228
|
+
warmedUp = true;
|
|
229
|
+
try {
|
|
230
|
+
await opts.warmup();
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
}
|
|
234
|
+
}
|
|
57
235
|
let lease;
|
|
58
236
|
try {
|
|
59
237
|
lease = await mailbox.claimLease(scope, boxHandle, drainOwner, ttl);
|
|
@@ -64,20 +242,21 @@ export function createPeerSessionDrain(opts) {
|
|
|
64
242
|
}
|
|
65
243
|
const previouslySettled = settledUpTo;
|
|
66
244
|
await ackSettled();
|
|
67
|
-
|
|
68
|
-
return;
|
|
69
|
-
let setting;
|
|
245
|
+
let facts;
|
|
70
246
|
let admissionConfig;
|
|
71
|
-
let selfModeClass;
|
|
72
247
|
try {
|
|
73
|
-
|
|
74
|
-
admissionConfig = resolvePeerAdmissionConfig(opts.admission);
|
|
75
|
-
selfModeClass = opts.selfModeClass();
|
|
248
|
+
facts = readFacts();
|
|
249
|
+
admissionConfig = resolvePeerAdmissionConfig(opts.admission, opts.onNotice);
|
|
76
250
|
}
|
|
77
251
|
catch (e) {
|
|
78
252
|
opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-settings" });
|
|
79
253
|
return;
|
|
80
254
|
}
|
|
255
|
+
bindHeld(true);
|
|
256
|
+
idleMachine.target.sweep();
|
|
257
|
+
held.rejudge();
|
|
258
|
+
if (lease === null)
|
|
259
|
+
return;
|
|
81
260
|
for (const m of lease.messages) {
|
|
82
261
|
if (previouslySettled !== undefined && m.seq <= previouslySettled)
|
|
83
262
|
continue;
|
|
@@ -88,7 +267,7 @@ export function createPeerSessionDrain(opts) {
|
|
|
88
267
|
if (verdict.reason === "rate_limited") {
|
|
89
268
|
if (!disclosedHeld.has(m.seq)) {
|
|
90
269
|
disclosedHeld.add(m.seq);
|
|
91
|
-
disclose("held", verdict.reason, m, "the inbound admission bucket is empty
|
|
270
|
+
disclose("held", verdict.reason, m, "it stays in the box; the inbound admission bucket is empty and it is re-judged at a later boundary once the bucket refills");
|
|
92
271
|
}
|
|
93
272
|
break;
|
|
94
273
|
}
|
|
@@ -97,53 +276,36 @@ export function createPeerSessionDrain(opts) {
|
|
|
97
276
|
continue;
|
|
98
277
|
}
|
|
99
278
|
}
|
|
100
|
-
if (meta?.kind
|
|
101
|
-
|
|
102
|
-
|
|
279
|
+
if (meta?.kind === "idle_subscription") {
|
|
280
|
+
settleSubscription(facts, m, meta);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (meta?.kind === "idle_notice" || meta?.kind === "delivery_notice") {
|
|
284
|
+
settleNoticeRecord(facts, m, meta);
|
|
103
285
|
continue;
|
|
104
286
|
}
|
|
105
287
|
{
|
|
106
|
-
const judged =
|
|
288
|
+
const judged = judgeMeta(facts, meta);
|
|
107
289
|
if (judged.verdict === "refuse") {
|
|
108
290
|
disclose("refused", judged.cause, m);
|
|
109
291
|
settledUpTo = m.seq;
|
|
110
292
|
continue;
|
|
111
293
|
}
|
|
112
294
|
if (judged.verdict === "hold") {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
break;
|
|
295
|
+
held.hold({ seq: m.seq, box: boxHandle, content: m.content, ...(m.from !== undefined ? { from: m.from } : {}), sentAt: m.sentAt, ...(m.hopChain !== undefined ? { hopChain: [...m.hopChain] } : {}), ...(meta !== undefined ? { peerMeta: { ...meta } } : {}), cause: judged.cause });
|
|
296
|
+
settledUpTo = m.seq;
|
|
297
|
+
continue;
|
|
118
298
|
}
|
|
119
299
|
}
|
|
120
|
-
let
|
|
300
|
+
let payload;
|
|
121
301
|
try {
|
|
122
|
-
|
|
123
|
-
from: encodePeerAddress(meta?.fromSession !== undefined ? peerSessionBoxHandle(meta.fromSession) : (m.from ?? "unknown")),
|
|
124
|
-
...(meta?.fromSession !== undefined ? { fromSession: meta.fromSession } : {}),
|
|
125
|
-
...(m.from !== undefined ? { fromName: m.from } : {}),
|
|
126
|
-
...(meta?.fromMode !== undefined ? { fromMode: meta.fromMode } : {}),
|
|
127
|
-
...(meta?.fromScope !== undefined ? { fromScope: meta.fromScope } : {}),
|
|
128
|
-
};
|
|
129
|
-
buildCrossSessionEnvelope(fields, m.content);
|
|
302
|
+
payload = buildPeerMessagePayload(m);
|
|
130
303
|
}
|
|
131
304
|
catch (e) {
|
|
132
305
|
disclose("refused", `record fields cannot be rendered canonically (${e instanceof CrossSessionCodecError ? e.code : e instanceof Error ? e.message : String(e)})`, m);
|
|
133
306
|
settledUpTo = m.seq;
|
|
134
307
|
continue;
|
|
135
308
|
}
|
|
136
|
-
const payload = {
|
|
137
|
-
task_id: boxHandle,
|
|
138
|
-
task_type: "background_agent",
|
|
139
|
-
status: "event",
|
|
140
|
-
summary: `cross-session message from ${m.from ?? fields.from}`,
|
|
141
|
-
result: m.content,
|
|
142
|
-
seq: m.seq,
|
|
143
|
-
peer: { hopChain: m.hopChain !== undefined ? [...m.hopChain] : [] },
|
|
144
|
-
crossSessionMessage: { ...fields, body: m.content },
|
|
145
|
-
_sema_provenance: { kind: "cross_session_message", from: fields.from, taskId: boxHandle, seq: m.seq, ...(meta !== undefined ? { peerMeta: { ...meta } } : {}) },
|
|
146
|
-
};
|
|
147
309
|
try {
|
|
148
310
|
opts.inject(payload, { priority: "next" });
|
|
149
311
|
}
|
|
@@ -160,6 +322,7 @@ export function createPeerSessionDrain(opts) {
|
|
|
160
322
|
};
|
|
161
323
|
const finishOnce = async () => {
|
|
162
324
|
await ackSettled();
|
|
325
|
+
held.detachLeg();
|
|
163
326
|
try {
|
|
164
327
|
await mailbox.releaseLease(scope, boxHandle, drainOwner);
|
|
165
328
|
}
|
|
@@ -170,7 +333,14 @@ export function createPeerSessionDrain(opts) {
|
|
|
170
333
|
inFlight = inFlight.then(finishOnce, finishOnce);
|
|
171
334
|
return inFlight;
|
|
172
335
|
};
|
|
173
|
-
return {
|
|
336
|
+
return {
|
|
337
|
+
drain,
|
|
338
|
+
finish,
|
|
339
|
+
busy: () => idleMachine.target.busy(),
|
|
340
|
+
idle: (finishedAt) => idleMachine.target.idle(finishedAt),
|
|
341
|
+
held,
|
|
342
|
+
idleMachine,
|
|
343
|
+
};
|
|
174
344
|
}
|
|
175
345
|
export function announcePeerLaneMount(deps, leg) {
|
|
176
346
|
const verdict = judgePeerLaneMount(deps, leg.sessionId);
|
|
@@ -222,13 +392,44 @@ export function bindPeerSessionDrain(harness, opts) {
|
|
|
222
392
|
await drain.drain();
|
|
223
393
|
return undefined;
|
|
224
394
|
};
|
|
225
|
-
harness.on("before_agent_start",
|
|
395
|
+
harness.on("before_agent_start", async () => {
|
|
396
|
+
drain.busy();
|
|
397
|
+
return round();
|
|
398
|
+
});
|
|
226
399
|
harness.on("turn_boundary", round);
|
|
227
|
-
harness.subscribe((event) =>
|
|
400
|
+
harness.subscribe((event) => {
|
|
401
|
+
if (event.type === "agent_end")
|
|
402
|
+
return drain.finish().catch(() => undefined);
|
|
403
|
+
if (event.type === "settled") {
|
|
404
|
+
const nothingQueued = (event.nextTurnCount ?? 0) === 0 && event.undrainedSteerCount === undefined && event.undrainedFollowUpCount === undefined;
|
|
405
|
+
let parked = false;
|
|
406
|
+
try {
|
|
407
|
+
parked = opts.parked?.() ?? false;
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
parked = true;
|
|
411
|
+
}
|
|
412
|
+
if (nothingQueued && !parked)
|
|
413
|
+
drain.idle(Date.now());
|
|
414
|
+
}
|
|
415
|
+
return undefined;
|
|
416
|
+
});
|
|
228
417
|
return drain;
|
|
229
418
|
}
|
|
230
419
|
export function bindPeerLaneDrain(harness, args) {
|
|
231
420
|
const { deps } = args;
|
|
421
|
+
let rowName;
|
|
422
|
+
const lookupRowName = async () => {
|
|
423
|
+
if (deps.peerDirectory === undefined)
|
|
424
|
+
return;
|
|
425
|
+
try {
|
|
426
|
+
const rows = await deps.peerDirectory.listPeerSessions({ scope: args.scope });
|
|
427
|
+
rowName = rows.find((r) => r.sessionId.toLowerCase() === args.sessionId.toLowerCase())?.name;
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
rowName = undefined;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
232
433
|
return bindPeerSessionDrain(harness, {
|
|
233
434
|
mailbox: deps.mailboxStore,
|
|
234
435
|
sessionId: args.sessionId,
|
|
@@ -239,6 +440,11 @@ export function bindPeerLaneDrain(harness, args) {
|
|
|
239
440
|
ownTokens: args.ownTokens,
|
|
240
441
|
settingLayers: () => (typeof deps.crossSessionInbound === "function" ? deps.crossSessionInbound() : deps.crossSessionInbound),
|
|
241
442
|
selfModeClass: () => (args.refs.askEffective !== undefined ? foldPermissionModeClass(args.refs.askEffective) : "unknown"),
|
|
443
|
+
dialogExpiry: () => (typeof deps.crossSessionDialogExpiry === "function" ? deps.crossSessionDialogExpiry() : deps.crossSessionDialogExpiry),
|
|
444
|
+
selfName: () => rowName ?? args.selfName?.(),
|
|
445
|
+
warmup: lookupRowName,
|
|
446
|
+
...(args.parked !== undefined ? { parked: args.parked } : {}),
|
|
447
|
+
...(args.lastTurnText !== undefined ? { lastTurnText: args.lastTurnText } : {}),
|
|
242
448
|
...(deps.onNotice !== undefined ? { onNotice: deps.onNotice } : {}),
|
|
243
449
|
...(deps.onError !== undefined ? { onError: deps.onError } : {}),
|
|
244
450
|
});
|
|
@@ -137,10 +137,17 @@ export interface SendMessageToolOptions {
|
|
|
137
137
|
*/
|
|
138
138
|
enrichCtx?: ToolCtxEnricher;
|
|
139
139
|
/** design/176 — caller tuning for the always-on peer-message admission gate (per-field
|
|
140
|
-
* range-validated;
|
|
140
|
+
* range-validated; a value outside its range — or one that is not a finite number — falls back to
|
|
141
|
+
* that field's default and is ANNOUNCED once per process through {@link onNotice}; there is no off
|
|
141
142
|
* switch). The Runner mount threads {@link import("../core/types.js").RunnerDeps.peerAdmission}
|
|
142
143
|
* here. Read per call — a changed value governs the next message. */
|
|
143
144
|
admission?: Partial<PeerAdmissionConfig>;
|
|
145
|
+
/** #551 — the deployment's structured notice sink, used by this face for ONE fact: a
|
|
146
|
+
* {@link admission} field the resolver could not use falls back to its default and announces
|
|
147
|
+
* `config.peer_admission_out_of_range` through here. The Runner mount threads
|
|
148
|
+
* {@link import("../core/types.js").RunnerDeps.onNotice}; a direct mount that leaves it absent
|
|
149
|
+
* still gets the line on `console.warn` (the shared delivery throat's unwired arm). */
|
|
150
|
+
onNotice?: (notice: import("../core/types.js").EngineNotice) => void;
|
|
144
151
|
/** design/176 — the SENDING run's peer-identity carrier (trusted internals chain; ctx twin
|
|
145
152
|
* {@link import("../core/types.js").ToolExecuteContext.peerSelfRef} wins when present). Supplies
|
|
146
153
|
* the sender's bucket key and the hop token appended to every outbound chain. A direct mount
|