@nuvin/session 0.4.0-rc.1 → 0.4.0-rc.11
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/dist/chunk-4AOFJGGN.js +141 -0
- package/dist/{chunk-DTLHDYZC.js → chunk-4FXP5TSI.js} +249 -46
- package/dist/{chunk-X6DYNSV2.js → chunk-O3YF6Q77.js} +50 -1
- package/dist/client/control-center-socket.d.ts +14 -0
- package/dist/client/control-center-socket.d.ts.map +1 -0
- package/dist/client/daemon-client.d.ts.map +1 -1
- package/dist/client/data-client.d.ts +8 -1
- package/dist/client/data-client.d.ts.map +1 -1
- package/dist/client/http-data-client.d.ts.map +1 -1
- package/dist/client/index.js +270 -8
- package/dist/client/session-client.d.ts +8 -2
- package/dist/client/session-client.d.ts.map +1 -1
- package/dist/client/websocket.d.ts +1 -1
- package/dist/client/websocket.d.ts.map +1 -1
- package/dist/controller/agent-channel.d.ts +7 -2
- package/dist/controller/agent-channel.d.ts.map +1 -1
- package/dist/controller/index.js +469 -82
- package/dist/controller/session-controller.d.ts +37 -9
- package/dist/controller/session-controller.d.ts.map +1 -1
- package/dist/protocol/background-work.d.ts +3 -1
- package/dist/protocol/background-work.d.ts.map +1 -1
- package/dist/protocol/control-center.d.ts +141 -0
- package/dist/protocol/control-center.d.ts.map +1 -0
- package/dist/protocol/data-rpc.d.ts +13 -1
- package/dist/protocol/data-rpc.d.ts.map +1 -1
- package/dist/protocol/index.d.ts +2 -0
- package/dist/protocol/index.d.ts.map +1 -1
- package/dist/protocol/index.js +14 -2
- package/dist/protocol/submission-recovery.d.ts +6 -0
- package/dist/protocol/submission-recovery.d.ts.map +1 -0
- package/dist/protocol/types.d.ts +159 -17
- package/dist/protocol/types.d.ts.map +1 -1
- package/dist/protocol/version.d.ts +1 -1
- package/dist/state/approvals.d.ts.map +1 -1
- package/dist/state/index.js +11 -3
- package/dist/state/messages.d.ts +30 -2
- package/dist/state/messages.d.ts.map +1 -1
- package/dist/state/session.d.ts +4 -1
- package/dist/state/session.d.ts.map +1 -1
- package/dist/state/tool-preview.d.ts.map +1 -1
- package/dist/state/tool-preview.js +1 -45
- package/dist/test-utils/index.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-5PHEC5GZ.js +0 -85
package/dist/controller/index.js
CHANGED
|
@@ -2,7 +2,9 @@ import {
|
|
|
2
2
|
resolveGrantDir
|
|
3
3
|
} from "../chunk-UVJIG4DT.js";
|
|
4
4
|
import {
|
|
5
|
+
appendRecallEntry,
|
|
5
6
|
asJsonObject,
|
|
7
|
+
copyRecallState,
|
|
6
8
|
createMessageStateFromMessages,
|
|
7
9
|
createSessionViewState,
|
|
8
10
|
isToolAutoApprovedForMode,
|
|
@@ -11,7 +13,7 @@ import {
|
|
|
11
13
|
sanitizeCoordinatorDeliveryText,
|
|
12
14
|
scopedToolKey,
|
|
13
15
|
summarizeToolCallInput
|
|
14
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-4FXP5TSI.js";
|
|
15
17
|
|
|
16
18
|
// src/controller/agent-channel.ts
|
|
17
19
|
import { EventEmitter } from "events";
|
|
@@ -27,12 +29,22 @@ var AgentChannel = class {
|
|
|
27
29
|
decider = null;
|
|
28
30
|
questionHandler = null;
|
|
29
31
|
// ---------- Agent-side (publishers) ----------
|
|
30
|
-
publishEvent(event, scope) {
|
|
32
|
+
publishEvent(event, scope, executionId) {
|
|
31
33
|
this.emitter.emit("event", {
|
|
32
34
|
event,
|
|
35
|
+
...executionId === void 0 ? {} : { executionId },
|
|
33
36
|
scope: scope === void 0 ? void 0 : toWireSessionScope(scope)
|
|
34
37
|
});
|
|
35
38
|
}
|
|
39
|
+
publishRecallEntry(entry) {
|
|
40
|
+
this.emitter.emit("recall-entry", entry);
|
|
41
|
+
}
|
|
42
|
+
onRecallEntry(listener) {
|
|
43
|
+
this.emitter.on("recall-entry", listener);
|
|
44
|
+
return () => {
|
|
45
|
+
this.emitter.off("recall-entry", listener);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
36
48
|
publishSessionLoaded(loaded) {
|
|
37
49
|
this.emitter.emit("session-loaded", loaded);
|
|
38
50
|
}
|
|
@@ -80,8 +92,10 @@ var AgentChannel = class {
|
|
|
80
92
|
};
|
|
81
93
|
|
|
82
94
|
// src/controller/session-controller.ts
|
|
95
|
+
import { randomUUID } from "crypto";
|
|
83
96
|
import {
|
|
84
|
-
isAbortError
|
|
97
|
+
isAbortError,
|
|
98
|
+
isAgentOnlyCoordinatorNotice
|
|
85
99
|
} from "@nuvin/agent-core/shared";
|
|
86
100
|
function formatBashPolicyDenial(diagnostics) {
|
|
87
101
|
const denied = diagnostics.operations.filter((operation) => operation.decision === "deny");
|
|
@@ -94,22 +108,109 @@ function formatBashPolicyDenial(diagnostics) {
|
|
|
94
108
|
});
|
|
95
109
|
return `Bash command rejected by policy: ${details.join("; ")}.`;
|
|
96
110
|
}
|
|
111
|
+
function coordinatorInfoMeta(submission) {
|
|
112
|
+
if (submission.origin !== "coordinator") return {};
|
|
113
|
+
if (isAgentOnlyCoordinatorNotice(submission)) {
|
|
114
|
+
return {
|
|
115
|
+
deliveryKind: submission.deliveryKind,
|
|
116
|
+
transcriptVisibility: "agent-only"
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return submission.subagentReport === void 0 ? {} : { subagentReport: structuredClone(submission.subagentReport) };
|
|
120
|
+
}
|
|
97
121
|
function preservesVerbatimCoordinatorInput(submission) {
|
|
98
|
-
return submission?.origin === "coordinator" && submission.deliveryKind === "subagent-report";
|
|
122
|
+
return submission?.origin === "coordinator" && (submission.deliveryKind === "subagent-report" || isAgentOnlyCoordinatorNotice(submission));
|
|
99
123
|
}
|
|
100
124
|
var MAX_AUTO_COMPACTION_CHAIN = 10;
|
|
125
|
+
function substantiveEventActivity(event) {
|
|
126
|
+
if (event.type === "assistant_chunk")
|
|
127
|
+
return event.chunk.text?.length ? "assistant-text" : void 0;
|
|
128
|
+
if (event.type === "reasoning_chunk") return event.text.length ? "reasoning" : void 0;
|
|
129
|
+
if (event.type === "tool_call" || event.type === "tool_use_chunk" || event.type === "tool_use_message")
|
|
130
|
+
return "tool";
|
|
131
|
+
const content = event.type === "model_response" ? event.response.content : event.type === "assistant_message" || event.type === "reasoning_message" || event.type === "final_message" ? event.message.content : [];
|
|
132
|
+
for (const block of content) {
|
|
133
|
+
if (block.type === "text" && block.text.length > 0) return "assistant-text";
|
|
134
|
+
if (block.type === "tool_use") return "tool";
|
|
135
|
+
if (block.type === "anthropic_thinking" && block.thinking.length > 0 || block.type === "anthropic_redacted_thinking" && block.data.length > 0 || block.type === "openai_reasoning" && (block.text?.length || block.encryptedContent?.length || block.summary.some((part) => part.text.length > 0)))
|
|
136
|
+
return "reasoning";
|
|
137
|
+
}
|
|
138
|
+
return void 0;
|
|
139
|
+
}
|
|
101
140
|
function createSessionController(deps) {
|
|
102
141
|
const now = deps.now ?? Date.now;
|
|
103
142
|
const errorDetail = deps.errorDetail ?? ((error) => error instanceof Error ? error.message : String(error));
|
|
104
143
|
let serverCommands = deps.serverCommands ?? [];
|
|
144
|
+
let submissionRecovery = deps.submissionRecovery;
|
|
105
145
|
let compaction = deps.compaction;
|
|
106
146
|
let guardPrompt = deps.guardPrompt;
|
|
107
147
|
let autoCompactionChain = 0;
|
|
108
148
|
let state = createSessionViewState();
|
|
149
|
+
const activeHumans = [];
|
|
150
|
+
const hostHumanReservations = /* @__PURE__ */ new Set();
|
|
151
|
+
const matchesIdentity = (key) => {
|
|
152
|
+
const identity = state.recall.identity;
|
|
153
|
+
return identity !== null && identity.profile === key.identity.profile && identity.workspace === key.identity.workspace && identity.sessionId === key.identity.sessionId;
|
|
154
|
+
};
|
|
155
|
+
const inputText = (input) => typeof input === "string" ? input : typeof input.content === "string" ? input.content : (input.content ?? []).filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
156
|
+
const humanId = (submission) => submission?.origin === "user" ? submission.humanSubmission?.submissionId : void 0;
|
|
157
|
+
const admitHuman = (input, opts) => {
|
|
158
|
+
if (opts.human !== void 0 && !matchesIdentity({
|
|
159
|
+
identity: opts.human.identity,
|
|
160
|
+
submissionId: opts.human.submission.submissionId
|
|
161
|
+
}))
|
|
162
|
+
throw new Error("Human submission identity mismatch");
|
|
163
|
+
if (opts.submission !== void 0 && opts.submission.origin !== "user") {
|
|
164
|
+
return {
|
|
165
|
+
...opts,
|
|
166
|
+
...opts.human === void 0 ? {} : { human: structuredClone(opts.human) }
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const humanSubmission = structuredClone(
|
|
170
|
+
opts.human?.submission ?? (opts.submission?.origin === "user" ? opts.submission.humanSubmission : void 0) ?? {
|
|
171
|
+
submissionId: randomUUID(),
|
|
172
|
+
rawText: inputText(input)
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
if (typeof humanSubmission.submissionId !== "string" || humanSubmission.submissionId.trim().length === 0 || typeof humanSubmission.rawText !== "string" || Object.keys(humanSubmission).some((key) => key !== "submissionId" && key !== "rawText"))
|
|
176
|
+
throw new Error("Invalid human submission");
|
|
177
|
+
const previous = [
|
|
178
|
+
...state.recall.entries.map((entry) => ({
|
|
179
|
+
submissionId: entry.key.submissionId,
|
|
180
|
+
rawText: entry.rawText
|
|
181
|
+
})),
|
|
182
|
+
...activeHumans,
|
|
183
|
+
...[...hostHumanReservations].filter(
|
|
184
|
+
(entry) => matchesIdentity({
|
|
185
|
+
identity: entry.identity,
|
|
186
|
+
submissionId: entry.submission.submissionId
|
|
187
|
+
})
|
|
188
|
+
).map((entry) => entry.submission),
|
|
189
|
+
...queue.flatMap(
|
|
190
|
+
(entry) => entry.opts.submission?.origin === "user" && entry.opts.submission.humanSubmission !== void 0 ? [entry.opts.submission.humanSubmission] : []
|
|
191
|
+
)
|
|
192
|
+
].find((entry) => entry.submissionId === humanSubmission.submissionId);
|
|
193
|
+
if (previous !== void 0 && previous.rawText !== humanSubmission.rawText)
|
|
194
|
+
throw new Error("Conflicting submission raw text");
|
|
195
|
+
return {
|
|
196
|
+
...opts,
|
|
197
|
+
submission: { origin: "user", humanSubmission },
|
|
198
|
+
...opts.human === void 0 ? {} : { human: structuredClone(opts.human) }
|
|
199
|
+
};
|
|
200
|
+
};
|
|
109
201
|
let seq = 0;
|
|
110
202
|
let approvalMode = deps.approvalMode ?? "readOnly";
|
|
111
203
|
let abortController = null;
|
|
112
204
|
let activeTurnPromise = null;
|
|
205
|
+
let executionOwner = null;
|
|
206
|
+
let recoveryPromise = null;
|
|
207
|
+
const recoveryResults = /* @__PURE__ */ new Map();
|
|
208
|
+
const recoveryKey = (key) => JSON.stringify([
|
|
209
|
+
key.identity.profile,
|
|
210
|
+
key.identity.workspace,
|
|
211
|
+
key.identity.sessionId,
|
|
212
|
+
key.submissionId
|
|
213
|
+
]);
|
|
113
214
|
let closePromise = null;
|
|
114
215
|
let closing = false;
|
|
115
216
|
let detached = false;
|
|
@@ -129,7 +230,7 @@ function createSessionController(deps) {
|
|
|
129
230
|
let draining = false;
|
|
130
231
|
const emit = (body, explicitTurnId) => {
|
|
131
232
|
seq += 1;
|
|
132
|
-
const turnId = explicitTurnId ?? activeTurnId;
|
|
233
|
+
const turnId = explicitTurnId === null ? void 0 : explicitTurnId ?? activeTurnId;
|
|
133
234
|
const event = Object.assign(
|
|
134
235
|
{},
|
|
135
236
|
body,
|
|
@@ -155,26 +256,45 @@ function createSessionController(deps) {
|
|
|
155
256
|
}
|
|
156
257
|
};
|
|
157
258
|
const emitTurnStatus = () => {
|
|
259
|
+
const visible = queue.filter((q) => !isAgentOnlyCoordinatorNotice(q.opts.submission));
|
|
158
260
|
emit({
|
|
159
261
|
type: "turn-status",
|
|
160
262
|
busy: abortController !== null || hostBusyCount > 0,
|
|
161
|
-
queuedCount:
|
|
162
|
-
queued:
|
|
263
|
+
queuedCount: visible.length,
|
|
264
|
+
queued: visible.map((q) => ({
|
|
163
265
|
id: q.id,
|
|
164
266
|
displayText: q.opts.displayText,
|
|
165
267
|
...q.opts.attachmentLabels?.length ? { attachmentLabels: q.opts.attachmentLabels } : {}
|
|
166
268
|
}))
|
|
167
269
|
});
|
|
168
270
|
};
|
|
169
|
-
const unsubscribeEvents = deps.channel.onEvent(({ event, scope }) => {
|
|
271
|
+
const unsubscribeEvents = deps.channel.onEvent(({ event, scope, executionId }) => {
|
|
272
|
+
const owner = executionId === void 0 ? void 0 : executionOwner;
|
|
273
|
+
if (executionId !== void 0) {
|
|
274
|
+
if (!owner || owner.control.executionId !== executionId || owner.phase === "recovering" || owner.phase === "finished")
|
|
275
|
+
return;
|
|
276
|
+
const substantive = substantiveEventActivity(event);
|
|
277
|
+
if (substantive && !owner.control.claimActivity(substantive)) return;
|
|
278
|
+
}
|
|
170
279
|
if (event.type === "tool_call") return;
|
|
171
|
-
emit(
|
|
280
|
+
emit(
|
|
281
|
+
{ type: "agent-event", event, ...scope ? { scope } : {} },
|
|
282
|
+
executionId === void 0 ? void 0 : owner?.turnId ?? null
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
const unsubscribeRecall = deps.channel.onRecallEntry((entry) => {
|
|
286
|
+
const recall = appendRecallEntry(state.recall, entry);
|
|
287
|
+
if (recall !== state.recall) emit({ type: "recall-state", recall });
|
|
172
288
|
});
|
|
173
289
|
const unsubscribeLoaded = deps.channel.onSessionLoaded((loaded) => {
|
|
290
|
+
copyRecallState(loaded.recall);
|
|
291
|
+
if (loaded.recall.identity !== null && loaded.recall.identity.sessionId !== loaded.id)
|
|
292
|
+
throw new Error("Loaded recall identity mismatch");
|
|
174
293
|
if (persistenceInFlight.size > 0) {
|
|
175
294
|
return;
|
|
176
295
|
}
|
|
177
296
|
queue.length = 0;
|
|
297
|
+
activeHumans.length = 0;
|
|
178
298
|
alwaysApproved.clear();
|
|
179
299
|
abortController?.abort(new Error("Session loaded; the active turn was cancelled."));
|
|
180
300
|
rejectAllPending("Session loaded; pending interactions were cancelled.");
|
|
@@ -188,11 +308,15 @@ function createSessionController(deps) {
|
|
|
188
308
|
...createSessionViewState(),
|
|
189
309
|
busy: hostBusyCount > 0,
|
|
190
310
|
meta: state.meta,
|
|
191
|
-
messages: createMessageStateFromMessages(loaded.messages)
|
|
311
|
+
messages: createMessageStateFromMessages(loaded.messages),
|
|
312
|
+
recall: loaded.recall
|
|
192
313
|
}
|
|
193
314
|
});
|
|
194
315
|
});
|
|
195
316
|
deps.channel.setToolDecider((request) => {
|
|
317
|
+
if (request.executionId !== void 0 && (executionOwner?.control.executionId !== request.executionId || !executionOwner.control.claimActivity("tool"))) {
|
|
318
|
+
return { action: "reject", reason: "Originating execution is no longer active." };
|
|
319
|
+
}
|
|
196
320
|
const { toolCall, agentId, toolCallPath, dirAccess, bashPolicy } = request;
|
|
197
321
|
const scope = toolCallPath === void 0 ? void 0 : { agentId, toolCallPath: [...toolCallPath] };
|
|
198
322
|
const approvalKey = request.approvalKey ?? `${agentId}:${toolCall.name}`;
|
|
@@ -408,80 +532,150 @@ function createSessionController(deps) {
|
|
|
408
532
|
return { ok: false, message: errorDetail(error) };
|
|
409
533
|
}
|
|
410
534
|
}
|
|
535
|
+
function allowsSteering(opts) {
|
|
536
|
+
return opts.turnId === void 0 || deps.headless !== true && opts.submission?.origin === "coordinator" && opts.submission.deliveryKind === "subagent-report";
|
|
537
|
+
}
|
|
411
538
|
function startNextQueuedSubmission() {
|
|
412
539
|
if (closing || abortController || hostBusyCount > 0) return;
|
|
413
|
-
|
|
540
|
+
let next = queue.shift();
|
|
541
|
+
let dropped = false;
|
|
542
|
+
while (next?.opts.signal?.aborted) {
|
|
543
|
+
dropped = true;
|
|
544
|
+
next = queue.shift();
|
|
545
|
+
}
|
|
546
|
+
if (dropped) emitTurnStatus();
|
|
414
547
|
if (!next) return;
|
|
415
548
|
const coordinatorOwned = next.opts.submission?.origin === "coordinator";
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
type: "user-message",
|
|
420
|
-
text: next.opts.displayText,
|
|
421
|
-
...next.opts.attachmentLabels?.length ? { attachmentLabels: next.opts.attachmentLabels } : {}
|
|
422
|
-
},
|
|
423
|
-
next.opts.turnId
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
void startTrackedTurn(next.input, {
|
|
549
|
+
const turn = startTrackedTurn(next.input, {
|
|
550
|
+
echo: coordinatorOwned || next.opts.silent ? void 0 : next.opts,
|
|
551
|
+
human: next.opts.human,
|
|
427
552
|
turnId: next.opts.turnId,
|
|
428
|
-
allowSteering: next.opts
|
|
553
|
+
allowSteering: allowsSteering(next.opts),
|
|
429
554
|
submission: next.opts.submission ?? { origin: "user" },
|
|
430
555
|
...coordinatorOwned ? { coordinatorDisplayText: next.opts.displayText } : {},
|
|
431
556
|
...next.opts.signal === void 0 ? {} : { signal: next.opts.signal }
|
|
432
557
|
});
|
|
558
|
+
if (next.opts.waitFor === "acceptance") observeAcceptedTurn(turn, next.opts.turnId);
|
|
559
|
+
}
|
|
560
|
+
function sealExecution(owner) {
|
|
561
|
+
owner.phase = "finished";
|
|
562
|
+
if (owner.key && !recoveryResults.has(recoveryKey(owner.key)))
|
|
563
|
+
recoveryResults.set(
|
|
564
|
+
recoveryKey(owner.key),
|
|
565
|
+
Promise.resolve({ status: "not-retracted", target: owner.key, reason: "already-finished" })
|
|
566
|
+
);
|
|
433
567
|
}
|
|
434
568
|
function finalizeTurn(controller, reason) {
|
|
435
|
-
if (abortController !== controller) return false;
|
|
436
|
-
|
|
569
|
+
if (abortController !== controller || executionOwner?.phase === "recovering") return false;
|
|
570
|
+
const endingOwner = executionOwner;
|
|
571
|
+
if (endingOwner?.controller === controller) sealExecution(endingOwner);
|
|
437
572
|
rejectAllPending(reason);
|
|
573
|
+
if (abortController !== controller || executionOwner !== endingOwner) return false;
|
|
574
|
+
executionOwner = null;
|
|
575
|
+
abortController = null;
|
|
576
|
+
activeHumans.length = 0;
|
|
577
|
+
emit({ type: "active-human-submission", key: null });
|
|
438
578
|
emitTurnStatus();
|
|
439
579
|
return true;
|
|
440
580
|
}
|
|
441
581
|
async function runTurn(input, opts) {
|
|
442
|
-
|
|
582
|
+
if (opts.submission.origin === "user" && opts.submission.humanSubmission !== void 0)
|
|
583
|
+
activeHumans.push(opts.submission.humanSubmission);
|
|
584
|
+
const controller = opts.enclosingExecution?.controller ?? new AbortController();
|
|
585
|
+
const openingHuman = opts.human?.submission ?? (opts.submission.origin === "user" ? opts.submission.humanSubmission : void 0);
|
|
586
|
+
const identity = opts.human?.identity ?? state.recall.identity;
|
|
587
|
+
let resolveSettlement;
|
|
588
|
+
let rejectSettlement;
|
|
589
|
+
const settled = new Promise((resolve, reject) => {
|
|
590
|
+
resolveSettlement = resolve;
|
|
591
|
+
rejectSettlement = reject;
|
|
592
|
+
});
|
|
593
|
+
void settled.catch(() => {
|
|
594
|
+
});
|
|
595
|
+
const owner = {
|
|
596
|
+
controller,
|
|
597
|
+
turnId: opts.correlationTurnId ?? opts.turnId,
|
|
598
|
+
key: openingHuman && identity ? { identity: { ...identity }, submissionId: openingHuman.submissionId } : null,
|
|
599
|
+
rawText: openingHuman?.rawText ?? "",
|
|
600
|
+
phase: "eligible",
|
|
601
|
+
settled,
|
|
602
|
+
settle() {
|
|
603
|
+
if (owner.abortFailure !== void 0) rejectSettlement(owner.abortFailure);
|
|
604
|
+
else resolveSettlement();
|
|
605
|
+
},
|
|
606
|
+
control: Object.freeze({
|
|
607
|
+
executionId: randomUUID(),
|
|
608
|
+
claimActivity() {
|
|
609
|
+
if (executionOwner !== owner || owner.phase === "recovering" || owner.phase === "finished")
|
|
610
|
+
return false;
|
|
611
|
+
owner.phase = "activity-started";
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
})
|
|
615
|
+
};
|
|
616
|
+
executionOwner = owner;
|
|
443
617
|
const abortFromCaller = () => controller.abort(opts.signal?.reason);
|
|
444
618
|
if (opts.signal !== void 0) {
|
|
445
619
|
if (opts.signal.aborted) abortFromCaller();
|
|
446
620
|
else opts.signal.addEventListener("abort", abortFromCaller, { once: true });
|
|
447
621
|
}
|
|
622
|
+
const steeringClosed = () => owner.phase === "recovering" || owner.phase === "finished";
|
|
448
623
|
const drainSteering = async () => {
|
|
449
|
-
if (abortController !== controller || controller.signal.aborted
|
|
624
|
+
if (abortController !== controller || controller.signal.aborted || steeringClosed())
|
|
625
|
+
return [];
|
|
450
626
|
const barrierIndex = queue.findIndex((entry) => entry.delivery === "next-turn");
|
|
451
627
|
const steeringCount = barrierIndex === -1 ? queue.length : barrierIndex;
|
|
452
628
|
if (steeringCount === 0) return [];
|
|
453
629
|
const drained = queue.splice(0, steeringCount);
|
|
630
|
+
for (const entry of drained) {
|
|
631
|
+
if (entry.opts.submission?.origin === "user" && entry.opts.submission.humanSubmission !== void 0)
|
|
632
|
+
activeHumans.push(entry.opts.submission.humanSubmission);
|
|
633
|
+
}
|
|
454
634
|
emitTurnStatus();
|
|
455
635
|
const accepted = [];
|
|
456
636
|
for (const entry of drained) {
|
|
637
|
+
if (entry.opts.signal?.aborted) continue;
|
|
457
638
|
const guarded = await guardQueuedSteering(entry.input);
|
|
639
|
+
if (entry.opts.signal?.aborted) continue;
|
|
458
640
|
if (guarded.ok) {
|
|
459
641
|
accepted.push({
|
|
460
642
|
input: preservesVerbatimCoordinatorInput(entry.opts.submission) ? entry.input : guarded.prompt,
|
|
461
|
-
opts: entry.opts
|
|
643
|
+
opts: entry.opts,
|
|
644
|
+
queuedAt: entry.queuedAt
|
|
462
645
|
});
|
|
463
646
|
} else {
|
|
464
647
|
emit({ type: "error", message: guarded.message });
|
|
465
648
|
}
|
|
466
649
|
}
|
|
467
|
-
if (abortController !== controller || controller.signal.aborted
|
|
468
|
-
|
|
650
|
+
if (abortController !== controller || controller.signal.aborted || steeringClosed())
|
|
651
|
+
return [];
|
|
652
|
+
const current = accepted.filter((entry) => !entry.opts.signal?.aborted);
|
|
653
|
+
for (const entry of current) {
|
|
469
654
|
if (entry.opts.submission?.origin === "coordinator") {
|
|
470
655
|
emit({
|
|
471
656
|
type: "info",
|
|
472
657
|
message: sanitizeCoordinatorDeliveryText(entry.opts.displayText),
|
|
473
|
-
...
|
|
658
|
+
...coordinatorInfoMeta(entry.opts.submission)
|
|
474
659
|
});
|
|
475
660
|
} else if (!entry.opts.silent) {
|
|
476
661
|
emit({
|
|
477
662
|
type: "user-message",
|
|
478
663
|
text: entry.opts.displayText,
|
|
479
|
-
...entry.opts.
|
|
664
|
+
...humanId(entry.opts.submission) === void 0 ? {} : { submissionId: humanId(entry.opts.submission) },
|
|
665
|
+
...entry.opts.attachmentLabels?.length ? { attachmentLabels: entry.opts.attachmentLabels } : {},
|
|
666
|
+
// The drain stamps event.at at delivery; steeredAt preserves when
|
|
667
|
+
// the client actually queued the input — the HUD's "time since
|
|
668
|
+
// last input" and the persisted steer marker both key off it.
|
|
669
|
+
steeredAt: entry.queuedAt
|
|
480
670
|
});
|
|
481
671
|
}
|
|
482
672
|
}
|
|
483
|
-
return
|
|
484
|
-
(entry) =>
|
|
673
|
+
return current.map(
|
|
674
|
+
(entry) => ({
|
|
675
|
+
input: entry.input,
|
|
676
|
+
submission: entry.opts.submission ?? { origin: "user" },
|
|
677
|
+
steeredAt: entry.queuedAt
|
|
678
|
+
})
|
|
485
679
|
);
|
|
486
680
|
};
|
|
487
681
|
const previousTurnId = activeTurnId;
|
|
@@ -491,6 +685,18 @@ function createSessionController(deps) {
|
|
|
491
685
|
let terminalStatus = "completed";
|
|
492
686
|
let terminalMessage;
|
|
493
687
|
try {
|
|
688
|
+
emit({ type: "active-human-submission", key: owner.key });
|
|
689
|
+
if (opts.echo) {
|
|
690
|
+
emit(
|
|
691
|
+
{
|
|
692
|
+
type: "user-message",
|
|
693
|
+
text: opts.echo.displayText,
|
|
694
|
+
...humanId(opts.submission) === void 0 ? {} : { submissionId: humanId(opts.submission) },
|
|
695
|
+
...opts.echo.attachmentLabels?.length ? { attachmentLabels: opts.echo.attachmentLabels } : {}
|
|
696
|
+
},
|
|
697
|
+
opts.turnId
|
|
698
|
+
);
|
|
699
|
+
}
|
|
494
700
|
emitTurnStatus();
|
|
495
701
|
if (!opts.systemInitiated) autoCompactionChain = 0;
|
|
496
702
|
compaction?.setEnabled(true);
|
|
@@ -498,7 +704,15 @@ function createSessionController(deps) {
|
|
|
498
704
|
if (!opts.systemInitiated && guardPrompt) {
|
|
499
705
|
let guarded;
|
|
500
706
|
try {
|
|
707
|
+
if (opts.signal?.aborted) {
|
|
708
|
+
terminalStatus = "aborted";
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
501
711
|
guarded = await guardPrompt(input);
|
|
712
|
+
if (opts.signal?.aborted) {
|
|
713
|
+
terminalStatus = "aborted";
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
502
716
|
} catch (error) {
|
|
503
717
|
terminalStatus = "failed";
|
|
504
718
|
terminalMessage = errorDetail(error);
|
|
@@ -513,18 +727,27 @@ function createSessionController(deps) {
|
|
|
513
727
|
}
|
|
514
728
|
finalInput = preservesVerbatimCoordinatorInput(opts.submission) ? input : guarded.prompt;
|
|
515
729
|
}
|
|
730
|
+
if (opts.signal?.aborted) {
|
|
731
|
+
terminalStatus = "aborted";
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
516
734
|
if (opts.coordinatorDisplayText !== void 0) {
|
|
517
735
|
emit({
|
|
518
736
|
type: "info",
|
|
519
737
|
message: sanitizeCoordinatorDeliveryText(opts.coordinatorDisplayText),
|
|
520
|
-
...
|
|
738
|
+
...coordinatorInfoMeta(opts.submission)
|
|
521
739
|
});
|
|
522
740
|
}
|
|
523
741
|
const compactionSend = (text) => runTurn(text, {
|
|
524
742
|
systemInitiated: true,
|
|
525
743
|
transientInput: true,
|
|
744
|
+
correlationTurnId: owner.turnId,
|
|
745
|
+
enclosingExecution: owner,
|
|
526
746
|
allowSteering: opts.allowSteering,
|
|
527
|
-
submission:
|
|
747
|
+
submission: {
|
|
748
|
+
origin: "synthetic",
|
|
749
|
+
syntheticKind: "compaction"
|
|
750
|
+
},
|
|
528
751
|
...opts.signal === void 0 ? {} : { signal: opts.signal }
|
|
529
752
|
});
|
|
530
753
|
const compactionIo = {
|
|
@@ -533,8 +756,14 @@ function createSessionController(deps) {
|
|
|
533
756
|
onInfo: (message) => emit({ type: "info", message }),
|
|
534
757
|
onError: (message) => emit({ type: "error", message })
|
|
535
758
|
};
|
|
759
|
+
const sealForMaintenance = () => {
|
|
760
|
+
if (owner.phase === "recovering") return false;
|
|
761
|
+
sealExecution(owner);
|
|
762
|
+
if (executionOwner === owner) emit({ type: "active-human-submission", key: null });
|
|
763
|
+
return true;
|
|
764
|
+
};
|
|
536
765
|
const runCompaction = async () => {
|
|
537
|
-
if (!compaction) return;
|
|
766
|
+
if (!compaction || owner.phase === "recovering") return;
|
|
538
767
|
if (autoCompactionChain >= MAX_AUTO_COMPACTION_CHAIN) {
|
|
539
768
|
compaction.setEnabled(false);
|
|
540
769
|
emit({
|
|
@@ -548,16 +777,17 @@ function createSessionController(deps) {
|
|
|
548
777
|
};
|
|
549
778
|
try {
|
|
550
779
|
const result = await deps.agent.send(finalInput, {
|
|
780
|
+
execution: owner.control,
|
|
551
781
|
streaming: true,
|
|
552
782
|
signal: controller.signal,
|
|
553
783
|
submission: opts.submission,
|
|
554
784
|
...opts.transientInput === true ? { transientInput: true } : {},
|
|
555
785
|
...opts.allowSteering ? { drainSteering } : {}
|
|
556
|
-
});
|
|
786
|
+
}).finally(owner.settle);
|
|
557
787
|
if (typeof result === "object" && result !== null && "status" in result && result.status === "aborted") {
|
|
558
788
|
terminalStatus = "aborted";
|
|
559
789
|
}
|
|
560
|
-
if (compaction && !controller.signal.aborted) {
|
|
790
|
+
if (sealForMaintenance() && compaction && !controller.signal.aborted) {
|
|
561
791
|
if (compaction.consumePendingAction() === "compact") {
|
|
562
792
|
await runCompaction();
|
|
563
793
|
}
|
|
@@ -567,7 +797,7 @@ function createSessionController(deps) {
|
|
|
567
797
|
terminalStatus = "aborted";
|
|
568
798
|
}
|
|
569
799
|
try {
|
|
570
|
-
const recovered = compaction ? compaction.messagesFromError(error) : null;
|
|
800
|
+
const recovered = sealForMaintenance() && compaction && !controller.signal.aborted ? compaction.messagesFromError(error) : null;
|
|
571
801
|
if (recovered) {
|
|
572
802
|
deps.agent.messages = recovered;
|
|
573
803
|
await runCompaction();
|
|
@@ -583,67 +813,206 @@ function createSessionController(deps) {
|
|
|
583
813
|
}
|
|
584
814
|
}
|
|
585
815
|
} finally {
|
|
816
|
+
owner.settle();
|
|
586
817
|
opts.signal?.removeEventListener("abort", abortFromCaller);
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
)
|
|
591
|
-
|
|
818
|
+
const ownedExecution = executionOwner === owner && abortController === controller;
|
|
819
|
+
const policyDenial = ownedExecution ? activePolicyDenial : void 0;
|
|
820
|
+
let ownedTurn = false;
|
|
821
|
+
if (opts.enclosingExecution !== void 0) {
|
|
822
|
+
if (ownedExecution) {
|
|
823
|
+
sealExecution(owner);
|
|
824
|
+
executionOwner = opts.enclosingExecution;
|
|
825
|
+
abortController = opts.enclosingExecution.controller;
|
|
826
|
+
activeTurnId = opts.enclosingExecution.turnId;
|
|
827
|
+
}
|
|
828
|
+
} else if (ownedExecution) {
|
|
829
|
+
ownedTurn = finalizeTurn(
|
|
830
|
+
controller,
|
|
831
|
+
"Turn ended before pending interactions could be resolved."
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
if (controller.signal.aborted) terminalStatus = "aborted";
|
|
835
|
+
if (policyDenial !== void 0) {
|
|
592
836
|
terminalStatus = "failed";
|
|
593
|
-
terminalMessage =
|
|
837
|
+
terminalMessage = policyDenial;
|
|
594
838
|
}
|
|
595
839
|
if (opts.turnId !== void 0) {
|
|
596
840
|
emit(
|
|
597
841
|
{
|
|
598
842
|
type: "turn-completed",
|
|
599
843
|
status: terminalStatus,
|
|
600
|
-
...
|
|
844
|
+
...policyDenial !== void 0 ? { reason: "policy-denied" } : {},
|
|
601
845
|
...terminalMessage !== void 0 ? { message: terminalMessage } : {}
|
|
602
846
|
},
|
|
603
847
|
opts.turnId
|
|
604
848
|
);
|
|
605
849
|
}
|
|
606
|
-
if (
|
|
607
|
-
|
|
850
|
+
if (opts.enclosingExecution === void 0 && (executionOwner === owner || ownedTurn && executionOwner === null)) {
|
|
851
|
+
if (!opts.systemInitiated) activePolicyDenial = void 0;
|
|
852
|
+
activeTurnId = previousTurnId;
|
|
853
|
+
}
|
|
608
854
|
if (ownedTurn) startNextQueuedSubmission();
|
|
609
855
|
}
|
|
610
856
|
}
|
|
611
857
|
function startTrackedTurn(input, opts) {
|
|
612
|
-
let
|
|
613
|
-
|
|
614
|
-
|
|
858
|
+
let resolve;
|
|
859
|
+
let reject;
|
|
860
|
+
const tracked = new Promise((done, fail) => {
|
|
861
|
+
resolve = done;
|
|
862
|
+
reject = fail;
|
|
615
863
|
});
|
|
616
864
|
activeTurnPromise = tracked;
|
|
865
|
+
void runTurn(input, opts).then(resolve, reject).finally(() => {
|
|
866
|
+
if (activeTurnPromise === tracked) activeTurnPromise = null;
|
|
867
|
+
});
|
|
617
868
|
return tracked;
|
|
618
869
|
}
|
|
870
|
+
function observeAcceptedTurn(turn, turnId) {
|
|
871
|
+
void turn.catch((error) => {
|
|
872
|
+
emit({ type: "error", message: errorDetail(error) }, turnId);
|
|
873
|
+
});
|
|
874
|
+
}
|
|
619
875
|
function detachController() {
|
|
620
876
|
if (detached) return;
|
|
621
877
|
detached = true;
|
|
622
878
|
unsubscribeEvents();
|
|
623
879
|
unsubscribeLoaded();
|
|
880
|
+
unsubscribeRecall();
|
|
624
881
|
deps.channel.setToolDecider(null);
|
|
625
882
|
deps.channel.setQuestionHandler(null);
|
|
626
883
|
listeners.clear();
|
|
627
884
|
abortListeners.clear();
|
|
628
885
|
}
|
|
886
|
+
function cancelRecoveryOwner(owner) {
|
|
887
|
+
queue.length = 0;
|
|
888
|
+
const reason = new Error("User aborted the current turn.");
|
|
889
|
+
for (const listener of abortListeners) {
|
|
890
|
+
try {
|
|
891
|
+
listener(reason, "cancel");
|
|
892
|
+
} catch (error) {
|
|
893
|
+
owner.abortFailure = new AggregateError(
|
|
894
|
+
[
|
|
895
|
+
...owner.abortFailure instanceof AggregateError ? owner.abortFailure.errors : [],
|
|
896
|
+
error
|
|
897
|
+
],
|
|
898
|
+
"Interactive cancellation failed"
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
owner.controller.abort(reason);
|
|
903
|
+
rejectAllPending(reason.message);
|
|
904
|
+
}
|
|
905
|
+
function abort(intent = "cancel") {
|
|
906
|
+
const reason = new Error("User aborted the current turn.");
|
|
907
|
+
for (const listener of abortListeners) {
|
|
908
|
+
try {
|
|
909
|
+
listener(reason, intent);
|
|
910
|
+
} catch {
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
queue.length = 0;
|
|
914
|
+
abortController?.abort(reason);
|
|
915
|
+
rejectAllPending("User aborted the current turn.");
|
|
916
|
+
}
|
|
629
917
|
return {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
918
|
+
abortWithRecovery(request) {
|
|
919
|
+
request = structuredClone(request);
|
|
920
|
+
const known = request.target === null ? void 0 : recoveryResults.get(recoveryKey(request.target));
|
|
921
|
+
if (known) return known;
|
|
922
|
+
const owner = executionOwner;
|
|
923
|
+
if (request.target === null && (owner?.key == null || owner.phase === "finished")) {
|
|
924
|
+
abort();
|
|
925
|
+
return Promise.resolve({
|
|
926
|
+
status: "not-retracted",
|
|
927
|
+
target: null,
|
|
928
|
+
reason: "no-active-human"
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
if (!owner?.key || !request.target || !matchesIdentity(request.target) || recoveryKey(owner.key) !== recoveryKey(request.target)) {
|
|
932
|
+
return Promise.resolve({
|
|
933
|
+
status: "not-retracted",
|
|
934
|
+
target: request.target,
|
|
935
|
+
reason: "target-mismatch"
|
|
936
|
+
});
|
|
638
937
|
}
|
|
639
|
-
|
|
640
|
-
|
|
938
|
+
if (owner.phase !== "eligible") {
|
|
939
|
+
const result = Promise.resolve({
|
|
940
|
+
status: "not-retracted",
|
|
941
|
+
target: request.target,
|
|
942
|
+
reason: owner.phase === "finished" ? "already-finished" : "activity-started"
|
|
943
|
+
});
|
|
944
|
+
recoveryResults.set(recoveryKey(owner.key), result);
|
|
945
|
+
if (owner.phase === "activity-started") abort();
|
|
946
|
+
return result;
|
|
947
|
+
}
|
|
948
|
+
owner.phase = "recovering";
|
|
949
|
+
let resolve;
|
|
950
|
+
const pending = new Promise((done) => {
|
|
951
|
+
resolve = done;
|
|
952
|
+
});
|
|
953
|
+
recoveryResults.set(recoveryKey(owner.key), pending);
|
|
954
|
+
recoveryPromise = pending;
|
|
955
|
+
const persistenceAvailable = submissionRecovery !== void 0;
|
|
956
|
+
let work;
|
|
957
|
+
try {
|
|
958
|
+
if (!submissionRecovery) throw new Error("Submission recovery persistence is unavailable");
|
|
959
|
+
work = submissionRecovery.retract(
|
|
960
|
+
{ key: owner.key, rawText: owner.rawText, executionId: owner.control.executionId },
|
|
961
|
+
owner.settled
|
|
962
|
+
);
|
|
963
|
+
} catch (error) {
|
|
964
|
+
work = Promise.reject(error);
|
|
965
|
+
}
|
|
966
|
+
cancelRecoveryOwner(owner);
|
|
967
|
+
const key = owner.key;
|
|
968
|
+
void work.then(
|
|
969
|
+
async () => {
|
|
970
|
+
await owner.settled;
|
|
971
|
+
const result = { status: "retracted", key, rawText: owner.rawText };
|
|
972
|
+
const removedMessageIds = state.messages.messages.filter((row) => row.role === "user" && row.submissionId === key.submissionId).map((row) => row.id);
|
|
973
|
+
emit({ type: "submission-retracted", result, removedMessageIds });
|
|
974
|
+
return result;
|
|
975
|
+
},
|
|
976
|
+
async (error) => {
|
|
977
|
+
await owner.settled.catch(() => {
|
|
978
|
+
});
|
|
979
|
+
return {
|
|
980
|
+
status: "failed",
|
|
981
|
+
target: key,
|
|
982
|
+
code: owner.abortFailure !== void 0 ? "abort-failed" : persistenceAvailable ? "persistence-failed" : "persistence-unavailable",
|
|
983
|
+
message: errorDetail(owner.abortFailure ?? error)
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
).catch(
|
|
987
|
+
(error) => ({
|
|
988
|
+
status: "failed",
|
|
989
|
+
target: key,
|
|
990
|
+
code: owner.abortFailure !== void 0 ? "abort-failed" : "persistence-failed",
|
|
991
|
+
message: error instanceof Error ? error.message : "Recovery publication failed"
|
|
992
|
+
})
|
|
993
|
+
).finally(() => {
|
|
994
|
+
owner.phase = "finished";
|
|
995
|
+
finalizeTurn(owner.controller, "Recovery ended.");
|
|
996
|
+
if (recoveryPromise === pending) recoveryPromise = null;
|
|
997
|
+
startNextQueuedSubmission();
|
|
998
|
+
}).then(
|
|
999
|
+
resolve,
|
|
1000
|
+
(error) => resolve({
|
|
1001
|
+
status: "failed",
|
|
1002
|
+
target: key,
|
|
1003
|
+
code: "persistence-failed",
|
|
1004
|
+
message: error instanceof Error ? error.message : "Recovery finalization failed"
|
|
1005
|
+
})
|
|
1006
|
+
);
|
|
1007
|
+
return pending;
|
|
641
1008
|
},
|
|
1009
|
+
abort,
|
|
642
1010
|
onAbort(listener) {
|
|
643
1011
|
abortListeners.add(listener);
|
|
644
1012
|
return () => abortListeners.delete(listener);
|
|
645
1013
|
},
|
|
646
1014
|
attachHostHooks(hooks) {
|
|
1015
|
+
if (hooks.submissionRecovery) submissionRecovery = hooks.submissionRecovery;
|
|
647
1016
|
if (hooks.compaction) compaction = hooks.compaction;
|
|
648
1017
|
if (hooks.guardPrompt) guardPrompt = hooks.guardPrompt;
|
|
649
1018
|
},
|
|
@@ -666,8 +1035,10 @@ function createSessionController(deps) {
|
|
|
666
1035
|
appendInfo(message) {
|
|
667
1036
|
emit({ type: "info", message });
|
|
668
1037
|
},
|
|
669
|
-
commitCompaction(messages) {
|
|
1038
|
+
commitCompaction({ messages, recall }) {
|
|
1039
|
+
copyRecallState(recall);
|
|
670
1040
|
queue.length = 0;
|
|
1041
|
+
activeHumans.length = 0;
|
|
671
1042
|
rejectAllPending("Session compacted; pending interactions were cancelled.");
|
|
672
1043
|
alwaysApproved.clear();
|
|
673
1044
|
emit({
|
|
@@ -677,7 +1048,8 @@ function createSessionController(deps) {
|
|
|
677
1048
|
busy: abortController !== null || hostBusyCount > 0,
|
|
678
1049
|
meta: state.meta,
|
|
679
1050
|
mcp: state.mcp,
|
|
680
|
-
messages: createMessageStateFromMessages([...messages])
|
|
1051
|
+
messages: createMessageStateFromMessages([...messages]),
|
|
1052
|
+
recall
|
|
681
1053
|
}
|
|
682
1054
|
});
|
|
683
1055
|
},
|
|
@@ -690,12 +1062,13 @@ function createSessionController(deps) {
|
|
|
690
1062
|
rejectAllPending("Session closed.");
|
|
691
1063
|
if (shouldEmitTurnStatus) emitTurnStatus();
|
|
692
1064
|
const active = activeTurnPromise;
|
|
693
|
-
|
|
1065
|
+
const recovery = recoveryPromise;
|
|
1066
|
+
if (!active && !recovery) {
|
|
694
1067
|
detachController();
|
|
695
1068
|
closePromise = Promise.resolve();
|
|
696
1069
|
return closePromise;
|
|
697
1070
|
}
|
|
698
|
-
closePromise = active.then(detachController
|
|
1071
|
+
closePromise = Promise.allSettled([active, recovery]).then(detachController);
|
|
699
1072
|
return closePromise;
|
|
700
1073
|
},
|
|
701
1074
|
dequeue(queuedId) {
|
|
@@ -821,12 +1194,27 @@ function createSessionController(deps) {
|
|
|
821
1194
|
emitTurnStatus();
|
|
822
1195
|
}
|
|
823
1196
|
},
|
|
824
|
-
|
|
1197
|
+
reserveHumanSubmission(human) {
|
|
1198
|
+
admitHuman("", { displayText: "", human });
|
|
1199
|
+
const captured = structuredClone(human);
|
|
1200
|
+
hostHumanReservations.add(captured);
|
|
1201
|
+
return () => {
|
|
1202
|
+
hostHumanReservations.delete(captured);
|
|
1203
|
+
};
|
|
1204
|
+
},
|
|
1205
|
+
echoUserMessage(text, attachmentLabels, key) {
|
|
1206
|
+
if (key !== void 0 && !matchesIdentity(key)) return false;
|
|
825
1207
|
emit({
|
|
826
1208
|
type: "user-message",
|
|
827
1209
|
text,
|
|
828
|
-
...
|
|
1210
|
+
...key === void 0 ? {} : { submissionId: key.submissionId },
|
|
1211
|
+
...attachmentLabels?.length ? { attachmentLabels } : {},
|
|
1212
|
+
// An echo emitted while a turn is in flight is a display-only
|
|
1213
|
+
// interjection, never a turn opener — mark it like a drained steer so
|
|
1214
|
+
// the WorkingStatusLine anchor and the persisted row both skip it.
|
|
1215
|
+
...abortController !== null || hostBusyCount > 0 ? { steeredAt: now() } : {}
|
|
829
1216
|
});
|
|
1217
|
+
return true;
|
|
830
1218
|
},
|
|
831
1219
|
getSnapshot() {
|
|
832
1220
|
return { seq, state, serverCommands };
|
|
@@ -872,11 +1260,13 @@ function createSessionController(deps) {
|
|
|
872
1260
|
publishMcpState(mcpState) {
|
|
873
1261
|
emit({ type: "mcp-state", state: mcpState });
|
|
874
1262
|
},
|
|
875
|
-
reset(options
|
|
1263
|
+
reset(options) {
|
|
1264
|
+
copyRecallState(options.recall);
|
|
876
1265
|
if (persistenceInFlight.size > 0) {
|
|
877
1266
|
return;
|
|
878
1267
|
}
|
|
879
1268
|
queue.length = 0;
|
|
1269
|
+
activeHumans.length = 0;
|
|
880
1270
|
if (options.preserveHostBusy !== true) hostBusyCount = 0;
|
|
881
1271
|
abortController?.abort(new Error("Session reset."));
|
|
882
1272
|
rejectAllPending("Session reset; pending interactions were cancelled.");
|
|
@@ -886,6 +1276,7 @@ function createSessionController(deps) {
|
|
|
886
1276
|
type: "state-reset",
|
|
887
1277
|
state: {
|
|
888
1278
|
...createSessionViewState(),
|
|
1279
|
+
recall: options.recall,
|
|
889
1280
|
busy: hostBusyCount > 0,
|
|
890
1281
|
meta: state.meta,
|
|
891
1282
|
mcp: state.mcp
|
|
@@ -907,34 +1298,30 @@ function createSessionController(deps) {
|
|
|
907
1298
|
async submit(input, opts) {
|
|
908
1299
|
if (closing) throw new Error("Session is closing.");
|
|
909
1300
|
opts.signal?.throwIfAborted();
|
|
1301
|
+
opts = admitHuman(input, opts);
|
|
910
1302
|
if (abortController || hostBusyCount > 0) {
|
|
911
1303
|
queue.push({
|
|
912
1304
|
id: `q-${++queueCounter}`,
|
|
913
1305
|
input,
|
|
914
1306
|
opts,
|
|
915
|
-
delivery: abortController !== null &&
|
|
1307
|
+
delivery: abortController !== null && executionOwner?.phase !== "recovering" && allowsSteering(opts) ? "steering" : "next-turn",
|
|
1308
|
+
queuedAt: now()
|
|
916
1309
|
});
|
|
917
1310
|
emitTurnStatus();
|
|
918
1311
|
return;
|
|
919
1312
|
}
|
|
920
1313
|
const coordinatorOwned = opts.submission?.origin === "coordinator";
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
type: "user-message",
|
|
925
|
-
text: opts.displayText,
|
|
926
|
-
...opts.attachmentLabels?.length ? { attachmentLabels: opts.attachmentLabels } : {}
|
|
927
|
-
},
|
|
928
|
-
opts.turnId
|
|
929
|
-
);
|
|
930
|
-
}
|
|
931
|
-
await startTrackedTurn(input, {
|
|
1314
|
+
const turn = startTrackedTurn(input, {
|
|
1315
|
+
echo: coordinatorOwned || opts.silent ? void 0 : opts,
|
|
1316
|
+
human: opts.human,
|
|
932
1317
|
turnId: opts.turnId,
|
|
933
|
-
allowSteering: opts
|
|
1318
|
+
allowSteering: allowsSteering(opts),
|
|
934
1319
|
submission: opts.submission ?? { origin: "user" },
|
|
935
1320
|
...coordinatorOwned ? { coordinatorDisplayText: opts.displayText } : {},
|
|
936
1321
|
...opts.signal === void 0 ? {} : { signal: opts.signal }
|
|
937
1322
|
});
|
|
1323
|
+
if (opts.waitFor === "acceptance") observeAcceptedTurn(turn, opts.turnId);
|
|
1324
|
+
else await turn;
|
|
938
1325
|
}
|
|
939
1326
|
};
|
|
940
1327
|
}
|