fix: scope wake settlement ownership and allow legacy result deletion
This commit is contained in:
@@ -484,7 +484,8 @@ export async function handleChatInput(
|
||||
// run's first LLM call (the native replacement for continuation
|
||||
// coalescing). Settlement still cannot beat the flow below: it needs
|
||||
// a streamed response, which never happens inside the constructor.
|
||||
registerLiveAgent(chatId, constructed, input.type !== "steers" && input.type !== "continue");
|
||||
registerLiveAgent(chatId, constructed, input.type !== "steers" && input.type !== "continue",
|
||||
input.type === "steers" ? new Set(appends.map((message) => message.id)) : undefined);
|
||||
// Debug recorder run marker (owner, 2026-10-04): correlates the
|
||||
// JSONL event stream with the record's turns; no-op unless
|
||||
// LH_PROVIDER_EVENTS=1.
|
||||
@@ -525,7 +526,7 @@ export async function handleChatInput(
|
||||
// merely because usage/extraction/other settlement work failed.
|
||||
const persisted = stores.chatStore.getChat(chatId)?.messages ?? [];
|
||||
reconcileUserSteers(chatId, persisted);
|
||||
settleSteers(chatId, persisted, true);
|
||||
settleSteers(chatId, persisted, true, constructed);
|
||||
},
|
||||
onFinish: (turnMessages, isAborted) => {
|
||||
// Wave B: the record write is the agent's own final state —
|
||||
@@ -577,7 +578,7 @@ export async function handleChatInput(
|
||||
// resolve; stranded markers return to pending), then let
|
||||
// the pump take any stragglers down the idle wake.
|
||||
unregisterLiveAgent(chatId);
|
||||
settleSteers(chatId, stamped);
|
||||
settleSteers(chatId, stamped, false, constructed);
|
||||
// Composer steers (2026-10-03): delivered ids dropped, stranded
|
||||
// ones stay parked for the chat's next run (the registry is the
|
||||
// stranding safety net — the agent's undrained copy died above).
|
||||
|
||||
+6
-1
@@ -1,4 +1,5 @@
|
||||
import { abortExtraction, extractionStatuses } from "../agent/subagents/extraction.ts";
|
||||
import { isJobLive } from "../agent/subagents/aborts.ts";
|
||||
import { coverageOf } from "../core/coverage.ts";
|
||||
import { deleteChatTraces, type ChatDeleteMode, type ChatDeletionStats } from "../core/graph-deletion.ts";
|
||||
import { titleFromPrompt } from "../core/contracts.ts";
|
||||
@@ -77,7 +78,11 @@ function runningWorkBlocker(chatId: string): Extract<ChatDeleteOutcome, { status
|
||||
jobIds: [],
|
||||
};
|
||||
}
|
||||
const runningJobs = stores.jobStore.listJobs(chatId).filter((job) => job.status === "running");
|
||||
// Legacy records kept completed workers running until delivery. A saved
|
||||
// result without a live loop is pending session data, not computing work;
|
||||
// deletion must not depend on a prior recovery poll to recognize it.
|
||||
const runningJobs = stores.jobStore.listJobs(chatId).filter((job) =>
|
||||
job.status === "running" && (!job.result || isJobLive(job.id)));
|
||||
if (runningJobs.length > 0) {
|
||||
return {
|
||||
ok: false,
|
||||
|
||||
+16
-7
@@ -88,6 +88,9 @@ interface QueuedSteer {
|
||||
promise: Promise<void>;
|
||||
/** Wake cargo remains owned here, but must not steer into its own run. */
|
||||
waking?: boolean;
|
||||
/** The constructed vehicle, once startup claims a turn. Until then only
|
||||
* the pump owns waking: an unrelated foreground settlement cannot free it. */
|
||||
wakeAgent?: Agent;
|
||||
/** A failed start waits for a learner run or process restart, not a timer. */
|
||||
deferred?: boolean;
|
||||
resolve: () => void;
|
||||
@@ -286,10 +289,13 @@ function handOffEligible(chatId: string, agent: Agent): void {
|
||||
* markers are steered into the run right here. The settle seam cannot
|
||||
* beat the registration: settlement needs a streamed response, which
|
||||
* never happens inside the constructor call. */
|
||||
export function registerLiveAgent(chatId: string, agent: Agent, allowDeferred = true): void {
|
||||
export function registerLiveAgent(chatId: string, agent: Agent, allowDeferred = true, wakeSteerIds?: ReadonlySet<string>): void {
|
||||
state().liveAgents.set(chatId, agent);
|
||||
restorePendingDeliveries(chatId);
|
||||
if (allowDeferred) for (const entry of state().queues.get(chatId) ?? []) entry.deferred = false;
|
||||
for (const entry of state().queues.get(chatId) ?? []) {
|
||||
if (allowDeferred) entry.deferred = false;
|
||||
if (entry.waking && wakeSteerIds?.has(entry.steer.id)) entry.wakeAgent = agent;
|
||||
}
|
||||
handOffEligible(chatId, agent);
|
||||
}
|
||||
|
||||
@@ -369,7 +375,7 @@ export function restoreUncommittedAutomatic(chatId: string, agent: Agent, automa
|
||||
* This is what makes the live hand-off safe: pi owns no retry for a marker
|
||||
* stranded in a settled agent's queue, so the registry stays the truth and
|
||||
* the dead agent's copy is simply abandoned (GC'd with the agent). */
|
||||
export function settleSteers(chatId: string, persisted: { id?: string }[], failed = false): void {
|
||||
export function settleSteers(chatId: string, persisted: { id?: string }[], failed = false, settledAgent?: Agent): void {
|
||||
const queue = state().queues.get(chatId);
|
||||
if (!queue || queue.length === 0) return;
|
||||
const deliveredIds = new Set(persisted.map((m) => m.id).filter(Boolean));
|
||||
@@ -379,9 +385,11 @@ export function settleSteers(chatId: string, persisted: { id?: string }[], faile
|
||||
finalizeSteer(entry.steer);
|
||||
entry.resolve();
|
||||
} else {
|
||||
entry.handedTo = undefined;
|
||||
entry.waking = false;
|
||||
if (failed && jobIdOf(entry.steer)) entry.deferred = true;
|
||||
const handedHere = entry.handedTo !== undefined && (!settledAgent || entry.handedTo === settledAgent);
|
||||
const wakeHere = settledAgent !== undefined && entry.wakeAgent === settledAgent;
|
||||
if (handedHere) entry.handedTo = undefined;
|
||||
if (wakeHere) { entry.waking = false; entry.wakeAgent = undefined; }
|
||||
if (failed && (handedHere || wakeHere) && jobIdOf(entry.steer)) entry.deferred = true;
|
||||
remaining.push(entry);
|
||||
}
|
||||
}
|
||||
@@ -577,7 +585,7 @@ async function pump(chatId: string): Promise<void> {
|
||||
if (saved) noteSteersDurable(chatId, saved.messages);
|
||||
const owned = fire.filter((entry) => s.queues.get(chatId)?.includes(entry));
|
||||
if (status === 403 || status === 409) {
|
||||
for (const entry of owned) entry.waking = false;
|
||||
for (const entry of owned) { entry.waking = false; entry.wakeAgent = undefined; }
|
||||
if (status === 403) { parked = true; return; }
|
||||
await delay(RETRY_BACKOFF_MS[Math.min(attempt++, RETRY_BACKOFF_MS.length - 1)]!);
|
||||
continue;
|
||||
@@ -627,6 +635,7 @@ state().stopConnectionWatch = onSubscriptionChange(() => {
|
||||
|
||||
function deferOrReject(chatId: string, entry: QueuedSteer, error: Error): void {
|
||||
entry.waking = false;
|
||||
entry.wakeAgent = undefined;
|
||||
if (jobIdOf(entry.steer)) {
|
||||
entry.deferred = true;
|
||||
console.error(`[steer ${entry.steer.id}] delivery remains pending:`, error.message);
|
||||
|
||||
Reference in New Issue
Block a user