@cabane/companion 0.6.71 → 0.6.73
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/cli.js +502 -355
- package/dist/runtime.js +497 -350
- package/package.json +1 -1
package/dist/runtime.js
CHANGED
|
@@ -6775,10 +6775,13 @@ var ConnectorHealthStore = class {
|
|
|
6775
6775
|
};
|
|
6776
6776
|
|
|
6777
6777
|
// src/dispatcher.ts
|
|
6778
|
-
import {
|
|
6779
|
-
import { existsSync as existsSync10, readdirSync as readdirSync2, statSync } from "fs";
|
|
6778
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync } from "fs";
|
|
6780
6779
|
import { join as join14 } from "path";
|
|
6781
6780
|
|
|
6781
|
+
// src/turn-execution.ts
|
|
6782
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
6783
|
+
import { existsSync as existsSync10 } from "fs";
|
|
6784
|
+
|
|
6782
6785
|
// src/turn-control-tools.ts
|
|
6783
6786
|
import { z as z13 } from "zod";
|
|
6784
6787
|
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
@@ -7588,7 +7591,7 @@ var TurnCommitter = class {
|
|
|
7588
7591
|
}
|
|
7589
7592
|
};
|
|
7590
7593
|
|
|
7591
|
-
// src/
|
|
7594
|
+
// src/turn-execution.ts
|
|
7592
7595
|
var PREPARING_TOOL_NAME = "preparing";
|
|
7593
7596
|
var PREPARE_FAILED_PREFIX = "**Couldn't prepare your environment.** I wasn't able to provision a working directory for this conversation, so I can't run this turn. The provisioning command reported:";
|
|
7594
7597
|
var MISSING_SECRET_PREFIX = "**Missing secret on this companion.** This agent's tools need a credential this device hasn't been given, so I can't run this turn safely. Declare it in this companion\u2019s secret store (`~/.cabane/secrets.json`) and try again. Missing:";
|
|
@@ -7600,33 +7603,6 @@ var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
|
7600
7603
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
7601
7604
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
7602
7605
|
var DEFAULT_LEASE_RENEWAL_MS = 3e4;
|
|
7603
|
-
function checkoutState(cwd) {
|
|
7604
|
-
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
7605
|
-
if (!existsSync10(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
7606
|
-
let entries;
|
|
7607
|
-
try {
|
|
7608
|
-
entries = readdirSync2(cwd);
|
|
7609
|
-
} catch (error) {
|
|
7610
|
-
return { ok: false, reason: `${cwd} is unreadable (${error.message})` };
|
|
7611
|
-
}
|
|
7612
|
-
if (entries.length === 0) {
|
|
7613
|
-
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
7614
|
-
}
|
|
7615
|
-
const gitPath = join14(cwd, ".git");
|
|
7616
|
-
if (!existsSync10(gitPath)) return { ok: true, reason: "usable" };
|
|
7617
|
-
let stat;
|
|
7618
|
-
try {
|
|
7619
|
-
stat = statSync(gitPath);
|
|
7620
|
-
} catch (error) {
|
|
7621
|
-
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
7622
|
-
}
|
|
7623
|
-
if (stat.isDirectory() && !existsSync10(join14(gitPath, "HEAD")))
|
|
7624
|
-
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
7625
|
-
return { ok: true, reason: "usable" };
|
|
7626
|
-
}
|
|
7627
|
-
function runKey(conversationId, agentId) {
|
|
7628
|
-
return `${conversationId}|${agentId}`;
|
|
7629
|
-
}
|
|
7630
7606
|
var LEASE_REFUSALS = /* @__PURE__ */ new Set([
|
|
7631
7607
|
"dispatch_not_admitted",
|
|
7632
7608
|
"turn_already_ended",
|
|
@@ -7661,61 +7637,128 @@ function describeErrorBody(body) {
|
|
|
7661
7637
|
if (text.length === 0) return void 0;
|
|
7662
7638
|
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
7663
7639
|
}
|
|
7664
|
-
|
|
7665
|
-
|
|
7640
|
+
function initialOutcome() {
|
|
7641
|
+
return {
|
|
7642
|
+
sessionWritten: false,
|
|
7643
|
+
sessionDegraded: false,
|
|
7644
|
+
sessionWriteRejected: false,
|
|
7645
|
+
okResult: false,
|
|
7646
|
+
resultReason: void 0,
|
|
7647
|
+
turnUsage: void 0,
|
|
7648
|
+
turnResolvedModel: void 0,
|
|
7649
|
+
turnResolvedConfig: void 0,
|
|
7650
|
+
turnMcpInventory: void 0,
|
|
7651
|
+
eventCounts: { session: 0, text: 0, thinking: 0, tool: 0, result: 0 },
|
|
7652
|
+
runtimeResultKind: null,
|
|
7653
|
+
contentBearingEvents: 0,
|
|
7654
|
+
latestSessionState: null,
|
|
7655
|
+
settledDiagnostics: null,
|
|
7656
|
+
silentMarkerEmitted: false,
|
|
7657
|
+
timeoutReason: null,
|
|
7658
|
+
leaseLost: false
|
|
7659
|
+
};
|
|
7660
|
+
}
|
|
7661
|
+
var TurnConcluded = class {
|
|
7662
|
+
constructor(reason, errorReason) {
|
|
7663
|
+
this.reason = reason;
|
|
7664
|
+
this.errorReason = errorReason;
|
|
7665
|
+
}
|
|
7666
|
+
reason;
|
|
7667
|
+
errorReason;
|
|
7668
|
+
};
|
|
7669
|
+
var TurnExecution = class {
|
|
7670
|
+
constructor(opts, supervisor, payload, handleOpts = {}) {
|
|
7666
7671
|
this.opts = opts;
|
|
7667
|
-
this.
|
|
7672
|
+
this.supervisor = supervisor;
|
|
7673
|
+
this.payload = payload;
|
|
7674
|
+
this.dispatchId = payload.messageId;
|
|
7675
|
+
this.workspaceId = opts.workspaceId;
|
|
7676
|
+
this.turnLog = opts.log.child({
|
|
7677
|
+
workspaceId: opts.workspaceId,
|
|
7678
|
+
conversationId: payload.conversationId,
|
|
7679
|
+
agentId: payload.agentId,
|
|
7680
|
+
messageId: payload.messageId
|
|
7681
|
+
});
|
|
7682
|
+
this.turnId = handleOpts.turnId ?? randomUUID();
|
|
7668
7683
|
}
|
|
7669
7684
|
opts;
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
//
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7685
|
+
supervisor;
|
|
7686
|
+
payload;
|
|
7687
|
+
startedAt = Date.now();
|
|
7688
|
+
turnId;
|
|
7689
|
+
dispatchId;
|
|
7690
|
+
workspaceId;
|
|
7691
|
+
turnLog;
|
|
7692
|
+
abortController = new AbortController();
|
|
7693
|
+
outcome = initialOutcome();
|
|
7694
|
+
seqCounter = 0;
|
|
7695
|
+
// CT11: per-turn monotonic counter mirroring the in-process dispatcher's.
|
|
7696
|
+
nextSeq = () => ++this.seqCounter;
|
|
7697
|
+
turnContext;
|
|
7698
|
+
resolvedMcpServers;
|
|
7699
|
+
effectiveCwd;
|
|
7700
|
+
hookEnv;
|
|
7701
|
+
turnEnv;
|
|
7702
|
+
sendState;
|
|
7703
|
+
skipState;
|
|
7704
|
+
askState;
|
|
7705
|
+
wakeState;
|
|
7706
|
+
replyState;
|
|
7707
|
+
controlOrder;
|
|
7708
|
+
turnControlServer;
|
|
7709
|
+
request;
|
|
7710
|
+
adapter;
|
|
7711
|
+
committer = null;
|
|
7712
|
+
transcript = null;
|
|
7713
|
+
turnControlIntentFetched = false;
|
|
7714
|
+
applyRecordedTurnControlIntent = async () => {
|
|
7715
|
+
};
|
|
7716
|
+
// CT1292: the committer is built before the lease watchdog arms, so the
|
|
7717
|
+
// failed-commit hint reaches it through this slot. Safe as a no-op until the
|
|
7718
|
+
// watchdog replaces it — the committer only fires it from inside the adapter
|
|
7719
|
+
// loop, which runs after.
|
|
7720
|
+
noteCommitFailed = () => {
|
|
7721
|
+
};
|
|
7722
|
+
disarmWatchdogs = () => {
|
|
7723
|
+
};
|
|
7724
|
+
// Codo's stack review, blocking finding #2: set the moment `acquireLease`'s
|
|
7725
|
+
// PATCH returns — the mailroom now holds this turn as the conversation's
|
|
7726
|
+
// running one, so every exit after this point must settle THE TURN, not just
|
|
7727
|
+
// clear the participant flag.
|
|
7728
|
+
admitted = false;
|
|
7729
|
+
concluded(reason, errorReason) {
|
|
7730
|
+
return new TurnConcluded(reason, errorReason);
|
|
7731
|
+
}
|
|
7732
|
+
// The arc. Phases in order; every pre-loop exit throws `TurnConcluded` and
|
|
7733
|
+
// funnels through ONE conclusion (hand back the controller — idempotent and
|
|
7734
|
+
// identity-checked at the supervisor — then clear the active-run flag);
|
|
7735
|
+
// everything past the lease settles in `execute()`'s one `finally`.
|
|
7736
|
+
async run() {
|
|
7696
7737
|
try {
|
|
7697
|
-
this.
|
|
7698
|
-
|
|
7738
|
+
await this.fetchContext();
|
|
7739
|
+
this.gateTrigger();
|
|
7740
|
+
await this.resolveSecrets();
|
|
7741
|
+
await this.prepareEnvironment();
|
|
7742
|
+
this.buildRequest();
|
|
7743
|
+
await this.acquireLease();
|
|
7744
|
+
await this.selectAdapter();
|
|
7745
|
+
} catch (err) {
|
|
7746
|
+
if (err instanceof TurnConcluded) {
|
|
7747
|
+
this.supervisor.releaseAbort(this.turnId, this.abortController);
|
|
7748
|
+
return this.admitted ? this.concludeAdmittedRun(err.reason, err.errorReason) : this.concludeBeforeRun(err.reason, err.errorReason);
|
|
7749
|
+
}
|
|
7750
|
+
if (this.admitted) {
|
|
7751
|
+
this.supervisor.releaseAbort(this.turnId, this.abortController);
|
|
7752
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
7753
|
+
await this.concludeAdmittedRun("setup_failed", `setup_failed: ${msg}`);
|
|
7754
|
+
}
|
|
7755
|
+
throw err;
|
|
7699
7756
|
}
|
|
7757
|
+
await this.execute();
|
|
7758
|
+
return this.report();
|
|
7700
7759
|
}
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
// The server lights the "X is replying…" indicator eagerly at dispatch
|
|
7704
|
-
// (chat-dispatch.ts `scheduleRun`), and from that point only the companion can
|
|
7705
|
-
// clear it — the SJ383 `finally` after the SDK loop is the one clear, and
|
|
7706
|
-
// every pre-run exit returns before reaching it. So each pre-run failure has
|
|
7707
|
-
// to clear `active_run_started_at` itself, mirroring that `finally`, or the
|
|
7708
|
-
// indicator strands until the 12h age sweep.
|
|
7709
|
-
//
|
|
7710
|
-
// `errorReason` controls the server's duplicate-notice rule (the active-run
|
|
7711
|
-
// PATCH handler in conversations.ts): a clear carrying `errorReason` makes the
|
|
7712
|
-
// server post a `role:'system'` "X's reply failed: …" notice. Exits that
|
|
7713
|
-
// already posted their own user-facing `final` (prepare-failed, missing-secret)
|
|
7714
|
-
// pass NO errorReason, so the user doesn't see a second failure message; exits
|
|
7715
|
-
// that posted nothing pass `errorReason` so the user still gets a notice. The
|
|
7716
|
-
// clear is durable (CT93 outbox, last-writer-wins per pair), so a terminal
|
|
7717
|
-
// failure here just logs and the server age-sweep backstops it.
|
|
7718
|
-
async concludeBeforeRun(payload, turnLog, startedAt, reason, errorReason) {
|
|
7760
|
+
async concludeBeforeRun(reason, errorReason) {
|
|
7761
|
+
const { payload, turnLog, startedAt } = this;
|
|
7719
7762
|
const body = {
|
|
7720
7763
|
activeRunStartedAt: null,
|
|
7721
7764
|
// CT1046: this dispatch is over before it ran — retire its evidence so it
|
|
@@ -7737,25 +7780,47 @@ var Dispatcher = class {
|
|
|
7737
7780
|
);
|
|
7738
7781
|
}
|
|
7739
7782
|
const durationMs = Date.now() - startedAt;
|
|
7740
|
-
this.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
7783
|
+
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
7741
7784
|
return { ok: false, durationMs, reason };
|
|
7742
7785
|
}
|
|
7743
|
-
//
|
|
7744
|
-
//
|
|
7745
|
-
//
|
|
7746
|
-
//
|
|
7747
|
-
//
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7786
|
+
// The post-admission analogue of `concludeBeforeRun` (Codo's stack review,
|
|
7787
|
+
// blocking finding #2): this turn WAS admitted — the mailroom holds it as
|
|
7788
|
+
// the conversation's running turn — so the clear must NAME the turn and
|
|
7789
|
+
// DECLARE its terminal outcome. A bare pre-run clear here would reset the
|
|
7790
|
+
// participant flag while the turn metadata stayed running, the dispatch
|
|
7791
|
+
// stayed live, and the queue stayed held until the reaper. `outcome`
|
|
7792
|
+
// follows the settle wire's rule: a failure names its reason; without one
|
|
7793
|
+
// the turn settled — the runtime-unavailable exit posts its user-facing
|
|
7794
|
+
// `final` before concluding, so the turn genuinely produced its terminal
|
|
7795
|
+
// output and `settled` releases the queue honestly.
|
|
7796
|
+
async concludeAdmittedRun(reason, errorReason) {
|
|
7797
|
+
const { payload, turnLog, startedAt, turnId } = this;
|
|
7798
|
+
const body = {
|
|
7799
|
+
activeRunStartedAt: null,
|
|
7800
|
+
turnId,
|
|
7801
|
+
settledMessageId: payload.messageId,
|
|
7802
|
+
outcome: errorReason ? "failed" : "settled"
|
|
7803
|
+
};
|
|
7804
|
+
if (errorReason) body.errorReason = errorReason.slice(0, 200);
|
|
7805
|
+
try {
|
|
7806
|
+
await this.opts.api.setActiveRun(
|
|
7807
|
+
this.opts.workspaceId,
|
|
7808
|
+
payload.conversationId,
|
|
7809
|
+
payload.agentId,
|
|
7810
|
+
body
|
|
7811
|
+
);
|
|
7812
|
+
} catch (err) {
|
|
7813
|
+
turnLog.warn(
|
|
7814
|
+
{ err: err instanceof Error ? err.message : String(err), turnId },
|
|
7815
|
+
"dispatcher: admitted-run settle failed terminally; server age-sweep is the backstop"
|
|
7816
|
+
);
|
|
7817
|
+
}
|
|
7818
|
+
const durationMs = Date.now() - startedAt;
|
|
7819
|
+
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
7820
|
+
return { ok: false, durationMs, reason };
|
|
7821
|
+
}
|
|
7822
|
+
async fetchContext() {
|
|
7823
|
+
const { payload, turnId, turnLog } = this;
|
|
7759
7824
|
let turnContext;
|
|
7760
7825
|
try {
|
|
7761
7826
|
turnContext = await this.opts.api.getTurnContext(
|
|
@@ -7770,35 +7835,36 @@ var Dispatcher = class {
|
|
|
7770
7835
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
7771
7836
|
"dispatcher: turn-context 404 (stale event \u2014 conversation/message/participant gone); skipping"
|
|
7772
7837
|
);
|
|
7773
|
-
|
|
7838
|
+
throw this.concluded("turn_context_not_found");
|
|
7774
7839
|
}
|
|
7775
7840
|
const reason = err instanceof Error ? err.message : String(err);
|
|
7776
7841
|
turnLog.error({ err: reason }, "dispatcher: failed to fetch turn context");
|
|
7777
7842
|
const fetchReason = `fetch_failed: ${reason}`;
|
|
7778
|
-
|
|
7843
|
+
throw this.concluded(fetchReason, fetchReason);
|
|
7779
7844
|
}
|
|
7780
|
-
|
|
7845
|
+
this.turnContext = turnContext;
|
|
7846
|
+
}
|
|
7847
|
+
gateTrigger() {
|
|
7848
|
+
const { payload, turnLog } = this;
|
|
7849
|
+
const message = this.turnContext.message;
|
|
7781
7850
|
const isDispatchableTrigger = message.role === "user" || message.role === "agent" || message.role === "system" && message.hasPrimaryDispatch === true;
|
|
7782
7851
|
if (!isDispatchableTrigger) {
|
|
7783
7852
|
turnLog.warn({ role: message.role }, "dispatcher: trigger role not dispatchable \u2014 skipping");
|
|
7784
|
-
|
|
7785
|
-
payload,
|
|
7786
|
-
turnLog,
|
|
7787
|
-
startedAt,
|
|
7788
|
-
"unexpected_role",
|
|
7789
|
-
UNEXPECTED_ROLE_REASON
|
|
7790
|
-
);
|
|
7853
|
+
throw this.concluded("unexpected_role", UNEXPECTED_ROLE_REASON);
|
|
7791
7854
|
}
|
|
7792
|
-
this.notifyStart({
|
|
7793
|
-
id: dispatchId,
|
|
7855
|
+
this.supervisor.notifyStart({
|
|
7856
|
+
id: this.dispatchId,
|
|
7794
7857
|
conversationId: payload.conversationId,
|
|
7795
7858
|
message: message.body
|
|
7796
7859
|
});
|
|
7860
|
+
}
|
|
7861
|
+
async resolveSecrets() {
|
|
7862
|
+
const { payload, workspaceId, turnLog } = this;
|
|
7797
7863
|
const secretStore = this.opts.secretStore ?? loadSecretStoreTolerant((m) => turnLog.warn(m));
|
|
7798
7864
|
const { servers: resolvedMcpServers, missing } = resolveMcpSecrets(
|
|
7799
7865
|
// CT262: the user MCP DEFINITIONS (placeholder form) come from the turn
|
|
7800
7866
|
// context now, not a separate `getAgentSelf` run-config fetch.
|
|
7801
|
-
turnContext.mcpServers ?? {},
|
|
7867
|
+
this.turnContext.mcpServers ?? {},
|
|
7802
7868
|
secretStore
|
|
7803
7869
|
);
|
|
7804
7870
|
if (missing.length > 0) {
|
|
@@ -7819,13 +7885,15 @@ var Dispatcher = class {
|
|
|
7819
7885
|
);
|
|
7820
7886
|
}
|
|
7821
7887
|
const reason = `missing_secret: ${missing.join(",")}`;
|
|
7822
|
-
|
|
7888
|
+
throw this.concluded(reason);
|
|
7823
7889
|
}
|
|
7890
|
+
this.resolvedMcpServers = resolvedMcpServers;
|
|
7891
|
+
}
|
|
7892
|
+
async prepareEnvironment() {
|
|
7893
|
+
const { payload, workspaceId, turnId, turnLog } = this;
|
|
7824
7894
|
const localCwd = this.opts.local.cwd;
|
|
7825
7895
|
const prepareHook = this.opts.local.prepareHook;
|
|
7826
|
-
const cabaneCwd = turnContext.cwd;
|
|
7827
|
-
let seqCounter = 0;
|
|
7828
|
-
const nextSeq = () => ++seqCounter;
|
|
7896
|
+
const cabaneCwd = this.turnContext.cwd;
|
|
7829
7897
|
let effectiveCwd = localCwd ?? cabaneCwd;
|
|
7830
7898
|
if (effectiveCwd && !existsSync10(effectiveCwd)) {
|
|
7831
7899
|
turnLog.warn(
|
|
@@ -7835,9 +7903,9 @@ var Dispatcher = class {
|
|
|
7835
7903
|
effectiveCwd = void 0;
|
|
7836
7904
|
}
|
|
7837
7905
|
let hookEnv;
|
|
7838
|
-
const triggerIsPrepareFailure = message.body.startsWith(PREPARE_FAILED_PREFIX);
|
|
7839
|
-
const prepareFailureDispatch = turnContext.dispatchedByAgentId && !triggerIsPrepareFailure ? {
|
|
7840
|
-
dispatch: turnContext.dispatchedByAgentId,
|
|
7906
|
+
const triggerIsPrepareFailure = this.turnContext.message.body.startsWith(PREPARE_FAILED_PREFIX);
|
|
7907
|
+
const prepareFailureDispatch = this.turnContext.dispatchedByAgentId && !triggerIsPrepareFailure ? {
|
|
7908
|
+
dispatch: this.turnContext.dispatchedByAgentId,
|
|
7841
7909
|
dispatchBody: `${PREPARE_FAILED_PREFIX}
|
|
7842
7910
|
|
|
7843
7911
|
The dispatched turn could not start. Re-dispatch it after repairing the preparation failure shown in this conversation.`
|
|
@@ -7863,11 +7931,11 @@ The dispatched turn could not start. Re-dispatch it after repairing the preparat
|
|
|
7863
7931
|
conversationId: payload.conversationId,
|
|
7864
7932
|
agentId: payload.agentId,
|
|
7865
7933
|
agentUsername: this.opts.agentUsername,
|
|
7866
|
-
runtime: turnContext.runtime,
|
|
7867
|
-
hostAccess: turnContext.policy.hostFs,
|
|
7868
|
-
triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
|
|
7869
|
-
title: turnContext.conversation.title,
|
|
7870
|
-
messageBody: message.body,
|
|
7934
|
+
runtime: this.turnContext.runtime,
|
|
7935
|
+
hostAccess: this.turnContext.policy.hostFs,
|
|
7936
|
+
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
7937
|
+
title: this.turnContext.conversation.title,
|
|
7938
|
+
messageBody: this.turnContext.message.body,
|
|
7871
7939
|
prepared: cached2
|
|
7872
7940
|
});
|
|
7873
7941
|
} catch (err) {
|
|
@@ -7889,7 +7957,7 @@ ${reason}`,
|
|
|
7889
7957
|
"dispatcher: prepare-rejection post failed"
|
|
7890
7958
|
);
|
|
7891
7959
|
}
|
|
7892
|
-
|
|
7960
|
+
throw this.concluded(`prepare_failed: ${reason}`);
|
|
7893
7961
|
}
|
|
7894
7962
|
}
|
|
7895
7963
|
} else {
|
|
@@ -7897,7 +7965,7 @@ ${reason}`,
|
|
|
7897
7965
|
let preparingStarted = false;
|
|
7898
7966
|
let preparingSeq = null;
|
|
7899
7967
|
const reportPreparing = (phase) => {
|
|
7900
|
-
if (preparingSeq === null) preparingSeq = nextSeq();
|
|
7968
|
+
if (preparingSeq === null) preparingSeq = this.nextSeq();
|
|
7901
7969
|
const seq = preparingSeq;
|
|
7902
7970
|
void this.opts.api.reportActivity(workspaceId, payload.conversationId, payload.agentId, {
|
|
7903
7971
|
turnId,
|
|
@@ -7925,18 +7993,18 @@ ${reason}`,
|
|
|
7925
7993
|
conversationId: payload.conversationId,
|
|
7926
7994
|
agentId: payload.agentId,
|
|
7927
7995
|
agentUsername: this.opts.agentUsername,
|
|
7928
|
-
runtime: turnContext.runtime,
|
|
7929
|
-
hostAccess: turnContext.policy.hostFs,
|
|
7996
|
+
runtime: this.turnContext.runtime,
|
|
7997
|
+
hostAccess: this.turnContext.policy.hostFs,
|
|
7930
7998
|
// CT317/CT319: the trigger message's referenced-entry paths — what the
|
|
7931
7999
|
// tasker prepare hook keys its per-task env off. Defaults to `[]` for
|
|
7932
8000
|
// an older API. The conversation anchor is gone (CT319).
|
|
7933
|
-
triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
|
|
7934
|
-
title: turnContext.conversation.title,
|
|
8001
|
+
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
8002
|
+
title: this.turnContext.conversation.title,
|
|
7935
8003
|
// CT943: the dispatching message's text — where an `env:` directive
|
|
7936
8004
|
// rides. The server has always sent the trigger body on the turn
|
|
7937
8005
|
// context (for the live feed); this is the first thing to read it as
|
|
7938
8006
|
// an INPUT, so a dispatch can ask for its environment in words.
|
|
7939
|
-
messageBody: message.body
|
|
8007
|
+
messageBody: this.turnContext.message.body
|
|
7940
8008
|
});
|
|
7941
8009
|
clearTimeout(preparingTimer);
|
|
7942
8010
|
if (preparingStarted) reportPreparing("done");
|
|
@@ -7966,31 +8034,23 @@ ${reason}`,
|
|
|
7966
8034
|
);
|
|
7967
8035
|
}
|
|
7968
8036
|
const failReason = `prepare_failed: ${reason}`;
|
|
7969
|
-
|
|
8037
|
+
throw this.concluded(failReason);
|
|
7970
8038
|
}
|
|
7971
8039
|
}
|
|
7972
8040
|
}
|
|
7973
8041
|
const turnEnv = {
|
|
7974
8042
|
...hookEnv,
|
|
7975
|
-
CABANE_HOST_ACCESS: turnContext.policy.hostFs ? "1" : "0",
|
|
8043
|
+
CABANE_HOST_ACCESS: this.turnContext.policy.hostFs ? "1" : "0",
|
|
7976
8044
|
CABANE_COMPANION_HOME: agentCompanionHome()
|
|
7977
8045
|
};
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
const releaseAbort = () => {
|
|
7987
|
-
const pair = this.aborts.get(key);
|
|
7988
|
-
if (pair?.get(turnId) !== abortController) return;
|
|
7989
|
-
pair.delete(turnId);
|
|
7990
|
-
if (pair.size === 0) this.aborts.delete(key);
|
|
7991
|
-
};
|
|
7992
|
-
let timeoutReason = null;
|
|
7993
|
-
let leaseLost = false;
|
|
8046
|
+
this.effectiveCwd = effectiveCwd;
|
|
8047
|
+
this.hookEnv = hookEnv;
|
|
8048
|
+
this.turnEnv = turnEnv;
|
|
8049
|
+
}
|
|
8050
|
+
async acquireLease() {
|
|
8051
|
+
const { payload, workspaceId, turnId, turnLog } = this;
|
|
8052
|
+
const abortController = this.abortController;
|
|
8053
|
+
this.supervisor.registerAbort(turnId, abortController);
|
|
7994
8054
|
try {
|
|
7995
8055
|
await this.opts.api.setActiveRun(workspaceId, payload.conversationId, payload.agentId, {
|
|
7996
8056
|
activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -8003,27 +8063,30 @@ ${reason}`,
|
|
|
8003
8063
|
});
|
|
8004
8064
|
} catch (err) {
|
|
8005
8065
|
const refusal = leaseRefusal(err);
|
|
8006
|
-
releaseAbort();
|
|
8007
8066
|
if (refusal === "dispatch_not_admitted" || refusal === "turn_already_ended") {
|
|
8008
8067
|
turnLog.error(
|
|
8009
8068
|
{ refusal, turnId },
|
|
8010
8069
|
"dispatcher: refused a turn lease; not running the model"
|
|
8011
8070
|
);
|
|
8012
|
-
|
|
8071
|
+
throw this.concluded(`lease_refused: ${refusal}`);
|
|
8013
8072
|
}
|
|
8014
8073
|
const reason = refusal ? `lease_refused: ${refusal}` : "lease_unconfirmed";
|
|
8015
8074
|
turnLog.error(
|
|
8016
8075
|
{ refusal, turnId, err: err instanceof Error ? err.message : String(err) },
|
|
8017
8076
|
"dispatcher: turn lease not confirmed; not running the model"
|
|
8018
8077
|
);
|
|
8019
|
-
|
|
8020
|
-
}
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
const
|
|
8025
|
-
const
|
|
8026
|
-
const
|
|
8078
|
+
throw this.concluded(reason, reason);
|
|
8079
|
+
}
|
|
8080
|
+
this.admitted = true;
|
|
8081
|
+
}
|
|
8082
|
+
buildRequest() {
|
|
8083
|
+
const { payload, workspaceId } = this;
|
|
8084
|
+
const sendState = this.sendState = createSendState();
|
|
8085
|
+
const skipState = this.skipState = createSkipState();
|
|
8086
|
+
const askState = this.askState = createAskState();
|
|
8087
|
+
const wakeState = this.wakeState = createWakeState();
|
|
8088
|
+
const replyState = this.replyState = createReplyState();
|
|
8089
|
+
const controlOrder = this.controlOrder = createTurnControlOrder();
|
|
8027
8090
|
const turnControlServer = createTurnControlMcpServer(
|
|
8028
8091
|
sendState,
|
|
8029
8092
|
skipState,
|
|
@@ -8032,18 +8095,18 @@ ${reason}`,
|
|
|
8032
8095
|
replyState,
|
|
8033
8096
|
controlOrder
|
|
8034
8097
|
);
|
|
8035
|
-
|
|
8036
|
-
turnContext,
|
|
8098
|
+
this.request = buildCompanionTurnRequest({
|
|
8099
|
+
turnContext: this.turnContext,
|
|
8037
8100
|
baseUrl: this.opts.baseUrl,
|
|
8038
8101
|
agentPat: this.opts.credential,
|
|
8039
8102
|
// CT306: the per-turn OBO credential when the API minted one; falls back to
|
|
8040
8103
|
// the companion PAT (`agentPat`) inside `buildCompanionTurnRequest` otherwise.
|
|
8041
|
-
...turnContext.turnToken ? { turnToken: turnContext.turnToken } : {},
|
|
8104
|
+
...this.turnContext.turnToken ? { turnToken: this.turnContext.turnToken } : {},
|
|
8042
8105
|
// SJ524: the hook-resolved cwd overrides the static local cwd.
|
|
8043
|
-
...effectiveCwd ? { cwd: effectiveCwd } : {},
|
|
8106
|
+
...this.effectiveCwd ? { cwd: this.effectiveCwd } : {},
|
|
8044
8107
|
// CT1103: the prepare-hook env plus the authoritative host-access token.
|
|
8045
|
-
env: turnEnv,
|
|
8046
|
-
mcpServers: resolvedMcpServers,
|
|
8108
|
+
env: this.turnEnv,
|
|
8109
|
+
mcpServers: this.resolvedMcpServers,
|
|
8047
8110
|
turnControlServer,
|
|
8048
8111
|
// CT238: this turn's conversation, forwarded as the active-conversation
|
|
8049
8112
|
// header so a cross-thread post/spawn stamps its origin.
|
|
@@ -8054,6 +8117,9 @@ ${reason}`,
|
|
|
8054
8117
|
// CT289: the operator's auto-memory escape hatch (machine-local), when set.
|
|
8055
8118
|
...this.opts.local.claudeCode ? { claudeCode: this.opts.local.claudeCode } : {}
|
|
8056
8119
|
});
|
|
8120
|
+
}
|
|
8121
|
+
async selectAdapter() {
|
|
8122
|
+
const { payload, workspaceId, turnId, turnLog } = this;
|
|
8057
8123
|
const onWarn = (msg, meta) => turnLog.warn(meta ?? {}, msg);
|
|
8058
8124
|
const adapters = [];
|
|
8059
8125
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
@@ -8072,9 +8138,8 @@ ${reason}`,
|
|
|
8072
8138
|
);
|
|
8073
8139
|
}
|
|
8074
8140
|
const registry = createAdapterRegistry(adapters);
|
|
8075
|
-
let adapter;
|
|
8076
8141
|
try {
|
|
8077
|
-
adapter = selectAdapter(registry, turnContext.runtime);
|
|
8142
|
+
this.adapter = selectAdapter(registry, this.turnContext.runtime);
|
|
8078
8143
|
} catch (err) {
|
|
8079
8144
|
if (!(err instanceof RuntimeUnavailableError)) throw err;
|
|
8080
8145
|
turnLog.error(
|
|
@@ -8093,17 +8158,18 @@ ${reason}`,
|
|
|
8093
8158
|
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
8094
8159
|
"dispatcher: runtime-unavailable notice post failed"
|
|
8095
8160
|
);
|
|
8161
|
+
const reason = `runtime_unavailable:${err.runtime}`;
|
|
8162
|
+
throw this.concluded(reason, reason);
|
|
8096
8163
|
}
|
|
8097
|
-
|
|
8098
|
-
return this.concludeBeforeRun(
|
|
8099
|
-
payload,
|
|
8100
|
-
turnLog,
|
|
8101
|
-
startedAt,
|
|
8102
|
-
`runtime_unavailable:${err.runtime}`
|
|
8103
|
-
);
|
|
8164
|
+
throw this.concluded(`runtime_unavailable:${err.runtime}`);
|
|
8104
8165
|
}
|
|
8166
|
+
}
|
|
8167
|
+
async execute() {
|
|
8168
|
+
const { payload, workspaceId, turnId, turnLog, startedAt } = this;
|
|
8169
|
+
const o = this.outcome;
|
|
8170
|
+
const abortController = this.abortController;
|
|
8105
8171
|
const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
|
|
8106
|
-
const transcript = this.opts.transcriptDir ? new TranscriptWriter(
|
|
8172
|
+
const transcript = this.transcript = this.opts.transcriptDir ? new TranscriptWriter(
|
|
8107
8173
|
this.opts.transcriptDir,
|
|
8108
8174
|
{
|
|
8109
8175
|
ts: new Date(startedAt).toISOString(),
|
|
@@ -8111,36 +8177,13 @@ ${reason}`,
|
|
|
8111
8177
|
workspaceId,
|
|
8112
8178
|
conversationId: payload.conversationId,
|
|
8113
8179
|
agentId: payload.agentId,
|
|
8114
|
-
dispatchId,
|
|
8115
|
-
message: message.body
|
|
8180
|
+
dispatchId: this.dispatchId,
|
|
8181
|
+
message: this.turnContext.message.body
|
|
8116
8182
|
},
|
|
8117
8183
|
(m) => turnLog.warn(m)
|
|
8118
8184
|
) : null;
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
let sessionWriteRejected = false;
|
|
8122
|
-
let okResult = false;
|
|
8123
|
-
let resultReason;
|
|
8124
|
-
const turnRuntime = turnContext.runtime;
|
|
8125
|
-
let turnUsage;
|
|
8126
|
-
let turnResolvedModel;
|
|
8127
|
-
let turnResolvedConfig;
|
|
8128
|
-
let turnMcpInventory;
|
|
8129
|
-
const eventCounts = {
|
|
8130
|
-
session: 0,
|
|
8131
|
-
text: 0,
|
|
8132
|
-
thinking: 0,
|
|
8133
|
-
tool: 0,
|
|
8134
|
-
result: 0
|
|
8135
|
-
};
|
|
8136
|
-
let runtimeResultKind = null;
|
|
8137
|
-
let contentBearingEvents = 0;
|
|
8138
|
-
let latestSessionState = request.session;
|
|
8139
|
-
let settledDiagnostics = null;
|
|
8140
|
-
let silentMarkerEmitted = false;
|
|
8141
|
-
let noteCommitFailed = () => {
|
|
8142
|
-
};
|
|
8143
|
-
const committer = new TurnCommitter({
|
|
8185
|
+
const turnRuntime = this.turnContext.runtime;
|
|
8186
|
+
const committer = this.committer = new TurnCommitter({
|
|
8144
8187
|
api: this.opts.api,
|
|
8145
8188
|
workspaceId,
|
|
8146
8189
|
conversationId: payload.conversationId,
|
|
@@ -8151,29 +8194,32 @@ ${reason}`,
|
|
|
8151
8194
|
parentMessageId: payload.messageId,
|
|
8152
8195
|
signal: abortController.signal,
|
|
8153
8196
|
log: turnLog,
|
|
8154
|
-
nextSeq,
|
|
8197
|
+
nextSeq: this.nextSeq,
|
|
8155
8198
|
// The committer reads this at commit to carry the addressed send on the
|
|
8156
8199
|
// terminal row; the server writes the send itself as a distinct message.
|
|
8157
|
-
sendState,
|
|
8200
|
+
sendState: this.sendState,
|
|
8158
8201
|
// CT326: likewise the ask payload. CT1281: the server writes the ask as its
|
|
8159
8202
|
// own addressed message to the human — which ENQUEUES like any other send —
|
|
8160
8203
|
// and creates the `asks` row against that carrier, not against turn speech.
|
|
8161
|
-
askState,
|
|
8204
|
+
askState: this.askState,
|
|
8162
8205
|
// CT442: likewise the wake payload — attached to the `final` row so the
|
|
8163
8206
|
// server arms the wake schedule atomically with the reply it rode on.
|
|
8164
|
-
wakeState,
|
|
8165
|
-
replyState,
|
|
8207
|
+
wakeState: this.wakeState,
|
|
8208
|
+
replyState: this.replyState,
|
|
8166
8209
|
// The ledger-derived owed reply, for the runtime's own declaration when
|
|
8167
8210
|
// the agent doesn't call `reply_to` (resolveDeclaredReplyField).
|
|
8168
|
-
owedReplyMessageId: turnContext.owedReplyMessageId ?? null,
|
|
8211
|
+
owedReplyMessageId: this.turnContext.owedReplyMessageId ?? null,
|
|
8169
8212
|
// CT1292: a commit that didn't land may mean this turn's lease is gone.
|
|
8170
|
-
|
|
8213
|
+
// `noteCommitFailed` is a field slot the lease watchdog fills when it
|
|
8214
|
+
// arms (it starts as a no-op) — the committer only fires it from inside
|
|
8215
|
+
// the adapter loop, which runs after the watchdog has replaced it.
|
|
8216
|
+
onCommitFailed: (err) => this.noteCommitFailed(err)
|
|
8171
8217
|
});
|
|
8172
8218
|
const usesHttpTurnControl = turnRuntime === "codex" || turnRuntime === "opencode";
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
turnControlIntentFetched = true;
|
|
8219
|
+
this.applyRecordedTurnControlIntent = async () => {
|
|
8220
|
+
if (this.turnControlIntentFetched || !usesHttpTurnControl || !this.turnContext.turnToken)
|
|
8221
|
+
return;
|
|
8222
|
+
this.turnControlIntentFetched = true;
|
|
8177
8223
|
try {
|
|
8178
8224
|
const intent = await this.opts.api.getTurnIntent(
|
|
8179
8225
|
workspaceId,
|
|
@@ -8182,44 +8228,44 @@ ${reason}`,
|
|
|
8182
8228
|
turnId
|
|
8183
8229
|
);
|
|
8184
8230
|
if (intent.ask) {
|
|
8185
|
-
askState.targetUserId = intent.ask.targetUserId;
|
|
8231
|
+
this.askState.targetUserId = intent.ask.targetUserId;
|
|
8186
8232
|
if (intent.ask.questions && intent.ask.questions.length > 0) {
|
|
8187
|
-
askState.questions = intent.ask.questions;
|
|
8188
|
-
askState.question = null;
|
|
8189
|
-
askState.headline = null;
|
|
8190
|
-
askState.options = null;
|
|
8233
|
+
this.askState.questions = intent.ask.questions;
|
|
8234
|
+
this.askState.question = null;
|
|
8235
|
+
this.askState.headline = null;
|
|
8236
|
+
this.askState.options = null;
|
|
8191
8237
|
} else {
|
|
8192
|
-
askState.question = intent.ask.question ?? null;
|
|
8193
|
-
askState.headline = intent.ask.headline ?? null;
|
|
8194
|
-
askState.options = intent.ask.options ?? null;
|
|
8195
|
-
askState.questions = null;
|
|
8238
|
+
this.askState.question = intent.ask.question ?? null;
|
|
8239
|
+
this.askState.headline = intent.ask.headline ?? null;
|
|
8240
|
+
this.askState.options = intent.ask.options ?? null;
|
|
8241
|
+
this.askState.questions = null;
|
|
8196
8242
|
}
|
|
8197
8243
|
}
|
|
8198
8244
|
if (intent.wake) {
|
|
8199
8245
|
if ("cancel" in intent.wake) {
|
|
8200
|
-
wakeState.cancelled = true;
|
|
8201
|
-
wakeState.afterSeconds = null;
|
|
8202
|
-
wakeState.at = null;
|
|
8203
|
-
wakeState.note = null;
|
|
8246
|
+
this.wakeState.cancelled = true;
|
|
8247
|
+
this.wakeState.afterSeconds = null;
|
|
8248
|
+
this.wakeState.at = null;
|
|
8249
|
+
this.wakeState.note = null;
|
|
8204
8250
|
} else {
|
|
8205
|
-
wakeState.cancelled = false;
|
|
8206
|
-
wakeState.afterSeconds = intent.wake.afterSeconds ?? null;
|
|
8207
|
-
wakeState.at = intent.wake.at ?? null;
|
|
8208
|
-
wakeState.note = intent.wake.note;
|
|
8251
|
+
this.wakeState.cancelled = false;
|
|
8252
|
+
this.wakeState.afterSeconds = intent.wake.afterSeconds ?? null;
|
|
8253
|
+
this.wakeState.at = intent.wake.at ?? null;
|
|
8254
|
+
this.wakeState.note = intent.wake.note;
|
|
8209
8255
|
}
|
|
8210
8256
|
}
|
|
8211
8257
|
if (intent.sendAgentId && intent.sendBody) {
|
|
8212
|
-
sendState.agentId = intent.sendAgentId;
|
|
8213
|
-
sendState.message = intent.sendBody;
|
|
8214
|
-
sendState.order = intent.sendOrder ?? null;
|
|
8258
|
+
this.sendState.agentId = intent.sendAgentId;
|
|
8259
|
+
this.sendState.message = intent.sendBody;
|
|
8260
|
+
this.sendState.order = intent.sendOrder ?? null;
|
|
8215
8261
|
}
|
|
8216
8262
|
if (intent.answersMessageId) {
|
|
8217
|
-
replyState.answersMessageId = intent.answersMessageId;
|
|
8218
|
-
replyState.order = intent.replyOrder ?? null;
|
|
8263
|
+
this.replyState.answersMessageId = intent.answersMessageId;
|
|
8264
|
+
this.replyState.order = intent.replyOrder ?? null;
|
|
8219
8265
|
}
|
|
8220
8266
|
if (intent.skipped) {
|
|
8221
|
-
skipState.skipped = true;
|
|
8222
|
-
skipState.reason = intent.skipReason;
|
|
8267
|
+
this.skipState.skipped = true;
|
|
8268
|
+
this.skipState.reason = intent.skipReason;
|
|
8223
8269
|
}
|
|
8224
8270
|
} catch (err) {
|
|
8225
8271
|
turnLog.warn(
|
|
@@ -8231,7 +8277,7 @@ ${reason}`,
|
|
|
8231
8277
|
const idleTimeoutMs = this.opts.idleTimeoutMs ?? DEFAULT_AGENT_IDLE_TIMEOUT_MS;
|
|
8232
8278
|
const fireTimeout = (reason) => {
|
|
8233
8279
|
if (abortController.signal.aborted) return;
|
|
8234
|
-
timeoutReason = reason;
|
|
8280
|
+
o.timeoutReason = reason;
|
|
8235
8281
|
turnLog.warn(
|
|
8236
8282
|
{ reason, idleTimeoutMs, totalTimeoutMs },
|
|
8237
8283
|
"dispatcher: turn timeout \u2014 aborting"
|
|
@@ -8250,7 +8296,7 @@ ${reason}`,
|
|
|
8250
8296
|
const leaseRenewalMs = this.opts.leaseRenewalMs ?? DEFAULT_LEASE_RENEWAL_MS;
|
|
8251
8297
|
let leaseCheckInFlight = false;
|
|
8252
8298
|
const checkLease = async (trigger) => {
|
|
8253
|
-
if (leaseLost || leaseCheckInFlight || abortController.signal.aborted) return;
|
|
8299
|
+
if (o.leaseLost || leaseCheckInFlight || abortController.signal.aborted) return;
|
|
8254
8300
|
leaseCheckInFlight = true;
|
|
8255
8301
|
try {
|
|
8256
8302
|
const state = await this.opts.api.checkTurnLease(
|
|
@@ -8261,7 +8307,7 @@ ${reason}`,
|
|
|
8261
8307
|
);
|
|
8262
8308
|
if (state !== "ended") return;
|
|
8263
8309
|
if (abortController.signal.aborted) return;
|
|
8264
|
-
leaseLost = true;
|
|
8310
|
+
o.leaseLost = true;
|
|
8265
8311
|
turnLog.warn(
|
|
8266
8312
|
{ turnId, trigger },
|
|
8267
8313
|
"dispatcher: this turn is no longer running server-side \u2014 aborting the loop"
|
|
@@ -8273,26 +8319,31 @@ ${reason}`,
|
|
|
8273
8319
|
};
|
|
8274
8320
|
const leaseTimer = setInterval(() => void checkLease("cadence"), leaseRenewalMs);
|
|
8275
8321
|
leaseTimer.unref?.();
|
|
8276
|
-
noteCommitFailed = (err) => {
|
|
8322
|
+
this.noteCommitFailed = (err) => {
|
|
8277
8323
|
if (isWriteFenceRefusal(err)) void checkLease("commit_refused");
|
|
8278
8324
|
};
|
|
8325
|
+
this.disarmWatchdogs = () => {
|
|
8326
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
8327
|
+
clearTimeout(totalTimer);
|
|
8328
|
+
clearInterval(leaseTimer);
|
|
8329
|
+
};
|
|
8279
8330
|
try {
|
|
8280
|
-
for await (const event of adapter.runTurn(request, abortController.signal)) {
|
|
8331
|
+
for await (const event of this.adapter.runTurn(this.request, abortController.signal)) {
|
|
8281
8332
|
transcript?.write(event);
|
|
8282
|
-
eventCounts[event.type] += 1;
|
|
8283
|
-
if (isContentBearingEvent(event)) contentBearingEvents += 1;
|
|
8333
|
+
o.eventCounts[event.type] += 1;
|
|
8334
|
+
if (isContentBearingEvent(event)) o.contentBearingEvents += 1;
|
|
8284
8335
|
armIdle();
|
|
8285
8336
|
if (abortController.signal.aborted) {
|
|
8286
8337
|
turnLog.info("dispatcher: aborted mid-turn");
|
|
8287
|
-
okResult = false;
|
|
8288
|
-
resultReason = timeoutReason ?? "cancelled";
|
|
8338
|
+
o.okResult = false;
|
|
8339
|
+
o.resultReason = o.timeoutReason ?? "cancelled";
|
|
8289
8340
|
break;
|
|
8290
8341
|
}
|
|
8291
8342
|
if (event.type === "session") {
|
|
8292
|
-
latestSessionState = event.state;
|
|
8293
|
-
if (event.degraded) sessionDegraded = true;
|
|
8294
|
-
if (!sessionWritten) {
|
|
8295
|
-
sessionWritten = true;
|
|
8343
|
+
o.latestSessionState = event.state;
|
|
8344
|
+
if (event.degraded) o.sessionDegraded = true;
|
|
8345
|
+
if (!o.sessionWritten) {
|
|
8346
|
+
o.sessionWritten = true;
|
|
8296
8347
|
try {
|
|
8297
8348
|
await this.opts.api.setActiveRun(
|
|
8298
8349
|
workspaceId,
|
|
@@ -8303,7 +8354,7 @@ ${reason}`,
|
|
|
8303
8354
|
} catch (err) {
|
|
8304
8355
|
const status = err instanceof ApiError ? err.status : void 0;
|
|
8305
8356
|
if (status !== void 0 && status >= 400 && status < 500) {
|
|
8306
|
-
sessionWriteRejected = true;
|
|
8357
|
+
o.sessionWriteRejected = true;
|
|
8307
8358
|
turnLog.error(
|
|
8308
8359
|
{
|
|
8309
8360
|
err: err instanceof Error ? err.message : String(err),
|
|
@@ -8323,48 +8374,52 @@ ${reason}`,
|
|
|
8323
8374
|
}
|
|
8324
8375
|
}
|
|
8325
8376
|
} else if (event.type === "result") {
|
|
8326
|
-
okResult = event.ok;
|
|
8327
|
-
resultReason = event.reason;
|
|
8328
|
-
turnUsage = event.usage;
|
|
8329
|
-
turnResolvedModel = event.resolvedModel;
|
|
8330
|
-
turnResolvedConfig = event.resolvedConfig;
|
|
8331
|
-
turnMcpInventory = event.mcpInventory;
|
|
8332
|
-
runtimeResultKind = event.ok ? "success" : event.reason === "no_terminal" ? "no_terminal" : "error";
|
|
8333
|
-
} else if (event.type === "text" && skipState.skipped) {
|
|
8377
|
+
o.okResult = event.ok;
|
|
8378
|
+
o.resultReason = event.reason;
|
|
8379
|
+
o.turnUsage = event.usage;
|
|
8380
|
+
o.turnResolvedModel = event.resolvedModel;
|
|
8381
|
+
o.turnResolvedConfig = event.resolvedConfig;
|
|
8382
|
+
o.turnMcpInventory = event.mcpInventory;
|
|
8383
|
+
o.runtimeResultKind = event.ok ? "success" : event.reason === "no_terminal" ? "no_terminal" : "error";
|
|
8384
|
+
} else if (event.type === "text" && this.skipState.skipped) {
|
|
8334
8385
|
} else {
|
|
8335
8386
|
if (event.type === "text" && event.terminal) {
|
|
8336
|
-
await applyRecordedTurnControlIntent();
|
|
8387
|
+
await this.applyRecordedTurnControlIntent();
|
|
8337
8388
|
}
|
|
8338
8389
|
await committer.ingestEvent(event);
|
|
8339
8390
|
}
|
|
8340
8391
|
}
|
|
8341
8392
|
if (abortController.signal.aborted) {
|
|
8342
|
-
okResult = false;
|
|
8343
|
-
resultReason = leaseLost ? "lease_lost" : timeoutReason ?? "cancelled";
|
|
8393
|
+
o.okResult = false;
|
|
8394
|
+
o.resultReason = o.leaseLost ? "lease_lost" : o.timeoutReason ?? "cancelled";
|
|
8344
8395
|
}
|
|
8345
|
-
const emptyResultReason = !skipState.skipped && classifyEmptyResult({
|
|
8396
|
+
const emptyResultReason = !this.skipState.skipped && classifyEmptyResult({
|
|
8397
|
+
ok: o.okResult,
|
|
8398
|
+
contentBearingEvents: o.contentBearingEvents,
|
|
8399
|
+
usage: o.turnUsage
|
|
8400
|
+
});
|
|
8346
8401
|
if (emptyResultReason) {
|
|
8347
|
-
okResult = false;
|
|
8348
|
-
resultReason = emptyResultReason;
|
|
8402
|
+
o.okResult = false;
|
|
8403
|
+
o.resultReason = emptyResultReason;
|
|
8349
8404
|
}
|
|
8350
|
-
if (!okResult && !resultReason) {
|
|
8351
|
-
resultReason = "no_result";
|
|
8405
|
+
if (!o.okResult && !o.resultReason) {
|
|
8406
|
+
o.resultReason = "no_result";
|
|
8352
8407
|
}
|
|
8353
8408
|
if (!abortController.signal.aborted) {
|
|
8354
|
-
await applyRecordedTurnControlIntent();
|
|
8409
|
+
await this.applyRecordedTurnControlIntent();
|
|
8355
8410
|
}
|
|
8356
|
-
if (!abortController.signal.aborted && skipState.skipped) {
|
|
8411
|
+
if (!abortController.signal.aborted && this.skipState.skipped) {
|
|
8357
8412
|
turnLog.info(
|
|
8358
|
-
{ reason: skipState.reason, turnId, ok: okResult },
|
|
8413
|
+
{ reason: this.skipState.reason, turnId, ok: o.okResult },
|
|
8359
8414
|
"agent skipped turn (skip_turn)"
|
|
8360
8415
|
);
|
|
8361
|
-
const { wake: skipWake } = wakeCommitField(wakeState);
|
|
8416
|
+
const { wake: skipWake } = wakeCommitField(this.wakeState);
|
|
8362
8417
|
try {
|
|
8363
8418
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8364
8419
|
body: SKIPPED_MARKER_BODY,
|
|
8365
8420
|
kind: "skipped",
|
|
8366
8421
|
turnId,
|
|
8367
|
-
seq: nextSeq(),
|
|
8422
|
+
seq: this.nextSeq(),
|
|
8368
8423
|
parentMessageId: payload.messageId,
|
|
8369
8424
|
...skipWake ? { wake: skipWake } : {}
|
|
8370
8425
|
});
|
|
@@ -8375,21 +8430,21 @@ ${reason}`,
|
|
|
8375
8430
|
);
|
|
8376
8431
|
}
|
|
8377
8432
|
} else {
|
|
8378
|
-
await committer.finalize(okResult);
|
|
8379
|
-
if (!abortController.signal.aborted && okResult && !committer.finalEmitted) {
|
|
8433
|
+
await committer.finalize(o.okResult);
|
|
8434
|
+
if (!abortController.signal.aborted && o.okResult && !committer.finalEmitted) {
|
|
8380
8435
|
try {
|
|
8381
8436
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8382
8437
|
body: SILENT_MARKER_BODY,
|
|
8383
8438
|
kind: "silent",
|
|
8384
8439
|
turnId,
|
|
8385
|
-
seq: nextSeq(),
|
|
8440
|
+
seq: this.nextSeq(),
|
|
8386
8441
|
parentMessageId: payload.messageId,
|
|
8387
8442
|
// ask, wake and send must survive a wordless turn exactly as
|
|
8388
8443
|
// they survive a textual final; dropping one can strand a person
|
|
8389
8444
|
// or the next actor with no visible failure.
|
|
8390
8445
|
...committer.turnControlFields("silent")
|
|
8391
8446
|
});
|
|
8392
|
-
silentMarkerEmitted = true;
|
|
8447
|
+
o.silentMarkerEmitted = true;
|
|
8393
8448
|
} catch (err) {
|
|
8394
8449
|
turnLog.warn(
|
|
8395
8450
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
@@ -8399,27 +8454,25 @@ ${reason}`,
|
|
|
8399
8454
|
}
|
|
8400
8455
|
}
|
|
8401
8456
|
} catch (err) {
|
|
8402
|
-
okResult = false;
|
|
8403
|
-
resultReason = err instanceof Error ? err.message : String(err);
|
|
8404
|
-
turnLog.error({ err: resultReason }, "dispatcher: SDK query threw");
|
|
8457
|
+
o.okResult = false;
|
|
8458
|
+
o.resultReason = err instanceof Error ? err.message : String(err);
|
|
8459
|
+
turnLog.error({ err: o.resultReason }, "dispatcher: SDK query threw");
|
|
8405
8460
|
} finally {
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
resultReason = "lease_lost";
|
|
8411
|
-
okResult = false;
|
|
8461
|
+
this.disarmWatchdogs();
|
|
8462
|
+
if (o.leaseLost) {
|
|
8463
|
+
o.resultReason = "lease_lost";
|
|
8464
|
+
o.okResult = false;
|
|
8412
8465
|
}
|
|
8413
|
-
const userCancelled = abortController.signal.aborted && timeoutReason === null && !leaseLost;
|
|
8414
|
-
if (timeoutReason !== null) {
|
|
8415
|
-
resultReason = timeoutReason;
|
|
8416
|
-
okResult = false;
|
|
8417
|
-
if (timeoutReason === "timeout_idle") {
|
|
8466
|
+
const userCancelled = abortController.signal.aborted && o.timeoutReason === null && !o.leaseLost;
|
|
8467
|
+
if (o.timeoutReason !== null) {
|
|
8468
|
+
o.resultReason = o.timeoutReason;
|
|
8469
|
+
o.okResult = false;
|
|
8470
|
+
if (o.timeoutReason === "timeout_idle") {
|
|
8418
8471
|
const health = this.opts.connectorHealth?.lookup(turnRuntime);
|
|
8419
8472
|
if (health?.quotaState === "limited" && health.limitedUntil) {
|
|
8420
8473
|
const resetMs = Date.parse(health.limitedUntil);
|
|
8421
8474
|
if (Number.isFinite(resetMs) && resetMs > Date.now()) {
|
|
8422
|
-
resultReason = encodeFailureReason({
|
|
8475
|
+
o.resultReason = encodeFailureReason({
|
|
8423
8476
|
kind: "usage_capped",
|
|
8424
8477
|
resetsAt: health.limitedUntil
|
|
8425
8478
|
});
|
|
@@ -8443,42 +8496,41 @@ ${reason}`,
|
|
|
8443
8496
|
// turn is just as over, and its evidence must die with it.
|
|
8444
8497
|
settledMessageId: payload.messageId
|
|
8445
8498
|
};
|
|
8446
|
-
if (turnUsage) {
|
|
8447
|
-
body.usage = turnUsage;
|
|
8499
|
+
if (o.turnUsage) {
|
|
8500
|
+
body.usage = o.turnUsage;
|
|
8448
8501
|
}
|
|
8449
|
-
if (turnResolvedModel) {
|
|
8450
|
-
body.resolvedModel = turnResolvedModel;
|
|
8502
|
+
if (o.turnResolvedModel) {
|
|
8503
|
+
body.resolvedModel = o.turnResolvedModel;
|
|
8451
8504
|
}
|
|
8452
|
-
if (turnResolvedConfig && Object.keys(turnResolvedConfig).length > 0) {
|
|
8453
|
-
body.resolvedConfig = turnResolvedConfig;
|
|
8505
|
+
if (o.turnResolvedConfig && Object.keys(o.turnResolvedConfig).length > 0) {
|
|
8506
|
+
body.resolvedConfig = o.turnResolvedConfig;
|
|
8454
8507
|
}
|
|
8455
|
-
if (!okResult && resultReason && resultReason !== "cancelled" && !userCancelled && !leaseLost && !skipState.skipped) {
|
|
8456
|
-
body.errorReason = resultReason.slice(0, 200);
|
|
8508
|
+
if (!o.okResult && o.resultReason && o.resultReason !== "cancelled" && !userCancelled && !o.leaseLost && !this.skipState.skipped) {
|
|
8509
|
+
body.errorReason = o.resultReason.slice(0, 200);
|
|
8457
8510
|
}
|
|
8458
|
-
if (okResult) {
|
|
8511
|
+
if (o.okResult) {
|
|
8459
8512
|
body.lastSeenMessageId = payload.messageId;
|
|
8460
8513
|
}
|
|
8461
|
-
body.outcome = okResult ? "settled" : body.errorReason ? "failed" : "interrupted";
|
|
8462
|
-
if (sessionDegraded) {
|
|
8514
|
+
body.outcome = o.okResult ? "settled" : body.errorReason ? "failed" : "interrupted";
|
|
8515
|
+
if (o.sessionDegraded) {
|
|
8463
8516
|
body.degraded = true;
|
|
8464
8517
|
}
|
|
8465
|
-
if (sessionWriteRejected &&
|
|
8518
|
+
if (o.sessionWriteRejected && this.supervisor.claimSessionWriteNotice()) {
|
|
8466
8519
|
body.sessionWriteRejected = true;
|
|
8467
|
-
this.sessionWriteNotified.add(key);
|
|
8468
8520
|
}
|
|
8469
|
-
const outcome = skipState.skipped ? "skipped" : userCancelled || leaseLost ? "cancelled" : okResult ? "success" : "failure";
|
|
8470
|
-
const diagnosticReason = outcome === "skipped" ? { kind: "skipped" } : outcome === "cancelled" ? leaseLost ? { kind: "lease_lost" } : { kind: "cancelled" } : outcome === "failure" ? normalizeTurnResultReason(resultReason) : null;
|
|
8471
|
-
settledDiagnostics = {
|
|
8521
|
+
const outcome = this.skipState.skipped ? "skipped" : userCancelled || o.leaseLost ? "cancelled" : o.okResult ? "success" : "failure";
|
|
8522
|
+
const diagnosticReason = outcome === "skipped" ? { kind: "skipped" } : outcome === "cancelled" ? o.leaseLost ? { kind: "lease_lost" } : { kind: "cancelled" } : outcome === "failure" ? normalizeTurnResultReason(o.resultReason) : null;
|
|
8523
|
+
o.settledDiagnostics = {
|
|
8472
8524
|
outcome,
|
|
8473
8525
|
resultReason: diagnosticReason,
|
|
8474
|
-
sessionMode: sessionDegraded ? "degraded" : request.session ? "resumed" : "fresh",
|
|
8475
|
-
sessionFingerprint: fingerprintSessionState(latestSessionState),
|
|
8476
|
-
eventCounts,
|
|
8477
|
-
runtimeResultKind,
|
|
8478
|
-
...turnMcpInventory ? { mcpInventory: turnMcpInventory } : {},
|
|
8479
|
-
finalSource: outcome === "skipped" || outcome === "cancelled" || silentMarkerEmitted ? "marker" : committer.finalSource
|
|
8526
|
+
sessionMode: o.sessionDegraded ? "degraded" : this.request.session ? "resumed" : "fresh",
|
|
8527
|
+
sessionFingerprint: fingerprintSessionState(o.latestSessionState),
|
|
8528
|
+
eventCounts: o.eventCounts,
|
|
8529
|
+
runtimeResultKind: o.runtimeResultKind,
|
|
8530
|
+
...o.turnMcpInventory ? { mcpInventory: o.turnMcpInventory } : {},
|
|
8531
|
+
finalSource: outcome === "skipped" || outcome === "cancelled" || o.silentMarkerEmitted ? "marker" : committer.finalSource
|
|
8480
8532
|
};
|
|
8481
|
-
body.diagnostics = settledDiagnostics;
|
|
8533
|
+
body.diagnostics = o.settledDiagnostics;
|
|
8482
8534
|
if (diagnosticReason && !["usage_capped", "rate_limited", "auth_expired", "cancelled", "skipped"].includes(
|
|
8483
8535
|
diagnosticReason.kind
|
|
8484
8536
|
)) {
|
|
@@ -8489,16 +8541,16 @@ ${reason}`,
|
|
|
8489
8541
|
conversationId: payload.conversationId,
|
|
8490
8542
|
agentId: payload.agentId,
|
|
8491
8543
|
runtime: turnRuntime,
|
|
8492
|
-
model: turnResolvedModel ?? null,
|
|
8493
|
-
usage: turnUsage ?? null,
|
|
8494
|
-
hadStoredSession: request.session != null,
|
|
8495
|
-
diagnostics: settledDiagnostics
|
|
8544
|
+
model: o.turnResolvedModel ?? null,
|
|
8545
|
+
usage: o.turnUsage ?? null,
|
|
8546
|
+
hadStoredSession: this.request.session != null,
|
|
8547
|
+
diagnostics: o.settledDiagnostics
|
|
8496
8548
|
},
|
|
8497
8549
|
"dispatcher: anomalous turn settled"
|
|
8498
8550
|
);
|
|
8499
8551
|
}
|
|
8500
8552
|
this.opts.connectorHealth?.recordSettle(turnRuntime, {
|
|
8501
|
-
ok: okResult,
|
|
8553
|
+
ok: o.okResult,
|
|
8502
8554
|
errorReason: body.errorReason ?? null
|
|
8503
8555
|
});
|
|
8504
8556
|
try {
|
|
@@ -8514,45 +8566,151 @@ ${reason}`,
|
|
|
8514
8566
|
"dispatcher: active-run clear failed terminally; server age-sweep is the backstop"
|
|
8515
8567
|
);
|
|
8516
8568
|
}
|
|
8517
|
-
releaseAbort();
|
|
8569
|
+
this.supervisor.releaseAbort(turnId, abortController);
|
|
8518
8570
|
}
|
|
8571
|
+
}
|
|
8572
|
+
report() {
|
|
8573
|
+
const o = this.outcome;
|
|
8574
|
+
const { startedAt, turnLog } = this;
|
|
8519
8575
|
const durationMs = Date.now() - startedAt;
|
|
8520
|
-
const finalReplyBody = committer.replyBody;
|
|
8521
|
-
if (transcript) {
|
|
8522
|
-
transcript.close({
|
|
8523
|
-
ok: okResult,
|
|
8524
|
-
...resultReason ? { reason: resultReason } : {},
|
|
8576
|
+
const finalReplyBody = this.committer.replyBody;
|
|
8577
|
+
if (this.transcript) {
|
|
8578
|
+
this.transcript.close({
|
|
8579
|
+
ok: o.okResult,
|
|
8580
|
+
...o.resultReason ? { reason: o.resultReason } : {},
|
|
8525
8581
|
durationMs
|
|
8526
8582
|
});
|
|
8527
|
-
if (!okResult && resultReason !== "cancelled") {
|
|
8528
|
-
turnLog.info(`turn failed \u2014 full transcript: ${transcript.path}`);
|
|
8583
|
+
if (!o.okResult && o.resultReason !== "cancelled") {
|
|
8584
|
+
turnLog.info(`turn failed \u2014 full this.transcript: ${this.transcript.path}`);
|
|
8529
8585
|
}
|
|
8530
|
-
if (settledDiagnostics?.resultReason?.kind === "empty_result" || settledDiagnostics?.resultReason?.kind === "empty_result_unverified") {
|
|
8531
|
-
transcript.preserveAnomaly();
|
|
8586
|
+
if (o.settledDiagnostics?.resultReason?.kind === "empty_result" || o.settledDiagnostics?.resultReason?.kind === "empty_result_unverified") {
|
|
8587
|
+
this.transcript.preserveAnomaly();
|
|
8532
8588
|
}
|
|
8533
8589
|
}
|
|
8534
|
-
if (okResult) {
|
|
8590
|
+
if (o.okResult) {
|
|
8535
8591
|
turnLog.debug({ durationMs }, "dispatcher: turn end (ok)");
|
|
8536
|
-
this.notifyEnd({
|
|
8537
|
-
id: dispatchId,
|
|
8592
|
+
this.supervisor.notifyEnd({
|
|
8593
|
+
id: this.dispatchId,
|
|
8538
8594
|
ok: true,
|
|
8539
8595
|
durationMs,
|
|
8540
8596
|
...finalReplyBody ? { reply: finalReplyBody.trim() } : {}
|
|
8541
8597
|
});
|
|
8542
8598
|
return { ok: true, durationMs };
|
|
8543
8599
|
}
|
|
8544
|
-
turnLog.debug({ durationMs, reason: resultReason }, "dispatcher: turn end (not ok)");
|
|
8545
|
-
this.notifyEnd({
|
|
8546
|
-
id: dispatchId,
|
|
8600
|
+
turnLog.debug({ durationMs, reason: o.resultReason }, "dispatcher: turn end (not ok)");
|
|
8601
|
+
this.supervisor.notifyEnd({
|
|
8602
|
+
id: this.dispatchId,
|
|
8547
8603
|
ok: false,
|
|
8548
8604
|
durationMs,
|
|
8549
|
-
...resultReason ? { reason: resultReason } : {}
|
|
8605
|
+
...o.resultReason ? { reason: o.resultReason } : {}
|
|
8550
8606
|
});
|
|
8551
8607
|
return {
|
|
8552
8608
|
ok: false,
|
|
8553
8609
|
durationMs,
|
|
8554
|
-
...resultReason ? { reason: resultReason } : {}
|
|
8610
|
+
...o.resultReason ? { reason: o.resultReason } : {}
|
|
8611
|
+
};
|
|
8612
|
+
}
|
|
8613
|
+
};
|
|
8614
|
+
function fingerprintSessionState(state) {
|
|
8615
|
+
if (!state) return null;
|
|
8616
|
+
let opaqueId = state;
|
|
8617
|
+
try {
|
|
8618
|
+
const parsed = JSON.parse(state);
|
|
8619
|
+
const candidate = parsed.sdkSessionId ?? parsed.threadId ?? parsed.sessionId;
|
|
8620
|
+
if (typeof candidate === "string" && candidate.length > 0) opaqueId = candidate;
|
|
8621
|
+
} catch {
|
|
8622
|
+
}
|
|
8623
|
+
return createHash2("sha256").update(opaqueId).digest("hex").slice(0, 16);
|
|
8624
|
+
}
|
|
8625
|
+
|
|
8626
|
+
// src/dispatcher.ts
|
|
8627
|
+
function checkoutState(cwd) {
|
|
8628
|
+
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
8629
|
+
if (!existsSync11(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
8630
|
+
let entries;
|
|
8631
|
+
try {
|
|
8632
|
+
entries = readdirSync2(cwd);
|
|
8633
|
+
} catch (error) {
|
|
8634
|
+
return { ok: false, reason: `${cwd} is unreadable (${error.message})` };
|
|
8635
|
+
}
|
|
8636
|
+
if (entries.length === 0) {
|
|
8637
|
+
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
8638
|
+
}
|
|
8639
|
+
const gitPath = join14(cwd, ".git");
|
|
8640
|
+
if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
|
|
8641
|
+
let stat;
|
|
8642
|
+
try {
|
|
8643
|
+
stat = statSync(gitPath);
|
|
8644
|
+
} catch (error) {
|
|
8645
|
+
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
8646
|
+
}
|
|
8647
|
+
if (stat.isDirectory() && !existsSync11(join14(gitPath, "HEAD")))
|
|
8648
|
+
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
8649
|
+
return { ok: true, reason: "usable" };
|
|
8650
|
+
}
|
|
8651
|
+
function runKey(conversationId, agentId) {
|
|
8652
|
+
return `${conversationId}|${agentId}`;
|
|
8653
|
+
}
|
|
8654
|
+
var Dispatcher = class {
|
|
8655
|
+
constructor(opts) {
|
|
8656
|
+
this.opts = opts;
|
|
8657
|
+
this.aborts = opts.aborts ?? /* @__PURE__ */ new Map();
|
|
8658
|
+
}
|
|
8659
|
+
opts;
|
|
8660
|
+
// CT1288: keyed (conversation|agent) → per-TURN controllers. Stop is
|
|
8661
|
+
// pair-scoped and must reach every live loop for the pair.
|
|
8662
|
+
aborts;
|
|
8663
|
+
// CT1109: pairs already told "your session couldn't be saved" — the notice is
|
|
8664
|
+
// once per (conversation, agent), while the rejection repeats every turn.
|
|
8665
|
+
sessionWriteNotified = /* @__PURE__ */ new Set();
|
|
8666
|
+
notifyStart(info) {
|
|
8667
|
+
try {
|
|
8668
|
+
this.opts.observer?.onStart(info);
|
|
8669
|
+
} catch {
|
|
8670
|
+
}
|
|
8671
|
+
}
|
|
8672
|
+
notifyEnd(info) {
|
|
8673
|
+
try {
|
|
8674
|
+
this.opts.observer?.onEnd(info);
|
|
8675
|
+
} catch {
|
|
8676
|
+
}
|
|
8677
|
+
}
|
|
8678
|
+
// CT1288: `opts.turnId` is a RESUMED turn id — the id this event already ran
|
|
8679
|
+
// under before the companion died. Passing it makes the replay ask the
|
|
8680
|
+
// mailroom to re-admit the turn it already owns, which the server explicitly
|
|
8681
|
+
// supports ("a replay reports, it never re-points"). Omitted on a fresh
|
|
8682
|
+
// dispatch, where minting is correct.
|
|
8683
|
+
//
|
|
8684
|
+
// CT1261: a short `handle` creates a `TurnExecution` and runs it. The
|
|
8685
|
+
// supervisor seam hands the execution exactly the three cross-turn duties —
|
|
8686
|
+
// the abort registry, the session-write notice latch, the observer — and
|
|
8687
|
+
// nothing else.
|
|
8688
|
+
async handle(payload, opts = {}) {
|
|
8689
|
+
const key = runKey(payload.conversationId, payload.agentId);
|
|
8690
|
+
const supervisor = {
|
|
8691
|
+
registerAbort: (turnId, controller) => {
|
|
8692
|
+
let pairAborts = this.aborts.get(key);
|
|
8693
|
+
if (!pairAborts) {
|
|
8694
|
+
pairAborts = /* @__PURE__ */ new Map();
|
|
8695
|
+
this.aborts.set(key, pairAborts);
|
|
8696
|
+
}
|
|
8697
|
+
pairAborts.set(turnId, controller);
|
|
8698
|
+
},
|
|
8699
|
+
releaseAbort: (turnId, controller) => {
|
|
8700
|
+
const pair = this.aborts.get(key);
|
|
8701
|
+
if (pair?.get(turnId) !== controller) return;
|
|
8702
|
+
pair.delete(turnId);
|
|
8703
|
+
if (pair.size === 0) this.aborts.delete(key);
|
|
8704
|
+
},
|
|
8705
|
+
claimSessionWriteNotice: () => {
|
|
8706
|
+
if (this.sessionWriteNotified.has(key)) return false;
|
|
8707
|
+
this.sessionWriteNotified.add(key);
|
|
8708
|
+
return true;
|
|
8709
|
+
},
|
|
8710
|
+
notifyStart: (info) => this.notifyStart(info),
|
|
8711
|
+
notifyEnd: (info) => this.notifyEnd(info)
|
|
8555
8712
|
};
|
|
8713
|
+
return new TurnExecution(this.opts, supervisor, payload, opts).run();
|
|
8556
8714
|
}
|
|
8557
8715
|
// SJ383: cancel a specific (conversation, agent) run if one is in flight in
|
|
8558
8716
|
// THIS companion process. Returns true if an in-flight run was aborted.
|
|
@@ -8569,17 +8727,6 @@ ${reason}`,
|
|
|
8569
8727
|
return true;
|
|
8570
8728
|
}
|
|
8571
8729
|
};
|
|
8572
|
-
function fingerprintSessionState(state) {
|
|
8573
|
-
if (!state) return null;
|
|
8574
|
-
let opaqueId = state;
|
|
8575
|
-
try {
|
|
8576
|
-
const parsed = JSON.parse(state);
|
|
8577
|
-
const candidate = parsed.sdkSessionId ?? parsed.threadId ?? parsed.sessionId;
|
|
8578
|
-
if (typeof candidate === "string" && candidate.length > 0) opaqueId = candidate;
|
|
8579
|
-
} catch {
|
|
8580
|
-
}
|
|
8581
|
-
return createHash2("sha256").update(opaqueId).digest("hex").slice(0, 16);
|
|
8582
|
-
}
|
|
8583
8730
|
|
|
8584
8731
|
// src/opencode-models.ts
|
|
8585
8732
|
var OPENCODE_RUNTIME = "opencode";
|
|
@@ -8624,7 +8771,7 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
|
|
|
8624
8771
|
|
|
8625
8772
|
// src/outbox.ts
|
|
8626
8773
|
import {
|
|
8627
|
-
existsSync as
|
|
8774
|
+
existsSync as existsSync12,
|
|
8628
8775
|
mkdirSync as mkdirSync10,
|
|
8629
8776
|
readdirSync as readdirSync3,
|
|
8630
8777
|
readFileSync as readFileSync8,
|
|
@@ -8680,7 +8827,7 @@ var Outbox = class {
|
|
|
8680
8827
|
// wedging the drain.
|
|
8681
8828
|
list() {
|
|
8682
8829
|
const dir2 = this.dir();
|
|
8683
|
-
if (!
|
|
8830
|
+
if (!existsSync12(dir2)) return [];
|
|
8684
8831
|
let names;
|
|
8685
8832
|
try {
|
|
8686
8833
|
names = readdirSync3(dir2);
|
|
@@ -8716,7 +8863,7 @@ var Outbox = class {
|
|
|
8716
8863
|
}
|
|
8717
8864
|
size() {
|
|
8718
8865
|
const dir2 = this.dir();
|
|
8719
|
-
if (!
|
|
8866
|
+
if (!existsSync12(dir2)) return 0;
|
|
8720
8867
|
try {
|
|
8721
8868
|
return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
|
|
8722
8869
|
} catch {
|
|
@@ -9956,7 +10103,7 @@ function handleUncaught(log, err, origin) {
|
|
|
9956
10103
|
}
|
|
9957
10104
|
|
|
9958
10105
|
// src/crash-marker.ts
|
|
9959
|
-
import { existsSync as
|
|
10106
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
9960
10107
|
import { join as join16 } from "path";
|
|
9961
10108
|
function crashMarkerPath() {
|
|
9962
10109
|
return join16(cabaneDir(), "last-error.json");
|
|
@@ -9971,7 +10118,7 @@ function recordCrash(rec) {
|
|
|
9971
10118
|
function clearCrash() {
|
|
9972
10119
|
try {
|
|
9973
10120
|
const path = crashMarkerPath();
|
|
9974
|
-
if (
|
|
10121
|
+
if (existsSync13(path)) rmSync8(path, { force: true });
|
|
9975
10122
|
} catch {
|
|
9976
10123
|
}
|
|
9977
10124
|
}
|