@nuvin/session 0.4.0-rc.10 → 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-U6V6IQJR.js → chunk-4FXP5TSI.js} +68 -4
- package/dist/{chunk-Y7JPSYTH.js → chunk-O3YF6Q77.js} +43 -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 +2 -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 +209 -5
- package/dist/client/session-client.d.ts +4 -2
- package/dist/client/session-client.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 +394 -65
- package/dist/controller/session-controller.d.ts +25 -5
- package/dist/controller/session-controller.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 +5 -0
- 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 +12 -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 +71 -6
- package/dist/protocol/types.d.ts.map +1 -1
- package/dist/protocol/version.d.ts +1 -1
- package/dist/state/index.js +5 -1
- package/dist/state/messages.d.ts +2 -1
- 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/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,6 +92,7 @@ var AgentChannel = class {
|
|
|
80
92
|
};
|
|
81
93
|
|
|
82
94
|
// src/controller/session-controller.ts
|
|
95
|
+
import { randomUUID } from "crypto";
|
|
83
96
|
import {
|
|
84
97
|
isAbortError,
|
|
85
98
|
isAgentOnlyCoordinatorNotice
|
|
@@ -109,18 +122,95 @@ function preservesVerbatimCoordinatorInput(submission) {
|
|
|
109
122
|
return submission?.origin === "coordinator" && (submission.deliveryKind === "subagent-report" || isAgentOnlyCoordinatorNotice(submission));
|
|
110
123
|
}
|
|
111
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
|
+
}
|
|
112
140
|
function createSessionController(deps) {
|
|
113
141
|
const now = deps.now ?? Date.now;
|
|
114
142
|
const errorDetail = deps.errorDetail ?? ((error) => error instanceof Error ? error.message : String(error));
|
|
115
143
|
let serverCommands = deps.serverCommands ?? [];
|
|
144
|
+
let submissionRecovery = deps.submissionRecovery;
|
|
116
145
|
let compaction = deps.compaction;
|
|
117
146
|
let guardPrompt = deps.guardPrompt;
|
|
118
147
|
let autoCompactionChain = 0;
|
|
119
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
|
+
};
|
|
120
201
|
let seq = 0;
|
|
121
202
|
let approvalMode = deps.approvalMode ?? "readOnly";
|
|
122
203
|
let abortController = null;
|
|
123
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
|
+
]);
|
|
124
214
|
let closePromise = null;
|
|
125
215
|
let closing = false;
|
|
126
216
|
let detached = false;
|
|
@@ -140,7 +230,7 @@ function createSessionController(deps) {
|
|
|
140
230
|
let draining = false;
|
|
141
231
|
const emit = (body, explicitTurnId) => {
|
|
142
232
|
seq += 1;
|
|
143
|
-
const turnId = explicitTurnId ?? activeTurnId;
|
|
233
|
+
const turnId = explicitTurnId === null ? void 0 : explicitTurnId ?? activeTurnId;
|
|
144
234
|
const event = Object.assign(
|
|
145
235
|
{},
|
|
146
236
|
body,
|
|
@@ -178,15 +268,33 @@ function createSessionController(deps) {
|
|
|
178
268
|
}))
|
|
179
269
|
});
|
|
180
270
|
};
|
|
181
|
-
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
|
+
}
|
|
182
279
|
if (event.type === "tool_call") return;
|
|
183
|
-
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 });
|
|
184
288
|
});
|
|
185
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");
|
|
186
293
|
if (persistenceInFlight.size > 0) {
|
|
187
294
|
return;
|
|
188
295
|
}
|
|
189
296
|
queue.length = 0;
|
|
297
|
+
activeHumans.length = 0;
|
|
190
298
|
alwaysApproved.clear();
|
|
191
299
|
abortController?.abort(new Error("Session loaded; the active turn was cancelled."));
|
|
192
300
|
rejectAllPending("Session loaded; pending interactions were cancelled.");
|
|
@@ -200,11 +308,15 @@ function createSessionController(deps) {
|
|
|
200
308
|
...createSessionViewState(),
|
|
201
309
|
busy: hostBusyCount > 0,
|
|
202
310
|
meta: state.meta,
|
|
203
|
-
messages: createMessageStateFromMessages(loaded.messages)
|
|
311
|
+
messages: createMessageStateFromMessages(loaded.messages),
|
|
312
|
+
recall: loaded.recall
|
|
204
313
|
}
|
|
205
314
|
});
|
|
206
315
|
});
|
|
207
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
|
+
}
|
|
208
320
|
const { toolCall, agentId, toolCallPath, dirAccess, bashPolicy } = request;
|
|
209
321
|
const scope = toolCallPath === void 0 ? void 0 : { agentId, toolCallPath: [...toolCallPath] };
|
|
210
322
|
const approvalKey = request.approvalKey ?? `${agentId}:${toolCall.name}`;
|
|
@@ -434,17 +546,9 @@ function createSessionController(deps) {
|
|
|
434
546
|
if (dropped) emitTurnStatus();
|
|
435
547
|
if (!next) return;
|
|
436
548
|
const coordinatorOwned = next.opts.submission?.origin === "coordinator";
|
|
437
|
-
if (!coordinatorOwned && !next.opts.silent) {
|
|
438
|
-
emit(
|
|
439
|
-
{
|
|
440
|
-
type: "user-message",
|
|
441
|
-
text: next.opts.displayText,
|
|
442
|
-
...next.opts.attachmentLabels?.length ? { attachmentLabels: next.opts.attachmentLabels } : {}
|
|
443
|
-
},
|
|
444
|
-
next.opts.turnId
|
|
445
|
-
);
|
|
446
|
-
}
|
|
447
549
|
const turn = startTrackedTurn(next.input, {
|
|
550
|
+
echo: coordinatorOwned || next.opts.silent ? void 0 : next.opts,
|
|
551
|
+
human: next.opts.human,
|
|
448
552
|
turnId: next.opts.turnId,
|
|
449
553
|
allowSteering: allowsSteering(next.opts),
|
|
450
554
|
submission: next.opts.submission ?? { origin: "user" },
|
|
@@ -453,26 +557,80 @@ function createSessionController(deps) {
|
|
|
453
557
|
});
|
|
454
558
|
if (next.opts.waitFor === "acceptance") observeAcceptedTurn(turn, next.opts.turnId);
|
|
455
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
|
+
);
|
|
567
|
+
}
|
|
456
568
|
function finalizeTurn(controller, reason) {
|
|
457
|
-
if (abortController !== controller) return false;
|
|
458
|
-
|
|
569
|
+
if (abortController !== controller || executionOwner?.phase === "recovering") return false;
|
|
570
|
+
const endingOwner = executionOwner;
|
|
571
|
+
if (endingOwner?.controller === controller) sealExecution(endingOwner);
|
|
459
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 });
|
|
460
578
|
emitTurnStatus();
|
|
461
579
|
return true;
|
|
462
580
|
}
|
|
463
581
|
async function runTurn(input, opts) {
|
|
464
|
-
|
|
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;
|
|
465
617
|
const abortFromCaller = () => controller.abort(opts.signal?.reason);
|
|
466
618
|
if (opts.signal !== void 0) {
|
|
467
619
|
if (opts.signal.aborted) abortFromCaller();
|
|
468
620
|
else opts.signal.addEventListener("abort", abortFromCaller, { once: true });
|
|
469
621
|
}
|
|
622
|
+
const steeringClosed = () => owner.phase === "recovering" || owner.phase === "finished";
|
|
470
623
|
const drainSteering = async () => {
|
|
471
|
-
if (abortController !== controller || controller.signal.aborted
|
|
624
|
+
if (abortController !== controller || controller.signal.aborted || steeringClosed())
|
|
625
|
+
return [];
|
|
472
626
|
const barrierIndex = queue.findIndex((entry) => entry.delivery === "next-turn");
|
|
473
627
|
const steeringCount = barrierIndex === -1 ? queue.length : barrierIndex;
|
|
474
628
|
if (steeringCount === 0) return [];
|
|
475
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
|
+
}
|
|
476
634
|
emitTurnStatus();
|
|
477
635
|
const accepted = [];
|
|
478
636
|
for (const entry of drained) {
|
|
@@ -489,7 +647,8 @@ function createSessionController(deps) {
|
|
|
489
647
|
emit({ type: "error", message: guarded.message });
|
|
490
648
|
}
|
|
491
649
|
}
|
|
492
|
-
if (abortController !== controller || controller.signal.aborted
|
|
650
|
+
if (abortController !== controller || controller.signal.aborted || steeringClosed())
|
|
651
|
+
return [];
|
|
493
652
|
const current = accepted.filter((entry) => !entry.opts.signal?.aborted);
|
|
494
653
|
for (const entry of current) {
|
|
495
654
|
if (entry.opts.submission?.origin === "coordinator") {
|
|
@@ -502,6 +661,7 @@ function createSessionController(deps) {
|
|
|
502
661
|
emit({
|
|
503
662
|
type: "user-message",
|
|
504
663
|
text: entry.opts.displayText,
|
|
664
|
+
...humanId(entry.opts.submission) === void 0 ? {} : { submissionId: humanId(entry.opts.submission) },
|
|
505
665
|
...entry.opts.attachmentLabels?.length ? { attachmentLabels: entry.opts.attachmentLabels } : {},
|
|
506
666
|
// The drain stamps event.at at delivery; steeredAt preserves when
|
|
507
667
|
// the client actually queued the input — the HUD's "time since
|
|
@@ -525,6 +685,18 @@ function createSessionController(deps) {
|
|
|
525
685
|
let terminalStatus = "completed";
|
|
526
686
|
let terminalMessage;
|
|
527
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
|
+
}
|
|
528
700
|
emitTurnStatus();
|
|
529
701
|
if (!opts.systemInitiated) autoCompactionChain = 0;
|
|
530
702
|
compaction?.setEnabled(true);
|
|
@@ -569,8 +741,13 @@ function createSessionController(deps) {
|
|
|
569
741
|
const compactionSend = (text) => runTurn(text, {
|
|
570
742
|
systemInitiated: true,
|
|
571
743
|
transientInput: true,
|
|
744
|
+
correlationTurnId: owner.turnId,
|
|
745
|
+
enclosingExecution: owner,
|
|
572
746
|
allowSteering: opts.allowSteering,
|
|
573
|
-
submission:
|
|
747
|
+
submission: {
|
|
748
|
+
origin: "synthetic",
|
|
749
|
+
syntheticKind: "compaction"
|
|
750
|
+
},
|
|
574
751
|
...opts.signal === void 0 ? {} : { signal: opts.signal }
|
|
575
752
|
});
|
|
576
753
|
const compactionIo = {
|
|
@@ -579,8 +756,14 @@ function createSessionController(deps) {
|
|
|
579
756
|
onInfo: (message) => emit({ type: "info", message }),
|
|
580
757
|
onError: (message) => emit({ type: "error", message })
|
|
581
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
|
+
};
|
|
582
765
|
const runCompaction = async () => {
|
|
583
|
-
if (!compaction) return;
|
|
766
|
+
if (!compaction || owner.phase === "recovering") return;
|
|
584
767
|
if (autoCompactionChain >= MAX_AUTO_COMPACTION_CHAIN) {
|
|
585
768
|
compaction.setEnabled(false);
|
|
586
769
|
emit({
|
|
@@ -594,16 +777,17 @@ function createSessionController(deps) {
|
|
|
594
777
|
};
|
|
595
778
|
try {
|
|
596
779
|
const result = await deps.agent.send(finalInput, {
|
|
780
|
+
execution: owner.control,
|
|
597
781
|
streaming: true,
|
|
598
782
|
signal: controller.signal,
|
|
599
783
|
submission: opts.submission,
|
|
600
784
|
...opts.transientInput === true ? { transientInput: true } : {},
|
|
601
785
|
...opts.allowSteering ? { drainSteering } : {}
|
|
602
|
-
});
|
|
786
|
+
}).finally(owner.settle);
|
|
603
787
|
if (typeof result === "object" && result !== null && "status" in result && result.status === "aborted") {
|
|
604
788
|
terminalStatus = "aborted";
|
|
605
789
|
}
|
|
606
|
-
if (compaction && !controller.signal.aborted) {
|
|
790
|
+
if (sealForMaintenance() && compaction && !controller.signal.aborted) {
|
|
607
791
|
if (compaction.consumePendingAction() === "compact") {
|
|
608
792
|
await runCompaction();
|
|
609
793
|
}
|
|
@@ -613,7 +797,7 @@ function createSessionController(deps) {
|
|
|
613
797
|
terminalStatus = "aborted";
|
|
614
798
|
}
|
|
615
799
|
try {
|
|
616
|
-
const recovered = compaction ? compaction.messagesFromError(error) : null;
|
|
800
|
+
const recovered = sealForMaintenance() && compaction && !controller.signal.aborted ? compaction.messagesFromError(error) : null;
|
|
617
801
|
if (recovered) {
|
|
618
802
|
deps.agent.messages = recovered;
|
|
619
803
|
await runCompaction();
|
|
@@ -629,37 +813,58 @@ function createSessionController(deps) {
|
|
|
629
813
|
}
|
|
630
814
|
}
|
|
631
815
|
} finally {
|
|
816
|
+
owner.settle();
|
|
632
817
|
opts.signal?.removeEventListener("abort", abortFromCaller);
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
)
|
|
637
|
-
|
|
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) {
|
|
638
836
|
terminalStatus = "failed";
|
|
639
|
-
terminalMessage =
|
|
837
|
+
terminalMessage = policyDenial;
|
|
640
838
|
}
|
|
641
839
|
if (opts.turnId !== void 0) {
|
|
642
840
|
emit(
|
|
643
841
|
{
|
|
644
842
|
type: "turn-completed",
|
|
645
843
|
status: terminalStatus,
|
|
646
|
-
...
|
|
844
|
+
...policyDenial !== void 0 ? { reason: "policy-denied" } : {},
|
|
647
845
|
...terminalMessage !== void 0 ? { message: terminalMessage } : {}
|
|
648
846
|
},
|
|
649
847
|
opts.turnId
|
|
650
848
|
);
|
|
651
849
|
}
|
|
652
|
-
if (
|
|
653
|
-
|
|
850
|
+
if (opts.enclosingExecution === void 0 && (executionOwner === owner || ownedTurn && executionOwner === null)) {
|
|
851
|
+
if (!opts.systemInitiated) activePolicyDenial = void 0;
|
|
852
|
+
activeTurnId = previousTurnId;
|
|
853
|
+
}
|
|
654
854
|
if (ownedTurn) startNextQueuedSubmission();
|
|
655
855
|
}
|
|
656
856
|
}
|
|
657
857
|
function startTrackedTurn(input, opts) {
|
|
658
|
-
let
|
|
659
|
-
|
|
660
|
-
|
|
858
|
+
let resolve;
|
|
859
|
+
let reject;
|
|
860
|
+
const tracked = new Promise((done, fail) => {
|
|
861
|
+
resolve = done;
|
|
862
|
+
reject = fail;
|
|
661
863
|
});
|
|
662
864
|
activeTurnPromise = tracked;
|
|
865
|
+
void runTurn(input, opts).then(resolve, reject).finally(() => {
|
|
866
|
+
if (activeTurnPromise === tracked) activeTurnPromise = null;
|
|
867
|
+
});
|
|
663
868
|
return tracked;
|
|
664
869
|
}
|
|
665
870
|
function observeAcceptedTurn(turn, turnId) {
|
|
@@ -672,29 +877,142 @@ function createSessionController(deps) {
|
|
|
672
877
|
detached = true;
|
|
673
878
|
unsubscribeEvents();
|
|
674
879
|
unsubscribeLoaded();
|
|
880
|
+
unsubscribeRecall();
|
|
675
881
|
deps.channel.setToolDecider(null);
|
|
676
882
|
deps.channel.setQuestionHandler(null);
|
|
677
883
|
listeners.clear();
|
|
678
884
|
abortListeners.clear();
|
|
679
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
|
+
}
|
|
680
917
|
return {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
+
});
|
|
688
930
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
+
});
|
|
937
|
+
}
|
|
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;
|
|
692
1008
|
},
|
|
1009
|
+
abort,
|
|
693
1010
|
onAbort(listener) {
|
|
694
1011
|
abortListeners.add(listener);
|
|
695
1012
|
return () => abortListeners.delete(listener);
|
|
696
1013
|
},
|
|
697
1014
|
attachHostHooks(hooks) {
|
|
1015
|
+
if (hooks.submissionRecovery) submissionRecovery = hooks.submissionRecovery;
|
|
698
1016
|
if (hooks.compaction) compaction = hooks.compaction;
|
|
699
1017
|
if (hooks.guardPrompt) guardPrompt = hooks.guardPrompt;
|
|
700
1018
|
},
|
|
@@ -717,8 +1035,10 @@ function createSessionController(deps) {
|
|
|
717
1035
|
appendInfo(message) {
|
|
718
1036
|
emit({ type: "info", message });
|
|
719
1037
|
},
|
|
720
|
-
commitCompaction(messages) {
|
|
1038
|
+
commitCompaction({ messages, recall }) {
|
|
1039
|
+
copyRecallState(recall);
|
|
721
1040
|
queue.length = 0;
|
|
1041
|
+
activeHumans.length = 0;
|
|
722
1042
|
rejectAllPending("Session compacted; pending interactions were cancelled.");
|
|
723
1043
|
alwaysApproved.clear();
|
|
724
1044
|
emit({
|
|
@@ -728,7 +1048,8 @@ function createSessionController(deps) {
|
|
|
728
1048
|
busy: abortController !== null || hostBusyCount > 0,
|
|
729
1049
|
meta: state.meta,
|
|
730
1050
|
mcp: state.mcp,
|
|
731
|
-
messages: createMessageStateFromMessages([...messages])
|
|
1051
|
+
messages: createMessageStateFromMessages([...messages]),
|
|
1052
|
+
recall
|
|
732
1053
|
}
|
|
733
1054
|
});
|
|
734
1055
|
},
|
|
@@ -741,12 +1062,13 @@ function createSessionController(deps) {
|
|
|
741
1062
|
rejectAllPending("Session closed.");
|
|
742
1063
|
if (shouldEmitTurnStatus) emitTurnStatus();
|
|
743
1064
|
const active = activeTurnPromise;
|
|
744
|
-
|
|
1065
|
+
const recovery = recoveryPromise;
|
|
1066
|
+
if (!active && !recovery) {
|
|
745
1067
|
detachController();
|
|
746
1068
|
closePromise = Promise.resolve();
|
|
747
1069
|
return closePromise;
|
|
748
1070
|
}
|
|
749
|
-
closePromise = active.then(detachController
|
|
1071
|
+
closePromise = Promise.allSettled([active, recovery]).then(detachController);
|
|
750
1072
|
return closePromise;
|
|
751
1073
|
},
|
|
752
1074
|
dequeue(queuedId) {
|
|
@@ -872,16 +1194,27 @@ function createSessionController(deps) {
|
|
|
872
1194
|
emitTurnStatus();
|
|
873
1195
|
}
|
|
874
1196
|
},
|
|
875
|
-
|
|
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;
|
|
876
1207
|
emit({
|
|
877
1208
|
type: "user-message",
|
|
878
1209
|
text,
|
|
1210
|
+
...key === void 0 ? {} : { submissionId: key.submissionId },
|
|
879
1211
|
...attachmentLabels?.length ? { attachmentLabels } : {},
|
|
880
1212
|
// An echo emitted while a turn is in flight is a display-only
|
|
881
1213
|
// interjection, never a turn opener — mark it like a drained steer so
|
|
882
1214
|
// the WorkingStatusLine anchor and the persisted row both skip it.
|
|
883
1215
|
...abortController !== null || hostBusyCount > 0 ? { steeredAt: now() } : {}
|
|
884
1216
|
});
|
|
1217
|
+
return true;
|
|
885
1218
|
},
|
|
886
1219
|
getSnapshot() {
|
|
887
1220
|
return { seq, state, serverCommands };
|
|
@@ -927,11 +1260,13 @@ function createSessionController(deps) {
|
|
|
927
1260
|
publishMcpState(mcpState) {
|
|
928
1261
|
emit({ type: "mcp-state", state: mcpState });
|
|
929
1262
|
},
|
|
930
|
-
reset(options
|
|
1263
|
+
reset(options) {
|
|
1264
|
+
copyRecallState(options.recall);
|
|
931
1265
|
if (persistenceInFlight.size > 0) {
|
|
932
1266
|
return;
|
|
933
1267
|
}
|
|
934
1268
|
queue.length = 0;
|
|
1269
|
+
activeHumans.length = 0;
|
|
935
1270
|
if (options.preserveHostBusy !== true) hostBusyCount = 0;
|
|
936
1271
|
abortController?.abort(new Error("Session reset."));
|
|
937
1272
|
rejectAllPending("Session reset; pending interactions were cancelled.");
|
|
@@ -941,6 +1276,7 @@ function createSessionController(deps) {
|
|
|
941
1276
|
type: "state-reset",
|
|
942
1277
|
state: {
|
|
943
1278
|
...createSessionViewState(),
|
|
1279
|
+
recall: options.recall,
|
|
944
1280
|
busy: hostBusyCount > 0,
|
|
945
1281
|
meta: state.meta,
|
|
946
1282
|
mcp: state.mcp
|
|
@@ -962,29 +1298,22 @@ function createSessionController(deps) {
|
|
|
962
1298
|
async submit(input, opts) {
|
|
963
1299
|
if (closing) throw new Error("Session is closing.");
|
|
964
1300
|
opts.signal?.throwIfAborted();
|
|
1301
|
+
opts = admitHuman(input, opts);
|
|
965
1302
|
if (abortController || hostBusyCount > 0) {
|
|
966
1303
|
queue.push({
|
|
967
1304
|
id: `q-${++queueCounter}`,
|
|
968
1305
|
input,
|
|
969
1306
|
opts,
|
|
970
|
-
delivery: abortController !== null && allowsSteering(opts) ? "steering" : "next-turn",
|
|
1307
|
+
delivery: abortController !== null && executionOwner?.phase !== "recovering" && allowsSteering(opts) ? "steering" : "next-turn",
|
|
971
1308
|
queuedAt: now()
|
|
972
1309
|
});
|
|
973
1310
|
emitTurnStatus();
|
|
974
1311
|
return;
|
|
975
1312
|
}
|
|
976
1313
|
const coordinatorOwned = opts.submission?.origin === "coordinator";
|
|
977
|
-
if (!coordinatorOwned && !opts.silent) {
|
|
978
|
-
emit(
|
|
979
|
-
{
|
|
980
|
-
type: "user-message",
|
|
981
|
-
text: opts.displayText,
|
|
982
|
-
...opts.attachmentLabels?.length ? { attachmentLabels: opts.attachmentLabels } : {}
|
|
983
|
-
},
|
|
984
|
-
opts.turnId
|
|
985
|
-
);
|
|
986
|
-
}
|
|
987
1314
|
const turn = startTrackedTurn(input, {
|
|
1315
|
+
echo: coordinatorOwned || opts.silent ? void 0 : opts,
|
|
1316
|
+
human: opts.human,
|
|
988
1317
|
turnId: opts.turnId,
|
|
989
1318
|
allowSteering: allowsSteering(opts),
|
|
990
1319
|
submission: opts.submission ?? { origin: "user" },
|