@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/cli.js
CHANGED
|
@@ -7276,10 +7276,13 @@ var ConnectorHealthStore = class {
|
|
|
7276
7276
|
};
|
|
7277
7277
|
|
|
7278
7278
|
// src/dispatcher.ts
|
|
7279
|
-
import {
|
|
7280
|
-
import { existsSync as existsSync10, readdirSync as readdirSync2, statSync } from "fs";
|
|
7279
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync } from "fs";
|
|
7281
7280
|
import { join as join14 } from "path";
|
|
7282
7281
|
|
|
7282
|
+
// src/turn-execution.ts
|
|
7283
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
7284
|
+
import { existsSync as existsSync10 } from "fs";
|
|
7285
|
+
|
|
7283
7286
|
// src/turn-control-tools.ts
|
|
7284
7287
|
import { z as z13 } from "zod";
|
|
7285
7288
|
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
@@ -8089,7 +8092,7 @@ var TurnCommitter = class {
|
|
|
8089
8092
|
}
|
|
8090
8093
|
};
|
|
8091
8094
|
|
|
8092
|
-
// src/
|
|
8095
|
+
// src/turn-execution.ts
|
|
8093
8096
|
var PREPARING_TOOL_NAME = "preparing";
|
|
8094
8097
|
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:";
|
|
8095
8098
|
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:";
|
|
@@ -8101,33 +8104,6 @@ var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
|
8101
8104
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
8102
8105
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
8103
8106
|
var DEFAULT_LEASE_RENEWAL_MS = 3e4;
|
|
8104
|
-
function checkoutState(cwd) {
|
|
8105
|
-
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
8106
|
-
if (!existsSync10(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
8107
|
-
let entries;
|
|
8108
|
-
try {
|
|
8109
|
-
entries = readdirSync2(cwd);
|
|
8110
|
-
} catch (error) {
|
|
8111
|
-
return { ok: false, reason: `${cwd} is unreadable (${error.message})` };
|
|
8112
|
-
}
|
|
8113
|
-
if (entries.length === 0) {
|
|
8114
|
-
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
8115
|
-
}
|
|
8116
|
-
const gitPath = join14(cwd, ".git");
|
|
8117
|
-
if (!existsSync10(gitPath)) return { ok: true, reason: "usable" };
|
|
8118
|
-
let stat;
|
|
8119
|
-
try {
|
|
8120
|
-
stat = statSync(gitPath);
|
|
8121
|
-
} catch (error) {
|
|
8122
|
-
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
8123
|
-
}
|
|
8124
|
-
if (stat.isDirectory() && !existsSync10(join14(gitPath, "HEAD")))
|
|
8125
|
-
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
8126
|
-
return { ok: true, reason: "usable" };
|
|
8127
|
-
}
|
|
8128
|
-
function runKey(conversationId, agentId) {
|
|
8129
|
-
return `${conversationId}|${agentId}`;
|
|
8130
|
-
}
|
|
8131
8107
|
var LEASE_REFUSALS = /* @__PURE__ */ new Set([
|
|
8132
8108
|
"dispatch_not_admitted",
|
|
8133
8109
|
"turn_already_ended",
|
|
@@ -8162,61 +8138,128 @@ function describeErrorBody(body) {
|
|
|
8162
8138
|
if (text.length === 0) return void 0;
|
|
8163
8139
|
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
8164
8140
|
}
|
|
8165
|
-
|
|
8166
|
-
|
|
8141
|
+
function initialOutcome() {
|
|
8142
|
+
return {
|
|
8143
|
+
sessionWritten: false,
|
|
8144
|
+
sessionDegraded: false,
|
|
8145
|
+
sessionWriteRejected: false,
|
|
8146
|
+
okResult: false,
|
|
8147
|
+
resultReason: void 0,
|
|
8148
|
+
turnUsage: void 0,
|
|
8149
|
+
turnResolvedModel: void 0,
|
|
8150
|
+
turnResolvedConfig: void 0,
|
|
8151
|
+
turnMcpInventory: void 0,
|
|
8152
|
+
eventCounts: { session: 0, text: 0, thinking: 0, tool: 0, result: 0 },
|
|
8153
|
+
runtimeResultKind: null,
|
|
8154
|
+
contentBearingEvents: 0,
|
|
8155
|
+
latestSessionState: null,
|
|
8156
|
+
settledDiagnostics: null,
|
|
8157
|
+
silentMarkerEmitted: false,
|
|
8158
|
+
timeoutReason: null,
|
|
8159
|
+
leaseLost: false
|
|
8160
|
+
};
|
|
8161
|
+
}
|
|
8162
|
+
var TurnConcluded = class {
|
|
8163
|
+
constructor(reason, errorReason) {
|
|
8164
|
+
this.reason = reason;
|
|
8165
|
+
this.errorReason = errorReason;
|
|
8166
|
+
}
|
|
8167
|
+
reason;
|
|
8168
|
+
errorReason;
|
|
8169
|
+
};
|
|
8170
|
+
var TurnExecution = class {
|
|
8171
|
+
constructor(opts, supervisor, payload, handleOpts = {}) {
|
|
8167
8172
|
this.opts = opts;
|
|
8168
|
-
this.
|
|
8173
|
+
this.supervisor = supervisor;
|
|
8174
|
+
this.payload = payload;
|
|
8175
|
+
this.dispatchId = payload.messageId;
|
|
8176
|
+
this.workspaceId = opts.workspaceId;
|
|
8177
|
+
this.turnLog = opts.log.child({
|
|
8178
|
+
workspaceId: opts.workspaceId,
|
|
8179
|
+
conversationId: payload.conversationId,
|
|
8180
|
+
agentId: payload.agentId,
|
|
8181
|
+
messageId: payload.messageId
|
|
8182
|
+
});
|
|
8183
|
+
this.turnId = handleOpts.turnId ?? randomUUID();
|
|
8169
8184
|
}
|
|
8170
8185
|
opts;
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
//
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8186
|
+
supervisor;
|
|
8187
|
+
payload;
|
|
8188
|
+
startedAt = Date.now();
|
|
8189
|
+
turnId;
|
|
8190
|
+
dispatchId;
|
|
8191
|
+
workspaceId;
|
|
8192
|
+
turnLog;
|
|
8193
|
+
abortController = new AbortController();
|
|
8194
|
+
outcome = initialOutcome();
|
|
8195
|
+
seqCounter = 0;
|
|
8196
|
+
// CT11: per-turn monotonic counter mirroring the in-process dispatcher's.
|
|
8197
|
+
nextSeq = () => ++this.seqCounter;
|
|
8198
|
+
turnContext;
|
|
8199
|
+
resolvedMcpServers;
|
|
8200
|
+
effectiveCwd;
|
|
8201
|
+
hookEnv;
|
|
8202
|
+
turnEnv;
|
|
8203
|
+
sendState;
|
|
8204
|
+
skipState;
|
|
8205
|
+
askState;
|
|
8206
|
+
wakeState;
|
|
8207
|
+
replyState;
|
|
8208
|
+
controlOrder;
|
|
8209
|
+
turnControlServer;
|
|
8210
|
+
request;
|
|
8211
|
+
adapter;
|
|
8212
|
+
committer = null;
|
|
8213
|
+
transcript = null;
|
|
8214
|
+
turnControlIntentFetched = false;
|
|
8215
|
+
applyRecordedTurnControlIntent = async () => {
|
|
8216
|
+
};
|
|
8217
|
+
// CT1292: the committer is built before the lease watchdog arms, so the
|
|
8218
|
+
// failed-commit hint reaches it through this slot. Safe as a no-op until the
|
|
8219
|
+
// watchdog replaces it — the committer only fires it from inside the adapter
|
|
8220
|
+
// loop, which runs after.
|
|
8221
|
+
noteCommitFailed = () => {
|
|
8222
|
+
};
|
|
8223
|
+
disarmWatchdogs = () => {
|
|
8224
|
+
};
|
|
8225
|
+
// Codo's stack review, blocking finding #2: set the moment `acquireLease`'s
|
|
8226
|
+
// PATCH returns — the mailroom now holds this turn as the conversation's
|
|
8227
|
+
// running one, so every exit after this point must settle THE TURN, not just
|
|
8228
|
+
// clear the participant flag.
|
|
8229
|
+
admitted = false;
|
|
8230
|
+
concluded(reason, errorReason) {
|
|
8231
|
+
return new TurnConcluded(reason, errorReason);
|
|
8232
|
+
}
|
|
8233
|
+
// The arc. Phases in order; every pre-loop exit throws `TurnConcluded` and
|
|
8234
|
+
// funnels through ONE conclusion (hand back the controller — idempotent and
|
|
8235
|
+
// identity-checked at the supervisor — then clear the active-run flag);
|
|
8236
|
+
// everything past the lease settles in `execute()`'s one `finally`.
|
|
8237
|
+
async run() {
|
|
8197
8238
|
try {
|
|
8198
|
-
this.
|
|
8199
|
-
|
|
8239
|
+
await this.fetchContext();
|
|
8240
|
+
this.gateTrigger();
|
|
8241
|
+
await this.resolveSecrets();
|
|
8242
|
+
await this.prepareEnvironment();
|
|
8243
|
+
this.buildRequest();
|
|
8244
|
+
await this.acquireLease();
|
|
8245
|
+
await this.selectAdapter();
|
|
8246
|
+
} catch (err) {
|
|
8247
|
+
if (err instanceof TurnConcluded) {
|
|
8248
|
+
this.supervisor.releaseAbort(this.turnId, this.abortController);
|
|
8249
|
+
return this.admitted ? this.concludeAdmittedRun(err.reason, err.errorReason) : this.concludeBeforeRun(err.reason, err.errorReason);
|
|
8250
|
+
}
|
|
8251
|
+
if (this.admitted) {
|
|
8252
|
+
this.supervisor.releaseAbort(this.turnId, this.abortController);
|
|
8253
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8254
|
+
await this.concludeAdmittedRun("setup_failed", `setup_failed: ${msg}`);
|
|
8255
|
+
}
|
|
8256
|
+
throw err;
|
|
8200
8257
|
}
|
|
8258
|
+
await this.execute();
|
|
8259
|
+
return this.report();
|
|
8201
8260
|
}
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
// The server lights the "X is replying…" indicator eagerly at dispatch
|
|
8205
|
-
// (chat-dispatch.ts `scheduleRun`), and from that point only the companion can
|
|
8206
|
-
// clear it — the SJ383 `finally` after the SDK loop is the one clear, and
|
|
8207
|
-
// every pre-run exit returns before reaching it. So each pre-run failure has
|
|
8208
|
-
// to clear `active_run_started_at` itself, mirroring that `finally`, or the
|
|
8209
|
-
// indicator strands until the 12h age sweep.
|
|
8210
|
-
//
|
|
8211
|
-
// `errorReason` controls the server's duplicate-notice rule (the active-run
|
|
8212
|
-
// PATCH handler in conversations.ts): a clear carrying `errorReason` makes the
|
|
8213
|
-
// server post a `role:'system'` "X's reply failed: …" notice. Exits that
|
|
8214
|
-
// already posted their own user-facing `final` (prepare-failed, missing-secret)
|
|
8215
|
-
// pass NO errorReason, so the user doesn't see a second failure message; exits
|
|
8216
|
-
// that posted nothing pass `errorReason` so the user still gets a notice. The
|
|
8217
|
-
// clear is durable (CT93 outbox, last-writer-wins per pair), so a terminal
|
|
8218
|
-
// failure here just logs and the server age-sweep backstops it.
|
|
8219
|
-
async concludeBeforeRun(payload, turnLog, startedAt, reason, errorReason) {
|
|
8261
|
+
async concludeBeforeRun(reason, errorReason) {
|
|
8262
|
+
const { payload, turnLog, startedAt } = this;
|
|
8220
8263
|
const body = {
|
|
8221
8264
|
activeRunStartedAt: null,
|
|
8222
8265
|
// CT1046: this dispatch is over before it ran — retire its evidence so it
|
|
@@ -8238,25 +8281,47 @@ var Dispatcher = class {
|
|
|
8238
8281
|
);
|
|
8239
8282
|
}
|
|
8240
8283
|
const durationMs = Date.now() - startedAt;
|
|
8241
|
-
this.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
8284
|
+
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
8242
8285
|
return { ok: false, durationMs, reason };
|
|
8243
8286
|
}
|
|
8244
|
-
//
|
|
8245
|
-
//
|
|
8246
|
-
//
|
|
8247
|
-
//
|
|
8248
|
-
//
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8287
|
+
// The post-admission analogue of `concludeBeforeRun` (Codo's stack review,
|
|
8288
|
+
// blocking finding #2): this turn WAS admitted — the mailroom holds it as
|
|
8289
|
+
// the conversation's running turn — so the clear must NAME the turn and
|
|
8290
|
+
// DECLARE its terminal outcome. A bare pre-run clear here would reset the
|
|
8291
|
+
// participant flag while the turn metadata stayed running, the dispatch
|
|
8292
|
+
// stayed live, and the queue stayed held until the reaper. `outcome`
|
|
8293
|
+
// follows the settle wire's rule: a failure names its reason; without one
|
|
8294
|
+
// the turn settled — the runtime-unavailable exit posts its user-facing
|
|
8295
|
+
// `final` before concluding, so the turn genuinely produced its terminal
|
|
8296
|
+
// output and `settled` releases the queue honestly.
|
|
8297
|
+
async concludeAdmittedRun(reason, errorReason) {
|
|
8298
|
+
const { payload, turnLog, startedAt, turnId } = this;
|
|
8299
|
+
const body = {
|
|
8300
|
+
activeRunStartedAt: null,
|
|
8301
|
+
turnId,
|
|
8302
|
+
settledMessageId: payload.messageId,
|
|
8303
|
+
outcome: errorReason ? "failed" : "settled"
|
|
8304
|
+
};
|
|
8305
|
+
if (errorReason) body.errorReason = errorReason.slice(0, 200);
|
|
8306
|
+
try {
|
|
8307
|
+
await this.opts.api.setActiveRun(
|
|
8308
|
+
this.opts.workspaceId,
|
|
8309
|
+
payload.conversationId,
|
|
8310
|
+
payload.agentId,
|
|
8311
|
+
body
|
|
8312
|
+
);
|
|
8313
|
+
} catch (err) {
|
|
8314
|
+
turnLog.warn(
|
|
8315
|
+
{ err: err instanceof Error ? err.message : String(err), turnId },
|
|
8316
|
+
"dispatcher: admitted-run settle failed terminally; server age-sweep is the backstop"
|
|
8317
|
+
);
|
|
8318
|
+
}
|
|
8319
|
+
const durationMs = Date.now() - startedAt;
|
|
8320
|
+
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
8321
|
+
return { ok: false, durationMs, reason };
|
|
8322
|
+
}
|
|
8323
|
+
async fetchContext() {
|
|
8324
|
+
const { payload, turnId, turnLog } = this;
|
|
8260
8325
|
let turnContext;
|
|
8261
8326
|
try {
|
|
8262
8327
|
turnContext = await this.opts.api.getTurnContext(
|
|
@@ -8271,35 +8336,36 @@ var Dispatcher = class {
|
|
|
8271
8336
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
8272
8337
|
"dispatcher: turn-context 404 (stale event \u2014 conversation/message/participant gone); skipping"
|
|
8273
8338
|
);
|
|
8274
|
-
|
|
8339
|
+
throw this.concluded("turn_context_not_found");
|
|
8275
8340
|
}
|
|
8276
8341
|
const reason = err instanceof Error ? err.message : String(err);
|
|
8277
8342
|
turnLog.error({ err: reason }, "dispatcher: failed to fetch turn context");
|
|
8278
8343
|
const fetchReason = `fetch_failed: ${reason}`;
|
|
8279
|
-
|
|
8344
|
+
throw this.concluded(fetchReason, fetchReason);
|
|
8280
8345
|
}
|
|
8281
|
-
|
|
8346
|
+
this.turnContext = turnContext;
|
|
8347
|
+
}
|
|
8348
|
+
gateTrigger() {
|
|
8349
|
+
const { payload, turnLog } = this;
|
|
8350
|
+
const message = this.turnContext.message;
|
|
8282
8351
|
const isDispatchableTrigger = message.role === "user" || message.role === "agent" || message.role === "system" && message.hasPrimaryDispatch === true;
|
|
8283
8352
|
if (!isDispatchableTrigger) {
|
|
8284
8353
|
turnLog.warn({ role: message.role }, "dispatcher: trigger role not dispatchable \u2014 skipping");
|
|
8285
|
-
|
|
8286
|
-
payload,
|
|
8287
|
-
turnLog,
|
|
8288
|
-
startedAt,
|
|
8289
|
-
"unexpected_role",
|
|
8290
|
-
UNEXPECTED_ROLE_REASON
|
|
8291
|
-
);
|
|
8354
|
+
throw this.concluded("unexpected_role", UNEXPECTED_ROLE_REASON);
|
|
8292
8355
|
}
|
|
8293
|
-
this.notifyStart({
|
|
8294
|
-
id: dispatchId,
|
|
8356
|
+
this.supervisor.notifyStart({
|
|
8357
|
+
id: this.dispatchId,
|
|
8295
8358
|
conversationId: payload.conversationId,
|
|
8296
8359
|
message: message.body
|
|
8297
8360
|
});
|
|
8361
|
+
}
|
|
8362
|
+
async resolveSecrets() {
|
|
8363
|
+
const { payload, workspaceId, turnLog } = this;
|
|
8298
8364
|
const secretStore = this.opts.secretStore ?? loadSecretStoreTolerant((m) => turnLog.warn(m));
|
|
8299
8365
|
const { servers: resolvedMcpServers, missing } = resolveMcpSecrets(
|
|
8300
8366
|
// CT262: the user MCP DEFINITIONS (placeholder form) come from the turn
|
|
8301
8367
|
// context now, not a separate `getAgentSelf` run-config fetch.
|
|
8302
|
-
turnContext.mcpServers ?? {},
|
|
8368
|
+
this.turnContext.mcpServers ?? {},
|
|
8303
8369
|
secretStore
|
|
8304
8370
|
);
|
|
8305
8371
|
if (missing.length > 0) {
|
|
@@ -8320,13 +8386,15 @@ var Dispatcher = class {
|
|
|
8320
8386
|
);
|
|
8321
8387
|
}
|
|
8322
8388
|
const reason = `missing_secret: ${missing.join(",")}`;
|
|
8323
|
-
|
|
8389
|
+
throw this.concluded(reason);
|
|
8324
8390
|
}
|
|
8391
|
+
this.resolvedMcpServers = resolvedMcpServers;
|
|
8392
|
+
}
|
|
8393
|
+
async prepareEnvironment() {
|
|
8394
|
+
const { payload, workspaceId, turnId, turnLog } = this;
|
|
8325
8395
|
const localCwd = this.opts.local.cwd;
|
|
8326
8396
|
const prepareHook = this.opts.local.prepareHook;
|
|
8327
|
-
const cabaneCwd = turnContext.cwd;
|
|
8328
|
-
let seqCounter = 0;
|
|
8329
|
-
const nextSeq = () => ++seqCounter;
|
|
8397
|
+
const cabaneCwd = this.turnContext.cwd;
|
|
8330
8398
|
let effectiveCwd = localCwd ?? cabaneCwd;
|
|
8331
8399
|
if (effectiveCwd && !existsSync10(effectiveCwd)) {
|
|
8332
8400
|
turnLog.warn(
|
|
@@ -8336,9 +8404,9 @@ var Dispatcher = class {
|
|
|
8336
8404
|
effectiveCwd = void 0;
|
|
8337
8405
|
}
|
|
8338
8406
|
let hookEnv;
|
|
8339
|
-
const triggerIsPrepareFailure = message.body.startsWith(PREPARE_FAILED_PREFIX);
|
|
8340
|
-
const prepareFailureDispatch = turnContext.dispatchedByAgentId && !triggerIsPrepareFailure ? {
|
|
8341
|
-
dispatch: turnContext.dispatchedByAgentId,
|
|
8407
|
+
const triggerIsPrepareFailure = this.turnContext.message.body.startsWith(PREPARE_FAILED_PREFIX);
|
|
8408
|
+
const prepareFailureDispatch = this.turnContext.dispatchedByAgentId && !triggerIsPrepareFailure ? {
|
|
8409
|
+
dispatch: this.turnContext.dispatchedByAgentId,
|
|
8342
8410
|
dispatchBody: `${PREPARE_FAILED_PREFIX}
|
|
8343
8411
|
|
|
8344
8412
|
The dispatched turn could not start. Re-dispatch it after repairing the preparation failure shown in this conversation.`
|
|
@@ -8364,11 +8432,11 @@ The dispatched turn could not start. Re-dispatch it after repairing the preparat
|
|
|
8364
8432
|
conversationId: payload.conversationId,
|
|
8365
8433
|
agentId: payload.agentId,
|
|
8366
8434
|
agentUsername: this.opts.agentUsername,
|
|
8367
|
-
runtime: turnContext.runtime,
|
|
8368
|
-
hostAccess: turnContext.policy.hostFs,
|
|
8369
|
-
triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
|
|
8370
|
-
title: turnContext.conversation.title,
|
|
8371
|
-
messageBody: message.body,
|
|
8435
|
+
runtime: this.turnContext.runtime,
|
|
8436
|
+
hostAccess: this.turnContext.policy.hostFs,
|
|
8437
|
+
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
8438
|
+
title: this.turnContext.conversation.title,
|
|
8439
|
+
messageBody: this.turnContext.message.body,
|
|
8372
8440
|
prepared: cached2
|
|
8373
8441
|
});
|
|
8374
8442
|
} catch (err) {
|
|
@@ -8390,7 +8458,7 @@ ${reason}`,
|
|
|
8390
8458
|
"dispatcher: prepare-rejection post failed"
|
|
8391
8459
|
);
|
|
8392
8460
|
}
|
|
8393
|
-
|
|
8461
|
+
throw this.concluded(`prepare_failed: ${reason}`);
|
|
8394
8462
|
}
|
|
8395
8463
|
}
|
|
8396
8464
|
} else {
|
|
@@ -8398,7 +8466,7 @@ ${reason}`,
|
|
|
8398
8466
|
let preparingStarted = false;
|
|
8399
8467
|
let preparingSeq = null;
|
|
8400
8468
|
const reportPreparing = (phase) => {
|
|
8401
|
-
if (preparingSeq === null) preparingSeq = nextSeq();
|
|
8469
|
+
if (preparingSeq === null) preparingSeq = this.nextSeq();
|
|
8402
8470
|
const seq = preparingSeq;
|
|
8403
8471
|
void this.opts.api.reportActivity(workspaceId, payload.conversationId, payload.agentId, {
|
|
8404
8472
|
turnId,
|
|
@@ -8426,18 +8494,18 @@ ${reason}`,
|
|
|
8426
8494
|
conversationId: payload.conversationId,
|
|
8427
8495
|
agentId: payload.agentId,
|
|
8428
8496
|
agentUsername: this.opts.agentUsername,
|
|
8429
|
-
runtime: turnContext.runtime,
|
|
8430
|
-
hostAccess: turnContext.policy.hostFs,
|
|
8497
|
+
runtime: this.turnContext.runtime,
|
|
8498
|
+
hostAccess: this.turnContext.policy.hostFs,
|
|
8431
8499
|
// CT317/CT319: the trigger message's referenced-entry paths — what the
|
|
8432
8500
|
// tasker prepare hook keys its per-task env off. Defaults to `[]` for
|
|
8433
8501
|
// an older API. The conversation anchor is gone (CT319).
|
|
8434
|
-
triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
|
|
8435
|
-
title: turnContext.conversation.title,
|
|
8502
|
+
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
8503
|
+
title: this.turnContext.conversation.title,
|
|
8436
8504
|
// CT943: the dispatching message's text — where an `env:` directive
|
|
8437
8505
|
// rides. The server has always sent the trigger body on the turn
|
|
8438
8506
|
// context (for the live feed); this is the first thing to read it as
|
|
8439
8507
|
// an INPUT, so a dispatch can ask for its environment in words.
|
|
8440
|
-
messageBody: message.body
|
|
8508
|
+
messageBody: this.turnContext.message.body
|
|
8441
8509
|
});
|
|
8442
8510
|
clearTimeout(preparingTimer);
|
|
8443
8511
|
if (preparingStarted) reportPreparing("done");
|
|
@@ -8467,31 +8535,23 @@ ${reason}`,
|
|
|
8467
8535
|
);
|
|
8468
8536
|
}
|
|
8469
8537
|
const failReason = `prepare_failed: ${reason}`;
|
|
8470
|
-
|
|
8538
|
+
throw this.concluded(failReason);
|
|
8471
8539
|
}
|
|
8472
8540
|
}
|
|
8473
8541
|
}
|
|
8474
8542
|
const turnEnv = {
|
|
8475
8543
|
...hookEnv,
|
|
8476
|
-
CABANE_HOST_ACCESS: turnContext.policy.hostFs ? "1" : "0",
|
|
8544
|
+
CABANE_HOST_ACCESS: this.turnContext.policy.hostFs ? "1" : "0",
|
|
8477
8545
|
CABANE_COMPANION_HOME: agentCompanionHome()
|
|
8478
8546
|
};
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
const releaseAbort = () => {
|
|
8488
|
-
const pair2 = this.aborts.get(key);
|
|
8489
|
-
if (pair2?.get(turnId) !== abortController) return;
|
|
8490
|
-
pair2.delete(turnId);
|
|
8491
|
-
if (pair2.size === 0) this.aborts.delete(key);
|
|
8492
|
-
};
|
|
8493
|
-
let timeoutReason = null;
|
|
8494
|
-
let leaseLost = false;
|
|
8547
|
+
this.effectiveCwd = effectiveCwd;
|
|
8548
|
+
this.hookEnv = hookEnv;
|
|
8549
|
+
this.turnEnv = turnEnv;
|
|
8550
|
+
}
|
|
8551
|
+
async acquireLease() {
|
|
8552
|
+
const { payload, workspaceId, turnId, turnLog } = this;
|
|
8553
|
+
const abortController = this.abortController;
|
|
8554
|
+
this.supervisor.registerAbort(turnId, abortController);
|
|
8495
8555
|
try {
|
|
8496
8556
|
await this.opts.api.setActiveRun(workspaceId, payload.conversationId, payload.agentId, {
|
|
8497
8557
|
activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -8504,27 +8564,30 @@ ${reason}`,
|
|
|
8504
8564
|
});
|
|
8505
8565
|
} catch (err) {
|
|
8506
8566
|
const refusal = leaseRefusal(err);
|
|
8507
|
-
releaseAbort();
|
|
8508
8567
|
if (refusal === "dispatch_not_admitted" || refusal === "turn_already_ended") {
|
|
8509
8568
|
turnLog.error(
|
|
8510
8569
|
{ refusal, turnId },
|
|
8511
8570
|
"dispatcher: refused a turn lease; not running the model"
|
|
8512
8571
|
);
|
|
8513
|
-
|
|
8572
|
+
throw this.concluded(`lease_refused: ${refusal}`);
|
|
8514
8573
|
}
|
|
8515
8574
|
const reason = refusal ? `lease_refused: ${refusal}` : "lease_unconfirmed";
|
|
8516
8575
|
turnLog.error(
|
|
8517
8576
|
{ refusal, turnId, err: err instanceof Error ? err.message : String(err) },
|
|
8518
8577
|
"dispatcher: turn lease not confirmed; not running the model"
|
|
8519
8578
|
);
|
|
8520
|
-
|
|
8521
|
-
}
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
const
|
|
8526
|
-
const
|
|
8527
|
-
const
|
|
8579
|
+
throw this.concluded(reason, reason);
|
|
8580
|
+
}
|
|
8581
|
+
this.admitted = true;
|
|
8582
|
+
}
|
|
8583
|
+
buildRequest() {
|
|
8584
|
+
const { payload, workspaceId } = this;
|
|
8585
|
+
const sendState = this.sendState = createSendState();
|
|
8586
|
+
const skipState = this.skipState = createSkipState();
|
|
8587
|
+
const askState = this.askState = createAskState();
|
|
8588
|
+
const wakeState = this.wakeState = createWakeState();
|
|
8589
|
+
const replyState = this.replyState = createReplyState();
|
|
8590
|
+
const controlOrder = this.controlOrder = createTurnControlOrder();
|
|
8528
8591
|
const turnControlServer = createTurnControlMcpServer(
|
|
8529
8592
|
sendState,
|
|
8530
8593
|
skipState,
|
|
@@ -8533,18 +8596,18 @@ ${reason}`,
|
|
|
8533
8596
|
replyState,
|
|
8534
8597
|
controlOrder
|
|
8535
8598
|
);
|
|
8536
|
-
|
|
8537
|
-
turnContext,
|
|
8599
|
+
this.request = buildCompanionTurnRequest({
|
|
8600
|
+
turnContext: this.turnContext,
|
|
8538
8601
|
baseUrl: this.opts.baseUrl,
|
|
8539
8602
|
agentPat: this.opts.credential,
|
|
8540
8603
|
// CT306: the per-turn OBO credential when the API minted one; falls back to
|
|
8541
8604
|
// the companion PAT (`agentPat`) inside `buildCompanionTurnRequest` otherwise.
|
|
8542
|
-
...turnContext.turnToken ? { turnToken: turnContext.turnToken } : {},
|
|
8605
|
+
...this.turnContext.turnToken ? { turnToken: this.turnContext.turnToken } : {},
|
|
8543
8606
|
// SJ524: the hook-resolved cwd overrides the static local cwd.
|
|
8544
|
-
...effectiveCwd ? { cwd: effectiveCwd } : {},
|
|
8607
|
+
...this.effectiveCwd ? { cwd: this.effectiveCwd } : {},
|
|
8545
8608
|
// CT1103: the prepare-hook env plus the authoritative host-access token.
|
|
8546
|
-
env: turnEnv,
|
|
8547
|
-
mcpServers: resolvedMcpServers,
|
|
8609
|
+
env: this.turnEnv,
|
|
8610
|
+
mcpServers: this.resolvedMcpServers,
|
|
8548
8611
|
turnControlServer,
|
|
8549
8612
|
// CT238: this turn's conversation, forwarded as the active-conversation
|
|
8550
8613
|
// header so a cross-thread post/spawn stamps its origin.
|
|
@@ -8555,6 +8618,9 @@ ${reason}`,
|
|
|
8555
8618
|
// CT289: the operator's auto-memory escape hatch (machine-local), when set.
|
|
8556
8619
|
...this.opts.local.claudeCode ? { claudeCode: this.opts.local.claudeCode } : {}
|
|
8557
8620
|
});
|
|
8621
|
+
}
|
|
8622
|
+
async selectAdapter() {
|
|
8623
|
+
const { payload, workspaceId, turnId, turnLog } = this;
|
|
8558
8624
|
const onWarn = (msg, meta) => turnLog.warn(meta ?? {}, msg);
|
|
8559
8625
|
const adapters = [];
|
|
8560
8626
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
@@ -8573,9 +8639,8 @@ ${reason}`,
|
|
|
8573
8639
|
);
|
|
8574
8640
|
}
|
|
8575
8641
|
const registry = createAdapterRegistry(adapters);
|
|
8576
|
-
let adapter;
|
|
8577
8642
|
try {
|
|
8578
|
-
adapter = selectAdapter(registry, turnContext.runtime);
|
|
8643
|
+
this.adapter = selectAdapter(registry, this.turnContext.runtime);
|
|
8579
8644
|
} catch (err) {
|
|
8580
8645
|
if (!(err instanceof RuntimeUnavailableError)) throw err;
|
|
8581
8646
|
turnLog.error(
|
|
@@ -8594,17 +8659,18 @@ ${reason}`,
|
|
|
8594
8659
|
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
8595
8660
|
"dispatcher: runtime-unavailable notice post failed"
|
|
8596
8661
|
);
|
|
8662
|
+
const reason = `runtime_unavailable:${err.runtime}`;
|
|
8663
|
+
throw this.concluded(reason, reason);
|
|
8597
8664
|
}
|
|
8598
|
-
|
|
8599
|
-
return this.concludeBeforeRun(
|
|
8600
|
-
payload,
|
|
8601
|
-
turnLog,
|
|
8602
|
-
startedAt,
|
|
8603
|
-
`runtime_unavailable:${err.runtime}`
|
|
8604
|
-
);
|
|
8665
|
+
throw this.concluded(`runtime_unavailable:${err.runtime}`);
|
|
8605
8666
|
}
|
|
8667
|
+
}
|
|
8668
|
+
async execute() {
|
|
8669
|
+
const { payload, workspaceId, turnId, turnLog, startedAt } = this;
|
|
8670
|
+
const o = this.outcome;
|
|
8671
|
+
const abortController = this.abortController;
|
|
8606
8672
|
const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
|
|
8607
|
-
const transcript2 = this.opts.transcriptDir ? new TranscriptWriter(
|
|
8673
|
+
const transcript2 = this.transcript = this.opts.transcriptDir ? new TranscriptWriter(
|
|
8608
8674
|
this.opts.transcriptDir,
|
|
8609
8675
|
{
|
|
8610
8676
|
ts: new Date(startedAt).toISOString(),
|
|
@@ -8612,36 +8678,13 @@ ${reason}`,
|
|
|
8612
8678
|
workspaceId,
|
|
8613
8679
|
conversationId: payload.conversationId,
|
|
8614
8680
|
agentId: payload.agentId,
|
|
8615
|
-
dispatchId,
|
|
8616
|
-
message: message.body
|
|
8681
|
+
dispatchId: this.dispatchId,
|
|
8682
|
+
message: this.turnContext.message.body
|
|
8617
8683
|
},
|
|
8618
8684
|
(m) => turnLog.warn(m)
|
|
8619
8685
|
) : null;
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
let sessionWriteRejected = false;
|
|
8623
|
-
let okResult = false;
|
|
8624
|
-
let resultReason;
|
|
8625
|
-
const turnRuntime = turnContext.runtime;
|
|
8626
|
-
let turnUsage;
|
|
8627
|
-
let turnResolvedModel;
|
|
8628
|
-
let turnResolvedConfig;
|
|
8629
|
-
let turnMcpInventory;
|
|
8630
|
-
const eventCounts = {
|
|
8631
|
-
session: 0,
|
|
8632
|
-
text: 0,
|
|
8633
|
-
thinking: 0,
|
|
8634
|
-
tool: 0,
|
|
8635
|
-
result: 0
|
|
8636
|
-
};
|
|
8637
|
-
let runtimeResultKind = null;
|
|
8638
|
-
let contentBearingEvents = 0;
|
|
8639
|
-
let latestSessionState = request.session;
|
|
8640
|
-
let settledDiagnostics = null;
|
|
8641
|
-
let silentMarkerEmitted = false;
|
|
8642
|
-
let noteCommitFailed = () => {
|
|
8643
|
-
};
|
|
8644
|
-
const committer = new TurnCommitter({
|
|
8686
|
+
const turnRuntime = this.turnContext.runtime;
|
|
8687
|
+
const committer = this.committer = new TurnCommitter({
|
|
8645
8688
|
api: this.opts.api,
|
|
8646
8689
|
workspaceId,
|
|
8647
8690
|
conversationId: payload.conversationId,
|
|
@@ -8652,29 +8695,32 @@ ${reason}`,
|
|
|
8652
8695
|
parentMessageId: payload.messageId,
|
|
8653
8696
|
signal: abortController.signal,
|
|
8654
8697
|
log: turnLog,
|
|
8655
|
-
nextSeq,
|
|
8698
|
+
nextSeq: this.nextSeq,
|
|
8656
8699
|
// The committer reads this at commit to carry the addressed send on the
|
|
8657
8700
|
// terminal row; the server writes the send itself as a distinct message.
|
|
8658
|
-
sendState,
|
|
8701
|
+
sendState: this.sendState,
|
|
8659
8702
|
// CT326: likewise the ask payload. CT1281: the server writes the ask as its
|
|
8660
8703
|
// own addressed message to the human — which ENQUEUES like any other send —
|
|
8661
8704
|
// and creates the `asks` row against that carrier, not against turn speech.
|
|
8662
|
-
askState,
|
|
8705
|
+
askState: this.askState,
|
|
8663
8706
|
// CT442: likewise the wake payload — attached to the `final` row so the
|
|
8664
8707
|
// server arms the wake schedule atomically with the reply it rode on.
|
|
8665
|
-
wakeState,
|
|
8666
|
-
replyState,
|
|
8708
|
+
wakeState: this.wakeState,
|
|
8709
|
+
replyState: this.replyState,
|
|
8667
8710
|
// The ledger-derived owed reply, for the runtime's own declaration when
|
|
8668
8711
|
// the agent doesn't call `reply_to` (resolveDeclaredReplyField).
|
|
8669
|
-
owedReplyMessageId: turnContext.owedReplyMessageId ?? null,
|
|
8712
|
+
owedReplyMessageId: this.turnContext.owedReplyMessageId ?? null,
|
|
8670
8713
|
// CT1292: a commit that didn't land may mean this turn's lease is gone.
|
|
8671
|
-
|
|
8714
|
+
// `noteCommitFailed` is a field slot the lease watchdog fills when it
|
|
8715
|
+
// arms (it starts as a no-op) — the committer only fires it from inside
|
|
8716
|
+
// the adapter loop, which runs after the watchdog has replaced it.
|
|
8717
|
+
onCommitFailed: (err) => this.noteCommitFailed(err)
|
|
8672
8718
|
});
|
|
8673
8719
|
const usesHttpTurnControl = turnRuntime === "codex" || turnRuntime === "opencode";
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
turnControlIntentFetched = true;
|
|
8720
|
+
this.applyRecordedTurnControlIntent = async () => {
|
|
8721
|
+
if (this.turnControlIntentFetched || !usesHttpTurnControl || !this.turnContext.turnToken)
|
|
8722
|
+
return;
|
|
8723
|
+
this.turnControlIntentFetched = true;
|
|
8678
8724
|
try {
|
|
8679
8725
|
const intent = await this.opts.api.getTurnIntent(
|
|
8680
8726
|
workspaceId,
|
|
@@ -8683,44 +8729,44 @@ ${reason}`,
|
|
|
8683
8729
|
turnId
|
|
8684
8730
|
);
|
|
8685
8731
|
if (intent.ask) {
|
|
8686
|
-
askState.targetUserId = intent.ask.targetUserId;
|
|
8732
|
+
this.askState.targetUserId = intent.ask.targetUserId;
|
|
8687
8733
|
if (intent.ask.questions && intent.ask.questions.length > 0) {
|
|
8688
|
-
askState.questions = intent.ask.questions;
|
|
8689
|
-
askState.question = null;
|
|
8690
|
-
askState.headline = null;
|
|
8691
|
-
askState.options = null;
|
|
8734
|
+
this.askState.questions = intent.ask.questions;
|
|
8735
|
+
this.askState.question = null;
|
|
8736
|
+
this.askState.headline = null;
|
|
8737
|
+
this.askState.options = null;
|
|
8692
8738
|
} else {
|
|
8693
|
-
askState.question = intent.ask.question ?? null;
|
|
8694
|
-
askState.headline = intent.ask.headline ?? null;
|
|
8695
|
-
askState.options = intent.ask.options ?? null;
|
|
8696
|
-
askState.questions = null;
|
|
8739
|
+
this.askState.question = intent.ask.question ?? null;
|
|
8740
|
+
this.askState.headline = intent.ask.headline ?? null;
|
|
8741
|
+
this.askState.options = intent.ask.options ?? null;
|
|
8742
|
+
this.askState.questions = null;
|
|
8697
8743
|
}
|
|
8698
8744
|
}
|
|
8699
8745
|
if (intent.wake) {
|
|
8700
8746
|
if ("cancel" in intent.wake) {
|
|
8701
|
-
wakeState.cancelled = true;
|
|
8702
|
-
wakeState.afterSeconds = null;
|
|
8703
|
-
wakeState.at = null;
|
|
8704
|
-
wakeState.note = null;
|
|
8747
|
+
this.wakeState.cancelled = true;
|
|
8748
|
+
this.wakeState.afterSeconds = null;
|
|
8749
|
+
this.wakeState.at = null;
|
|
8750
|
+
this.wakeState.note = null;
|
|
8705
8751
|
} else {
|
|
8706
|
-
wakeState.cancelled = false;
|
|
8707
|
-
wakeState.afterSeconds = intent.wake.afterSeconds ?? null;
|
|
8708
|
-
wakeState.at = intent.wake.at ?? null;
|
|
8709
|
-
wakeState.note = intent.wake.note;
|
|
8752
|
+
this.wakeState.cancelled = false;
|
|
8753
|
+
this.wakeState.afterSeconds = intent.wake.afterSeconds ?? null;
|
|
8754
|
+
this.wakeState.at = intent.wake.at ?? null;
|
|
8755
|
+
this.wakeState.note = intent.wake.note;
|
|
8710
8756
|
}
|
|
8711
8757
|
}
|
|
8712
8758
|
if (intent.sendAgentId && intent.sendBody) {
|
|
8713
|
-
sendState.agentId = intent.sendAgentId;
|
|
8714
|
-
sendState.message = intent.sendBody;
|
|
8715
|
-
sendState.order = intent.sendOrder ?? null;
|
|
8759
|
+
this.sendState.agentId = intent.sendAgentId;
|
|
8760
|
+
this.sendState.message = intent.sendBody;
|
|
8761
|
+
this.sendState.order = intent.sendOrder ?? null;
|
|
8716
8762
|
}
|
|
8717
8763
|
if (intent.answersMessageId) {
|
|
8718
|
-
replyState.answersMessageId = intent.answersMessageId;
|
|
8719
|
-
replyState.order = intent.replyOrder ?? null;
|
|
8764
|
+
this.replyState.answersMessageId = intent.answersMessageId;
|
|
8765
|
+
this.replyState.order = intent.replyOrder ?? null;
|
|
8720
8766
|
}
|
|
8721
8767
|
if (intent.skipped) {
|
|
8722
|
-
skipState.skipped = true;
|
|
8723
|
-
skipState.reason = intent.skipReason;
|
|
8768
|
+
this.skipState.skipped = true;
|
|
8769
|
+
this.skipState.reason = intent.skipReason;
|
|
8724
8770
|
}
|
|
8725
8771
|
} catch (err) {
|
|
8726
8772
|
turnLog.warn(
|
|
@@ -8732,7 +8778,7 @@ ${reason}`,
|
|
|
8732
8778
|
const idleTimeoutMs = this.opts.idleTimeoutMs ?? DEFAULT_AGENT_IDLE_TIMEOUT_MS;
|
|
8733
8779
|
const fireTimeout = (reason) => {
|
|
8734
8780
|
if (abortController.signal.aborted) return;
|
|
8735
|
-
timeoutReason = reason;
|
|
8781
|
+
o.timeoutReason = reason;
|
|
8736
8782
|
turnLog.warn(
|
|
8737
8783
|
{ reason, idleTimeoutMs, totalTimeoutMs },
|
|
8738
8784
|
"dispatcher: turn timeout \u2014 aborting"
|
|
@@ -8751,7 +8797,7 @@ ${reason}`,
|
|
|
8751
8797
|
const leaseRenewalMs = this.opts.leaseRenewalMs ?? DEFAULT_LEASE_RENEWAL_MS;
|
|
8752
8798
|
let leaseCheckInFlight = false;
|
|
8753
8799
|
const checkLease = async (trigger) => {
|
|
8754
|
-
if (leaseLost || leaseCheckInFlight || abortController.signal.aborted) return;
|
|
8800
|
+
if (o.leaseLost || leaseCheckInFlight || abortController.signal.aborted) return;
|
|
8755
8801
|
leaseCheckInFlight = true;
|
|
8756
8802
|
try {
|
|
8757
8803
|
const state = await this.opts.api.checkTurnLease(
|
|
@@ -8762,7 +8808,7 @@ ${reason}`,
|
|
|
8762
8808
|
);
|
|
8763
8809
|
if (state !== "ended") return;
|
|
8764
8810
|
if (abortController.signal.aborted) return;
|
|
8765
|
-
leaseLost = true;
|
|
8811
|
+
o.leaseLost = true;
|
|
8766
8812
|
turnLog.warn(
|
|
8767
8813
|
{ turnId, trigger },
|
|
8768
8814
|
"dispatcher: this turn is no longer running server-side \u2014 aborting the loop"
|
|
@@ -8774,26 +8820,31 @@ ${reason}`,
|
|
|
8774
8820
|
};
|
|
8775
8821
|
const leaseTimer = setInterval(() => void checkLease("cadence"), leaseRenewalMs);
|
|
8776
8822
|
leaseTimer.unref?.();
|
|
8777
|
-
noteCommitFailed = (err) => {
|
|
8823
|
+
this.noteCommitFailed = (err) => {
|
|
8778
8824
|
if (isWriteFenceRefusal(err)) void checkLease("commit_refused");
|
|
8779
8825
|
};
|
|
8826
|
+
this.disarmWatchdogs = () => {
|
|
8827
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
8828
|
+
clearTimeout(totalTimer);
|
|
8829
|
+
clearInterval(leaseTimer);
|
|
8830
|
+
};
|
|
8780
8831
|
try {
|
|
8781
|
-
for await (const event of adapter.runTurn(request, abortController.signal)) {
|
|
8832
|
+
for await (const event of this.adapter.runTurn(this.request, abortController.signal)) {
|
|
8782
8833
|
transcript2?.write(event);
|
|
8783
|
-
eventCounts[event.type] += 1;
|
|
8784
|
-
if (isContentBearingEvent(event)) contentBearingEvents += 1;
|
|
8834
|
+
o.eventCounts[event.type] += 1;
|
|
8835
|
+
if (isContentBearingEvent(event)) o.contentBearingEvents += 1;
|
|
8785
8836
|
armIdle();
|
|
8786
8837
|
if (abortController.signal.aborted) {
|
|
8787
8838
|
turnLog.info("dispatcher: aborted mid-turn");
|
|
8788
|
-
okResult = false;
|
|
8789
|
-
resultReason = timeoutReason ?? "cancelled";
|
|
8839
|
+
o.okResult = false;
|
|
8840
|
+
o.resultReason = o.timeoutReason ?? "cancelled";
|
|
8790
8841
|
break;
|
|
8791
8842
|
}
|
|
8792
8843
|
if (event.type === "session") {
|
|
8793
|
-
latestSessionState = event.state;
|
|
8794
|
-
if (event.degraded) sessionDegraded = true;
|
|
8795
|
-
if (!sessionWritten) {
|
|
8796
|
-
sessionWritten = true;
|
|
8844
|
+
o.latestSessionState = event.state;
|
|
8845
|
+
if (event.degraded) o.sessionDegraded = true;
|
|
8846
|
+
if (!o.sessionWritten) {
|
|
8847
|
+
o.sessionWritten = true;
|
|
8797
8848
|
try {
|
|
8798
8849
|
await this.opts.api.setActiveRun(
|
|
8799
8850
|
workspaceId,
|
|
@@ -8804,7 +8855,7 @@ ${reason}`,
|
|
|
8804
8855
|
} catch (err) {
|
|
8805
8856
|
const status2 = err instanceof ApiError ? err.status : void 0;
|
|
8806
8857
|
if (status2 !== void 0 && status2 >= 400 && status2 < 500) {
|
|
8807
|
-
sessionWriteRejected = true;
|
|
8858
|
+
o.sessionWriteRejected = true;
|
|
8808
8859
|
turnLog.error(
|
|
8809
8860
|
{
|
|
8810
8861
|
err: err instanceof Error ? err.message : String(err),
|
|
@@ -8824,48 +8875,52 @@ ${reason}`,
|
|
|
8824
8875
|
}
|
|
8825
8876
|
}
|
|
8826
8877
|
} else if (event.type === "result") {
|
|
8827
|
-
okResult = event.ok;
|
|
8828
|
-
resultReason = event.reason;
|
|
8829
|
-
turnUsage = event.usage;
|
|
8830
|
-
turnResolvedModel = event.resolvedModel;
|
|
8831
|
-
turnResolvedConfig = event.resolvedConfig;
|
|
8832
|
-
turnMcpInventory = event.mcpInventory;
|
|
8833
|
-
runtimeResultKind = event.ok ? "success" : event.reason === "no_terminal" ? "no_terminal" : "error";
|
|
8834
|
-
} else if (event.type === "text" && skipState.skipped) {
|
|
8878
|
+
o.okResult = event.ok;
|
|
8879
|
+
o.resultReason = event.reason;
|
|
8880
|
+
o.turnUsage = event.usage;
|
|
8881
|
+
o.turnResolvedModel = event.resolvedModel;
|
|
8882
|
+
o.turnResolvedConfig = event.resolvedConfig;
|
|
8883
|
+
o.turnMcpInventory = event.mcpInventory;
|
|
8884
|
+
o.runtimeResultKind = event.ok ? "success" : event.reason === "no_terminal" ? "no_terminal" : "error";
|
|
8885
|
+
} else if (event.type === "text" && this.skipState.skipped) {
|
|
8835
8886
|
} else {
|
|
8836
8887
|
if (event.type === "text" && event.terminal) {
|
|
8837
|
-
await applyRecordedTurnControlIntent();
|
|
8888
|
+
await this.applyRecordedTurnControlIntent();
|
|
8838
8889
|
}
|
|
8839
8890
|
await committer.ingestEvent(event);
|
|
8840
8891
|
}
|
|
8841
8892
|
}
|
|
8842
8893
|
if (abortController.signal.aborted) {
|
|
8843
|
-
okResult = false;
|
|
8844
|
-
resultReason = leaseLost ? "lease_lost" : timeoutReason ?? "cancelled";
|
|
8894
|
+
o.okResult = false;
|
|
8895
|
+
o.resultReason = o.leaseLost ? "lease_lost" : o.timeoutReason ?? "cancelled";
|
|
8845
8896
|
}
|
|
8846
|
-
const emptyResultReason = !skipState.skipped && classifyEmptyResult({
|
|
8897
|
+
const emptyResultReason = !this.skipState.skipped && classifyEmptyResult({
|
|
8898
|
+
ok: o.okResult,
|
|
8899
|
+
contentBearingEvents: o.contentBearingEvents,
|
|
8900
|
+
usage: o.turnUsage
|
|
8901
|
+
});
|
|
8847
8902
|
if (emptyResultReason) {
|
|
8848
|
-
okResult = false;
|
|
8849
|
-
resultReason = emptyResultReason;
|
|
8903
|
+
o.okResult = false;
|
|
8904
|
+
o.resultReason = emptyResultReason;
|
|
8850
8905
|
}
|
|
8851
|
-
if (!okResult && !resultReason) {
|
|
8852
|
-
resultReason = "no_result";
|
|
8906
|
+
if (!o.okResult && !o.resultReason) {
|
|
8907
|
+
o.resultReason = "no_result";
|
|
8853
8908
|
}
|
|
8854
8909
|
if (!abortController.signal.aborted) {
|
|
8855
|
-
await applyRecordedTurnControlIntent();
|
|
8910
|
+
await this.applyRecordedTurnControlIntent();
|
|
8856
8911
|
}
|
|
8857
|
-
if (!abortController.signal.aborted && skipState.skipped) {
|
|
8912
|
+
if (!abortController.signal.aborted && this.skipState.skipped) {
|
|
8858
8913
|
turnLog.info(
|
|
8859
|
-
{ reason: skipState.reason, turnId, ok: okResult },
|
|
8914
|
+
{ reason: this.skipState.reason, turnId, ok: o.okResult },
|
|
8860
8915
|
"agent skipped turn (skip_turn)"
|
|
8861
8916
|
);
|
|
8862
|
-
const { wake: skipWake } = wakeCommitField(wakeState);
|
|
8917
|
+
const { wake: skipWake } = wakeCommitField(this.wakeState);
|
|
8863
8918
|
try {
|
|
8864
8919
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8865
8920
|
body: SKIPPED_MARKER_BODY,
|
|
8866
8921
|
kind: "skipped",
|
|
8867
8922
|
turnId,
|
|
8868
|
-
seq: nextSeq(),
|
|
8923
|
+
seq: this.nextSeq(),
|
|
8869
8924
|
parentMessageId: payload.messageId,
|
|
8870
8925
|
...skipWake ? { wake: skipWake } : {}
|
|
8871
8926
|
});
|
|
@@ -8876,21 +8931,21 @@ ${reason}`,
|
|
|
8876
8931
|
);
|
|
8877
8932
|
}
|
|
8878
8933
|
} else {
|
|
8879
|
-
await committer.finalize(okResult);
|
|
8880
|
-
if (!abortController.signal.aborted && okResult && !committer.finalEmitted) {
|
|
8934
|
+
await committer.finalize(o.okResult);
|
|
8935
|
+
if (!abortController.signal.aborted && o.okResult && !committer.finalEmitted) {
|
|
8881
8936
|
try {
|
|
8882
8937
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8883
8938
|
body: SILENT_MARKER_BODY,
|
|
8884
8939
|
kind: "silent",
|
|
8885
8940
|
turnId,
|
|
8886
|
-
seq: nextSeq(),
|
|
8941
|
+
seq: this.nextSeq(),
|
|
8887
8942
|
parentMessageId: payload.messageId,
|
|
8888
8943
|
// ask, wake and send must survive a wordless turn exactly as
|
|
8889
8944
|
// they survive a textual final; dropping one can strand a person
|
|
8890
8945
|
// or the next actor with no visible failure.
|
|
8891
8946
|
...committer.turnControlFields("silent")
|
|
8892
8947
|
});
|
|
8893
|
-
silentMarkerEmitted = true;
|
|
8948
|
+
o.silentMarkerEmitted = true;
|
|
8894
8949
|
} catch (err) {
|
|
8895
8950
|
turnLog.warn(
|
|
8896
8951
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
@@ -8900,27 +8955,25 @@ ${reason}`,
|
|
|
8900
8955
|
}
|
|
8901
8956
|
}
|
|
8902
8957
|
} catch (err) {
|
|
8903
|
-
okResult = false;
|
|
8904
|
-
resultReason = err instanceof Error ? err.message : String(err);
|
|
8905
|
-
turnLog.error({ err: resultReason }, "dispatcher: SDK query threw");
|
|
8958
|
+
o.okResult = false;
|
|
8959
|
+
o.resultReason = err instanceof Error ? err.message : String(err);
|
|
8960
|
+
turnLog.error({ err: o.resultReason }, "dispatcher: SDK query threw");
|
|
8906
8961
|
} finally {
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
resultReason = "lease_lost";
|
|
8912
|
-
okResult = false;
|
|
8962
|
+
this.disarmWatchdogs();
|
|
8963
|
+
if (o.leaseLost) {
|
|
8964
|
+
o.resultReason = "lease_lost";
|
|
8965
|
+
o.okResult = false;
|
|
8913
8966
|
}
|
|
8914
|
-
const userCancelled = abortController.signal.aborted && timeoutReason === null && !leaseLost;
|
|
8915
|
-
if (timeoutReason !== null) {
|
|
8916
|
-
resultReason = timeoutReason;
|
|
8917
|
-
okResult = false;
|
|
8918
|
-
if (timeoutReason === "timeout_idle") {
|
|
8967
|
+
const userCancelled = abortController.signal.aborted && o.timeoutReason === null && !o.leaseLost;
|
|
8968
|
+
if (o.timeoutReason !== null) {
|
|
8969
|
+
o.resultReason = o.timeoutReason;
|
|
8970
|
+
o.okResult = false;
|
|
8971
|
+
if (o.timeoutReason === "timeout_idle") {
|
|
8919
8972
|
const health = this.opts.connectorHealth?.lookup(turnRuntime);
|
|
8920
8973
|
if (health?.quotaState === "limited" && health.limitedUntil) {
|
|
8921
8974
|
const resetMs = Date.parse(health.limitedUntil);
|
|
8922
8975
|
if (Number.isFinite(resetMs) && resetMs > Date.now()) {
|
|
8923
|
-
resultReason = encodeFailureReason({
|
|
8976
|
+
o.resultReason = encodeFailureReason({
|
|
8924
8977
|
kind: "usage_capped",
|
|
8925
8978
|
resetsAt: health.limitedUntil
|
|
8926
8979
|
});
|
|
@@ -8944,42 +8997,41 @@ ${reason}`,
|
|
|
8944
8997
|
// turn is just as over, and its evidence must die with it.
|
|
8945
8998
|
settledMessageId: payload.messageId
|
|
8946
8999
|
};
|
|
8947
|
-
if (turnUsage) {
|
|
8948
|
-
body.usage = turnUsage;
|
|
9000
|
+
if (o.turnUsage) {
|
|
9001
|
+
body.usage = o.turnUsage;
|
|
8949
9002
|
}
|
|
8950
|
-
if (turnResolvedModel) {
|
|
8951
|
-
body.resolvedModel = turnResolvedModel;
|
|
9003
|
+
if (o.turnResolvedModel) {
|
|
9004
|
+
body.resolvedModel = o.turnResolvedModel;
|
|
8952
9005
|
}
|
|
8953
|
-
if (turnResolvedConfig && Object.keys(turnResolvedConfig).length > 0) {
|
|
8954
|
-
body.resolvedConfig = turnResolvedConfig;
|
|
9006
|
+
if (o.turnResolvedConfig && Object.keys(o.turnResolvedConfig).length > 0) {
|
|
9007
|
+
body.resolvedConfig = o.turnResolvedConfig;
|
|
8955
9008
|
}
|
|
8956
|
-
if (!okResult && resultReason && resultReason !== "cancelled" && !userCancelled && !leaseLost && !skipState.skipped) {
|
|
8957
|
-
body.errorReason = resultReason.slice(0, 200);
|
|
9009
|
+
if (!o.okResult && o.resultReason && o.resultReason !== "cancelled" && !userCancelled && !o.leaseLost && !this.skipState.skipped) {
|
|
9010
|
+
body.errorReason = o.resultReason.slice(0, 200);
|
|
8958
9011
|
}
|
|
8959
|
-
if (okResult) {
|
|
9012
|
+
if (o.okResult) {
|
|
8960
9013
|
body.lastSeenMessageId = payload.messageId;
|
|
8961
9014
|
}
|
|
8962
|
-
body.outcome = okResult ? "settled" : body.errorReason ? "failed" : "interrupted";
|
|
8963
|
-
if (sessionDegraded) {
|
|
9015
|
+
body.outcome = o.okResult ? "settled" : body.errorReason ? "failed" : "interrupted";
|
|
9016
|
+
if (o.sessionDegraded) {
|
|
8964
9017
|
body.degraded = true;
|
|
8965
9018
|
}
|
|
8966
|
-
if (sessionWriteRejected &&
|
|
9019
|
+
if (o.sessionWriteRejected && this.supervisor.claimSessionWriteNotice()) {
|
|
8967
9020
|
body.sessionWriteRejected = true;
|
|
8968
|
-
this.sessionWriteNotified.add(key);
|
|
8969
9021
|
}
|
|
8970
|
-
const outcome = skipState.skipped ? "skipped" : userCancelled || leaseLost ? "cancelled" : okResult ? "success" : "failure";
|
|
8971
|
-
const diagnosticReason = outcome === "skipped" ? { kind: "skipped" } : outcome === "cancelled" ? leaseLost ? { kind: "lease_lost" } : { kind: "cancelled" } : outcome === "failure" ? normalizeTurnResultReason(resultReason) : null;
|
|
8972
|
-
settledDiagnostics = {
|
|
9022
|
+
const outcome = this.skipState.skipped ? "skipped" : userCancelled || o.leaseLost ? "cancelled" : o.okResult ? "success" : "failure";
|
|
9023
|
+
const diagnosticReason = outcome === "skipped" ? { kind: "skipped" } : outcome === "cancelled" ? o.leaseLost ? { kind: "lease_lost" } : { kind: "cancelled" } : outcome === "failure" ? normalizeTurnResultReason(o.resultReason) : null;
|
|
9024
|
+
o.settledDiagnostics = {
|
|
8973
9025
|
outcome,
|
|
8974
9026
|
resultReason: diagnosticReason,
|
|
8975
|
-
sessionMode: sessionDegraded ? "degraded" : request.session ? "resumed" : "fresh",
|
|
8976
|
-
sessionFingerprint: fingerprintSessionState(latestSessionState),
|
|
8977
|
-
eventCounts,
|
|
8978
|
-
runtimeResultKind,
|
|
8979
|
-
...turnMcpInventory ? { mcpInventory: turnMcpInventory } : {},
|
|
8980
|
-
finalSource: outcome === "skipped" || outcome === "cancelled" || silentMarkerEmitted ? "marker" : committer.finalSource
|
|
9027
|
+
sessionMode: o.sessionDegraded ? "degraded" : this.request.session ? "resumed" : "fresh",
|
|
9028
|
+
sessionFingerprint: fingerprintSessionState(o.latestSessionState),
|
|
9029
|
+
eventCounts: o.eventCounts,
|
|
9030
|
+
runtimeResultKind: o.runtimeResultKind,
|
|
9031
|
+
...o.turnMcpInventory ? { mcpInventory: o.turnMcpInventory } : {},
|
|
9032
|
+
finalSource: outcome === "skipped" || outcome === "cancelled" || o.silentMarkerEmitted ? "marker" : committer.finalSource
|
|
8981
9033
|
};
|
|
8982
|
-
body.diagnostics = settledDiagnostics;
|
|
9034
|
+
body.diagnostics = o.settledDiagnostics;
|
|
8983
9035
|
if (diagnosticReason && !["usage_capped", "rate_limited", "auth_expired", "cancelled", "skipped"].includes(
|
|
8984
9036
|
diagnosticReason.kind
|
|
8985
9037
|
)) {
|
|
@@ -8990,16 +9042,16 @@ ${reason}`,
|
|
|
8990
9042
|
conversationId: payload.conversationId,
|
|
8991
9043
|
agentId: payload.agentId,
|
|
8992
9044
|
runtime: turnRuntime,
|
|
8993
|
-
model: turnResolvedModel ?? null,
|
|
8994
|
-
usage: turnUsage ?? null,
|
|
8995
|
-
hadStoredSession: request.session != null,
|
|
8996
|
-
diagnostics: settledDiagnostics
|
|
9045
|
+
model: o.turnResolvedModel ?? null,
|
|
9046
|
+
usage: o.turnUsage ?? null,
|
|
9047
|
+
hadStoredSession: this.request.session != null,
|
|
9048
|
+
diagnostics: o.settledDiagnostics
|
|
8997
9049
|
},
|
|
8998
9050
|
"dispatcher: anomalous turn settled"
|
|
8999
9051
|
);
|
|
9000
9052
|
}
|
|
9001
9053
|
this.opts.connectorHealth?.recordSettle(turnRuntime, {
|
|
9002
|
-
ok: okResult,
|
|
9054
|
+
ok: o.okResult,
|
|
9003
9055
|
errorReason: body.errorReason ?? null
|
|
9004
9056
|
});
|
|
9005
9057
|
try {
|
|
@@ -9015,45 +9067,151 @@ ${reason}`,
|
|
|
9015
9067
|
"dispatcher: active-run clear failed terminally; server age-sweep is the backstop"
|
|
9016
9068
|
);
|
|
9017
9069
|
}
|
|
9018
|
-
releaseAbort();
|
|
9070
|
+
this.supervisor.releaseAbort(turnId, abortController);
|
|
9019
9071
|
}
|
|
9072
|
+
}
|
|
9073
|
+
report() {
|
|
9074
|
+
const o = this.outcome;
|
|
9075
|
+
const { startedAt, turnLog } = this;
|
|
9020
9076
|
const durationMs = Date.now() - startedAt;
|
|
9021
|
-
const finalReplyBody = committer.replyBody;
|
|
9022
|
-
if (
|
|
9023
|
-
|
|
9024
|
-
ok: okResult,
|
|
9025
|
-
...resultReason ? { reason: resultReason } : {},
|
|
9077
|
+
const finalReplyBody = this.committer.replyBody;
|
|
9078
|
+
if (this.transcript) {
|
|
9079
|
+
this.transcript.close({
|
|
9080
|
+
ok: o.okResult,
|
|
9081
|
+
...o.resultReason ? { reason: o.resultReason } : {},
|
|
9026
9082
|
durationMs
|
|
9027
9083
|
});
|
|
9028
|
-
if (!okResult && resultReason !== "cancelled") {
|
|
9029
|
-
turnLog.info(`turn failed \u2014 full transcript: ${
|
|
9084
|
+
if (!o.okResult && o.resultReason !== "cancelled") {
|
|
9085
|
+
turnLog.info(`turn failed \u2014 full this.transcript: ${this.transcript.path}`);
|
|
9030
9086
|
}
|
|
9031
|
-
if (settledDiagnostics?.resultReason?.kind === "empty_result" || settledDiagnostics?.resultReason?.kind === "empty_result_unverified") {
|
|
9032
|
-
|
|
9087
|
+
if (o.settledDiagnostics?.resultReason?.kind === "empty_result" || o.settledDiagnostics?.resultReason?.kind === "empty_result_unverified") {
|
|
9088
|
+
this.transcript.preserveAnomaly();
|
|
9033
9089
|
}
|
|
9034
9090
|
}
|
|
9035
|
-
if (okResult) {
|
|
9091
|
+
if (o.okResult) {
|
|
9036
9092
|
turnLog.debug({ durationMs }, "dispatcher: turn end (ok)");
|
|
9037
|
-
this.notifyEnd({
|
|
9038
|
-
id: dispatchId,
|
|
9093
|
+
this.supervisor.notifyEnd({
|
|
9094
|
+
id: this.dispatchId,
|
|
9039
9095
|
ok: true,
|
|
9040
9096
|
durationMs,
|
|
9041
9097
|
...finalReplyBody ? { reply: finalReplyBody.trim() } : {}
|
|
9042
9098
|
});
|
|
9043
9099
|
return { ok: true, durationMs };
|
|
9044
9100
|
}
|
|
9045
|
-
turnLog.debug({ durationMs, reason: resultReason }, "dispatcher: turn end (not ok)");
|
|
9046
|
-
this.notifyEnd({
|
|
9047
|
-
id: dispatchId,
|
|
9101
|
+
turnLog.debug({ durationMs, reason: o.resultReason }, "dispatcher: turn end (not ok)");
|
|
9102
|
+
this.supervisor.notifyEnd({
|
|
9103
|
+
id: this.dispatchId,
|
|
9048
9104
|
ok: false,
|
|
9049
9105
|
durationMs,
|
|
9050
|
-
...resultReason ? { reason: resultReason } : {}
|
|
9106
|
+
...o.resultReason ? { reason: o.resultReason } : {}
|
|
9051
9107
|
});
|
|
9052
9108
|
return {
|
|
9053
9109
|
ok: false,
|
|
9054
9110
|
durationMs,
|
|
9055
|
-
...resultReason ? { reason: resultReason } : {}
|
|
9111
|
+
...o.resultReason ? { reason: o.resultReason } : {}
|
|
9112
|
+
};
|
|
9113
|
+
}
|
|
9114
|
+
};
|
|
9115
|
+
function fingerprintSessionState(state) {
|
|
9116
|
+
if (!state) return null;
|
|
9117
|
+
let opaqueId = state;
|
|
9118
|
+
try {
|
|
9119
|
+
const parsed = JSON.parse(state);
|
|
9120
|
+
const candidate = parsed.sdkSessionId ?? parsed.threadId ?? parsed.sessionId;
|
|
9121
|
+
if (typeof candidate === "string" && candidate.length > 0) opaqueId = candidate;
|
|
9122
|
+
} catch {
|
|
9123
|
+
}
|
|
9124
|
+
return createHash2("sha256").update(opaqueId).digest("hex").slice(0, 16);
|
|
9125
|
+
}
|
|
9126
|
+
|
|
9127
|
+
// src/dispatcher.ts
|
|
9128
|
+
function checkoutState(cwd) {
|
|
9129
|
+
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
9130
|
+
if (!existsSync11(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
9131
|
+
let entries;
|
|
9132
|
+
try {
|
|
9133
|
+
entries = readdirSync2(cwd);
|
|
9134
|
+
} catch (error) {
|
|
9135
|
+
return { ok: false, reason: `${cwd} is unreadable (${error.message})` };
|
|
9136
|
+
}
|
|
9137
|
+
if (entries.length === 0) {
|
|
9138
|
+
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
9139
|
+
}
|
|
9140
|
+
const gitPath = join14(cwd, ".git");
|
|
9141
|
+
if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
|
|
9142
|
+
let stat;
|
|
9143
|
+
try {
|
|
9144
|
+
stat = statSync(gitPath);
|
|
9145
|
+
} catch (error) {
|
|
9146
|
+
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
9147
|
+
}
|
|
9148
|
+
if (stat.isDirectory() && !existsSync11(join14(gitPath, "HEAD")))
|
|
9149
|
+
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
9150
|
+
return { ok: true, reason: "usable" };
|
|
9151
|
+
}
|
|
9152
|
+
function runKey(conversationId, agentId) {
|
|
9153
|
+
return `${conversationId}|${agentId}`;
|
|
9154
|
+
}
|
|
9155
|
+
var Dispatcher = class {
|
|
9156
|
+
constructor(opts) {
|
|
9157
|
+
this.opts = opts;
|
|
9158
|
+
this.aborts = opts.aborts ?? /* @__PURE__ */ new Map();
|
|
9159
|
+
}
|
|
9160
|
+
opts;
|
|
9161
|
+
// CT1288: keyed (conversation|agent) → per-TURN controllers. Stop is
|
|
9162
|
+
// pair-scoped and must reach every live loop for the pair.
|
|
9163
|
+
aborts;
|
|
9164
|
+
// CT1109: pairs already told "your session couldn't be saved" — the notice is
|
|
9165
|
+
// once per (conversation, agent), while the rejection repeats every turn.
|
|
9166
|
+
sessionWriteNotified = /* @__PURE__ */ new Set();
|
|
9167
|
+
notifyStart(info) {
|
|
9168
|
+
try {
|
|
9169
|
+
this.opts.observer?.onStart(info);
|
|
9170
|
+
} catch {
|
|
9171
|
+
}
|
|
9172
|
+
}
|
|
9173
|
+
notifyEnd(info) {
|
|
9174
|
+
try {
|
|
9175
|
+
this.opts.observer?.onEnd(info);
|
|
9176
|
+
} catch {
|
|
9177
|
+
}
|
|
9178
|
+
}
|
|
9179
|
+
// CT1288: `opts.turnId` is a RESUMED turn id — the id this event already ran
|
|
9180
|
+
// under before the companion died. Passing it makes the replay ask the
|
|
9181
|
+
// mailroom to re-admit the turn it already owns, which the server explicitly
|
|
9182
|
+
// supports ("a replay reports, it never re-points"). Omitted on a fresh
|
|
9183
|
+
// dispatch, where minting is correct.
|
|
9184
|
+
//
|
|
9185
|
+
// CT1261: a short `handle` creates a `TurnExecution` and runs it. The
|
|
9186
|
+
// supervisor seam hands the execution exactly the three cross-turn duties —
|
|
9187
|
+
// the abort registry, the session-write notice latch, the observer — and
|
|
9188
|
+
// nothing else.
|
|
9189
|
+
async handle(payload, opts = {}) {
|
|
9190
|
+
const key = runKey(payload.conversationId, payload.agentId);
|
|
9191
|
+
const supervisor = {
|
|
9192
|
+
registerAbort: (turnId, controller) => {
|
|
9193
|
+
let pairAborts = this.aborts.get(key);
|
|
9194
|
+
if (!pairAborts) {
|
|
9195
|
+
pairAborts = /* @__PURE__ */ new Map();
|
|
9196
|
+
this.aborts.set(key, pairAborts);
|
|
9197
|
+
}
|
|
9198
|
+
pairAborts.set(turnId, controller);
|
|
9199
|
+
},
|
|
9200
|
+
releaseAbort: (turnId, controller) => {
|
|
9201
|
+
const pair2 = this.aborts.get(key);
|
|
9202
|
+
if (pair2?.get(turnId) !== controller) return;
|
|
9203
|
+
pair2.delete(turnId);
|
|
9204
|
+
if (pair2.size === 0) this.aborts.delete(key);
|
|
9205
|
+
},
|
|
9206
|
+
claimSessionWriteNotice: () => {
|
|
9207
|
+
if (this.sessionWriteNotified.has(key)) return false;
|
|
9208
|
+
this.sessionWriteNotified.add(key);
|
|
9209
|
+
return true;
|
|
9210
|
+
},
|
|
9211
|
+
notifyStart: (info) => this.notifyStart(info),
|
|
9212
|
+
notifyEnd: (info) => this.notifyEnd(info)
|
|
9056
9213
|
};
|
|
9214
|
+
return new TurnExecution(this.opts, supervisor, payload, opts).run();
|
|
9057
9215
|
}
|
|
9058
9216
|
// SJ383: cancel a specific (conversation, agent) run if one is in flight in
|
|
9059
9217
|
// THIS companion process. Returns true if an in-flight run was aborted.
|
|
@@ -9070,17 +9228,6 @@ ${reason}`,
|
|
|
9070
9228
|
return true;
|
|
9071
9229
|
}
|
|
9072
9230
|
};
|
|
9073
|
-
function fingerprintSessionState(state) {
|
|
9074
|
-
if (!state) return null;
|
|
9075
|
-
let opaqueId = state;
|
|
9076
|
-
try {
|
|
9077
|
-
const parsed = JSON.parse(state);
|
|
9078
|
-
const candidate = parsed.sdkSessionId ?? parsed.threadId ?? parsed.sessionId;
|
|
9079
|
-
if (typeof candidate === "string" && candidate.length > 0) opaqueId = candidate;
|
|
9080
|
-
} catch {
|
|
9081
|
-
}
|
|
9082
|
-
return createHash2("sha256").update(opaqueId).digest("hex").slice(0, 16);
|
|
9083
|
-
}
|
|
9084
9231
|
|
|
9085
9232
|
// src/opencode-models.ts
|
|
9086
9233
|
var OPENCODE_RUNTIME = "opencode";
|
|
@@ -9125,7 +9272,7 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
|
|
|
9125
9272
|
|
|
9126
9273
|
// src/outbox.ts
|
|
9127
9274
|
import {
|
|
9128
|
-
existsSync as
|
|
9275
|
+
existsSync as existsSync12,
|
|
9129
9276
|
mkdirSync as mkdirSync10,
|
|
9130
9277
|
readdirSync as readdirSync3,
|
|
9131
9278
|
readFileSync as readFileSync8,
|
|
@@ -9181,7 +9328,7 @@ var Outbox = class {
|
|
|
9181
9328
|
// wedging the drain.
|
|
9182
9329
|
list() {
|
|
9183
9330
|
const dir2 = this.dir();
|
|
9184
|
-
if (!
|
|
9331
|
+
if (!existsSync12(dir2)) return [];
|
|
9185
9332
|
let names;
|
|
9186
9333
|
try {
|
|
9187
9334
|
names = readdirSync3(dir2);
|
|
@@ -9217,7 +9364,7 @@ var Outbox = class {
|
|
|
9217
9364
|
}
|
|
9218
9365
|
size() {
|
|
9219
9366
|
const dir2 = this.dir();
|
|
9220
|
-
if (!
|
|
9367
|
+
if (!existsSync12(dir2)) return 0;
|
|
9221
9368
|
try {
|
|
9222
9369
|
return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
|
|
9223
9370
|
} catch {
|
|
@@ -10457,7 +10604,7 @@ function handleUncaught(log, err, origin) {
|
|
|
10457
10604
|
}
|
|
10458
10605
|
|
|
10459
10606
|
// src/crash-marker.ts
|
|
10460
|
-
import { existsSync as
|
|
10607
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
10461
10608
|
import { join as join16 } from "path";
|
|
10462
10609
|
function crashMarkerPath() {
|
|
10463
10610
|
return join16(cabaneDir(), "last-error.json");
|
|
@@ -10472,7 +10619,7 @@ function recordCrash(rec2) {
|
|
|
10472
10619
|
function clearCrash() {
|
|
10473
10620
|
try {
|
|
10474
10621
|
const path = crashMarkerPath();
|
|
10475
|
-
if (
|
|
10622
|
+
if (existsSync13(path)) rmSync8(path, { force: true });
|
|
10476
10623
|
} catch {
|
|
10477
10624
|
}
|
|
10478
10625
|
}
|
|
@@ -10647,11 +10794,11 @@ import { spawn as spawn4 } from "child_process";
|
|
|
10647
10794
|
import { closeSync as closeSync3, mkdirSync as mkdirSync12, openSync as openSync3 } from "fs";
|
|
10648
10795
|
|
|
10649
10796
|
// src/cli-entry.ts
|
|
10650
|
-
import { existsSync as
|
|
10797
|
+
import { existsSync as existsSync14 } from "fs";
|
|
10651
10798
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10652
10799
|
var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
|
|
10653
10800
|
function companionCliEntry(deps = {}) {
|
|
10654
|
-
const exists = deps.exists ??
|
|
10801
|
+
const exists = deps.exists ?? existsSync14;
|
|
10655
10802
|
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath2(new URL(rel, import.meta.url)));
|
|
10656
10803
|
for (const candidate of candidates) {
|
|
10657
10804
|
if (exists(candidate)) return candidate;
|
|
@@ -11167,7 +11314,7 @@ function isAlive(kill, pid) {
|
|
|
11167
11314
|
}
|
|
11168
11315
|
|
|
11169
11316
|
// src/commands/transcript.ts
|
|
11170
|
-
import { existsSync as
|
|
11317
|
+
import { existsSync as existsSync15, readFileSync as readFileSync10, readdirSync as readdirSync5 } from "fs";
|
|
11171
11318
|
import { isAbsolute, join as join17 } from "path";
|
|
11172
11319
|
async function transcript(opts = {}) {
|
|
11173
11320
|
const dir2 = transcriptsDir();
|
|
@@ -11342,11 +11489,11 @@ function peek(path) {
|
|
|
11342
11489
|
}
|
|
11343
11490
|
function resolveTarget(dir2, target) {
|
|
11344
11491
|
if (isAbsolute(target) || target.includes("/")) {
|
|
11345
|
-
if (
|
|
11492
|
+
if (existsSync15(target)) return target;
|
|
11346
11493
|
throw new CompanionError(`no transcript at ${target}.`);
|
|
11347
11494
|
}
|
|
11348
11495
|
const exact = join17(dir2, target);
|
|
11349
|
-
if (
|
|
11496
|
+
if (existsSync15(exact)) return exact;
|
|
11350
11497
|
const matches = listFiles(dir2).filter((f) => f.includes(target));
|
|
11351
11498
|
if (matches.length === 1) return join17(dir2, matches[0]);
|
|
11352
11499
|
if (matches.length === 0) {
|