fix: separate worker completion from durable session delivery

This commit is contained in:
amosblomqvist
2026-10-26 01:55:52 +02:00
parent fc3673466d
commit e851432f4c
19 changed files with 646 additions and 326 deletions
+28 -32
View File
@@ -21,6 +21,12 @@ export interface JobRecord {
chatId: string;
/** The tool part this job fulfills. */
toolCallId: string;
/** Dispatch-time arguments, saved before spawn. Older jobs resolve from history. */
input?: unknown;
/** Session-owned delivery, independent of the worker's terminal outcome.
* Absent on legacy records: old terminal jobs must never be replayed. */
delivery?: { kind: "result" | "abort"; status: "pending" | "delivered" };
/** Worker computation only; a pending delivery does not keep it running. */
status: JobStatus;
/** Latest human-readable activity line, e.g. `web_search: "sigma.js v3"`. */
activity: string;
@@ -148,42 +154,32 @@ export function accumulateJobUsage(
* sourced briefs) — tracking a live activity line in the job record.
* ONE central client poll of GET /api/jobs?chatId=… (in Chat) drives every
* job surface: the transcript dispatch cards (live activity by
* toolCallId, quiet one-liner after delivery), the sub-agents sidebar
* toolCallId, quiet one-liner after worker completion), the sub-agents sidebar
* panel, and the single re-sync when a job settles.
*
* 3. On completion the runner builds a STEER MESSAGE (buildSubagentSteer)
* and hands it to the wake pipeline (src/server/jobs.ts): a user-role
* result-carrying native marker — the content is what the MODEL needs
* (the research brief, the builder's notes), plus a data-subagent-artifact
* part with what the UI renders (the full result: brief + sources /
* SVG / demo HTML). Provider conversion drops data parts, so artifact
* payloads never enter the teacher's context. metadata.subagentResult
* identifies the job; the UI renders these as sub-agent result cards,
* NEVER as learner interjections.
* 3. Dispatch saves input on the job before spawn. On completion the
* runner saves result + terminal worker status + pending delivery in
* one job update, then queues buildSubagentSteer WITHOUT waiting for
* delivery. The native custom marker's content is model-facing text;
* details carry the full artifact payload for the UI. Worker resources
* are released even if a question keeps the result pending indefinitely.
* Only computing workers appear in Background agents or accept abort.
*
* 4. The wake pipeline delivers the steer by firing a SERVER-SIDE teacher
* turn through the same turn composition as a client request
* (handleChatRequest) — write-ahead persist, turn liveness, extraction
* trigger: one code path. A per-chat serialized queue: a steer waits
* while a turn is in flight OR a question tool part is pending (an
* unanswered client-side tool call breaks provider conversion — the
* steer fires right after the learner's continuation), then fires; a 409
* (a client turn raced in) retries. Delivery is idempotent per jobId, so
* a crashed wake can be re-entered by the stale-job recovery sweep. The
* job's status flips to done/error only AFTER the wake turn completes —
* a polling client that sees the flip re-syncs once and gets the
* finished record.
* 4. Session delivery owns one registry entry per deterministic steer ID,
* including while a wake is in flight or a 409/403 returns its cargo.
* A live agent takes it via pi steering; registration restores saved
* pending jobs before the answer run's first poll. Otherwise the idle
* wake uses the normal turn composition and respects pending questions,
* dismissal, and background consent. A saved transcript marker completes
* delivery; HTTP 200 or streamed visibility alone does not.
*
* CONTINUATION COALESCING: the idle window above can starve — a
* quiz-chaining teacher ends every turn with a new pending check, so
* the gates never stay open long enough for a wake to fire. The fix:
* when a client turn wins the turn registry, the handler CLAIMS every
* queued, undelivered steer (jobs.ts claimSteers) and appends it after
* the learner's answer in that very turn — conversion-safe, because the
* answered quiz carries its output. Every answer flushes pending
* steers immediately; the wake pump remains for turns that end with no
* pending question. The pump re-checks steerDelivered right before
* firing, so a claim racing the pump converges to exactly one delivery.
* Failed starts remain quietly pending until a learner run or restart
* recovery, without new timers, visible delivery errors, or worker-status
* changes. Existing 409 backoff and consent deferral remain. Accepted
* turns are never automatically replayed. Legacy running records with a
* result become finished/pending; legacy terminal records with no
* delivery metadata remain untouched. Session deletion discards pending
* deliveries rather than treating them as running work.
*
* 5. No pending job parts ever exist, so the composer never gates on jobs
* (the learner can interject mid-job; the steer queues behind their
+2
View File
@@ -136,6 +136,8 @@ export interface CreateJobInit {
toolCallId: string;
/** First activity line (defaults to "starting"). */
activity?: string;
/** Saved on the job before its worker is spawned. */
input?: unknown;
}
/** Mergeable fields — identity (id, kind, chatId, toolCallId, createdAt) is immutable. */