@cabane/companion 0.6.88 → 0.6.89
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 +101 -4
- package/dist/runtime.js +101 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2457,10 +2457,22 @@ var CabaneApi = class {
|
|
|
2457
2457
|
// token whose turn has ended. The dispatcher mints it before this call and
|
|
2458
2458
|
// reuses the same value on its active-run PATCH, so the token's turn id and the
|
|
2459
2459
|
// pair's `active_turn_id` agree.
|
|
2460
|
-
|
|
2461
|
-
|
|
2460
|
+
// CT1380: `resumed` says this dispatch is a REPLAY of an interrupted turn, so
|
|
2461
|
+
// the server should hand back `turnSeqFloor` — the turn's committed seq
|
|
2462
|
+
// high-water mark. Only the supervisor knows (it minted the id or recovered
|
|
2463
|
+
// it), so it is passed down rather than inferred here. It gates the COST of
|
|
2464
|
+
// the aggregate, never the tenancy scoping, which the route applies
|
|
2465
|
+
// unconditionally.
|
|
2466
|
+
getTurnContext(conversationId, messageId2, turnId, resumed) {
|
|
2467
|
+
const q = `conversationId=${encodeURIComponent(conversationId)}&messageId=${encodeURIComponent(messageId2)}` + (turnId ? `&turnId=${encodeURIComponent(turnId)}` : "") + (resumed ? "&resumed=1" : "");
|
|
2462
2468
|
return this.request("GET", `/api/agent/turn-context?${q}`);
|
|
2463
2469
|
}
|
|
2470
|
+
// CT1380: the local half of a resumed turn's seq floor (see
|
|
2471
|
+
// `Outbox.maxSeqForTurn`). 0 when no outbox is configured — a one-shot CLI or
|
|
2472
|
+
// test has no durable queue, so nothing can be hiding in it.
|
|
2473
|
+
outboxMaxSeqForTurn(turnId) {
|
|
2474
|
+
return this.opts.outbox?.maxSeqForTurn(turnId) ?? 0;
|
|
2475
|
+
}
|
|
2464
2476
|
// CT714: read a turn's recorded turn-control intent. An EXTERNAL adapter
|
|
2465
2477
|
// (Codex / opencode) records `reply_to` / `skip_turn` into `turn_intents`
|
|
2466
2478
|
// server-side (the URL MCP surface) rather than the dispatcher's in-memory
|
|
@@ -7967,6 +7979,39 @@ async function writeCodexInstructionsFile(contents) {
|
|
|
7967
7979
|
};
|
|
7968
7980
|
}
|
|
7969
7981
|
|
|
7982
|
+
// src/turn-seq-floor.ts
|
|
7983
|
+
var SeqFloorUnavailable = class extends Error {
|
|
7984
|
+
constructor(detail) {
|
|
7985
|
+
super(`seq_floor_unavailable: ${detail}`);
|
|
7986
|
+
this.detail = detail;
|
|
7987
|
+
this.name = "SeqFloorUnavailable";
|
|
7988
|
+
}
|
|
7989
|
+
detail;
|
|
7990
|
+
};
|
|
7991
|
+
function resolveSeqFloor(sources, ctx) {
|
|
7992
|
+
const { serverFloor, outboxFloor } = sources;
|
|
7993
|
+
if (serverFloor === void 0) {
|
|
7994
|
+
throw new SeqFloorUnavailable("server sent no committed floor for a resumed turn");
|
|
7995
|
+
}
|
|
7996
|
+
const floor = Math.max(serverFloor, outboxFloor);
|
|
7997
|
+
ctx.log.info(
|
|
7998
|
+
{ turnId: ctx.turnId, floor, outboxFloor, serverFloor },
|
|
7999
|
+
"companion: resumed turn \u2014 seq counter seeded above its committed high-water mark"
|
|
8000
|
+
);
|
|
8001
|
+
return floor;
|
|
8002
|
+
}
|
|
8003
|
+
function readOutboxFloor(read, turnId, log) {
|
|
8004
|
+
try {
|
|
8005
|
+
return read(turnId);
|
|
8006
|
+
} catch (err) {
|
|
8007
|
+
log.error(
|
|
8008
|
+
{ turnId, err: err instanceof Error ? err.message : String(err) },
|
|
8009
|
+
"companion: the on-disk outbox floor is unreadable; refusing to resume"
|
|
8010
|
+
);
|
|
8011
|
+
throw new SeqFloorUnavailable("outbox unreadable");
|
|
8012
|
+
}
|
|
8013
|
+
}
|
|
8014
|
+
|
|
7970
8015
|
// src/prepared.ts
|
|
7971
8016
|
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
|
|
7972
8017
|
import { join as join11 } from "path";
|
|
@@ -8417,6 +8462,7 @@ var TurnExecution = class {
|
|
|
8417
8462
|
this.opts = opts;
|
|
8418
8463
|
this.supervisor = supervisor;
|
|
8419
8464
|
this.payload = payload;
|
|
8465
|
+
this.resumed = handleOpts.resumed === true;
|
|
8420
8466
|
this.dispatchId = payload.messageId;
|
|
8421
8467
|
this.workspaceId = opts.workspaceId;
|
|
8422
8468
|
this.turnLog = opts.log.child({
|
|
@@ -8439,7 +8485,13 @@ var TurnExecution = class {
|
|
|
8439
8485
|
outcome = initialOutcome();
|
|
8440
8486
|
seqCounter = 0;
|
|
8441
8487
|
// CT11: per-turn monotonic counter mirroring the in-process dispatcher's.
|
|
8488
|
+
//
|
|
8489
|
+
// CT1380: 0 for a NEW turn only — a resume is seeded in `fetchContext` first.
|
|
8442
8490
|
nextSeq = () => ++this.seqCounter;
|
|
8491
|
+
// CT1380: a REPLAY, told to us by the supervisor — not derivable here, since
|
|
8492
|
+
// `turnId` is set on every dispatch. `resumedFromSeq` is what it seeded from.
|
|
8493
|
+
resumed;
|
|
8494
|
+
resumedFromSeq;
|
|
8443
8495
|
turnContext;
|
|
8444
8496
|
resolvedMcpServers;
|
|
8445
8497
|
effectiveCwd;
|
|
@@ -8485,6 +8537,10 @@ var TurnExecution = class {
|
|
|
8485
8537
|
await this.acquireLease();
|
|
8486
8538
|
await this.selectAdapter();
|
|
8487
8539
|
} catch (err) {
|
|
8540
|
+
if (err instanceof SeqFloorUnavailable) {
|
|
8541
|
+
this.supervisor.releaseAbort(this.turnId, this.abortController);
|
|
8542
|
+
return this.concludeBeforeRun(err.message, err.message);
|
|
8543
|
+
}
|
|
8488
8544
|
if (err instanceof TurnConcluded) {
|
|
8489
8545
|
this.supervisor.releaseAbort(this.turnId, this.abortController);
|
|
8490
8546
|
return this.admitted ? this.concludeAdmittedRun(err.reason, err.errorReason) : this.concludeBeforeRun(err.reason, err.errorReason);
|
|
@@ -8563,12 +8619,14 @@ var TurnExecution = class {
|
|
|
8563
8619
|
}
|
|
8564
8620
|
async fetchContext() {
|
|
8565
8621
|
const { payload, turnId, turnLog } = this;
|
|
8622
|
+
const outboxFloor = this.resumed ? readOutboxFloor((id) => this.opts.api.outboxMaxSeqForTurn(id), turnId, turnLog) : 0;
|
|
8566
8623
|
let turnContext;
|
|
8567
8624
|
try {
|
|
8568
8625
|
turnContext = await this.opts.api.getTurnContext(
|
|
8569
8626
|
payload.conversationId,
|
|
8570
8627
|
payload.messageId,
|
|
8571
|
-
turnId
|
|
8628
|
+
turnId,
|
|
8629
|
+
this.resumed
|
|
8572
8630
|
);
|
|
8573
8631
|
} catch (err) {
|
|
8574
8632
|
const status2 = err instanceof ApiError ? err.status : 0;
|
|
@@ -8585,6 +8643,11 @@ var TurnExecution = class {
|
|
|
8585
8643
|
throw this.concluded(fetchReason, fetchReason);
|
|
8586
8644
|
}
|
|
8587
8645
|
this.turnContext = turnContext;
|
|
8646
|
+
if (this.resumed) {
|
|
8647
|
+
const sources = { serverFloor: turnContext.turnSeqFloor, outboxFloor };
|
|
8648
|
+
this.resumedFromSeq = resolveSeqFloor(sources, { turnId, log: turnLog });
|
|
8649
|
+
this.seqCounter = Math.max(this.seqCounter, this.resumedFromSeq);
|
|
8650
|
+
}
|
|
8588
8651
|
}
|
|
8589
8652
|
gateTrigger() {
|
|
8590
8653
|
const { payload, turnLog } = this;
|
|
@@ -8815,6 +8878,8 @@ ${reason}`,
|
|
|
8815
8878
|
try {
|
|
8816
8879
|
await this.opts.api.setActiveRun(workspaceId, payload.conversationId, payload.agentId, {
|
|
8817
8880
|
activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8881
|
+
// CT1380: the durable record that this turn resumed, and from where.
|
|
8882
|
+
...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
|
|
8818
8883
|
// CT33: hand the server this turn's id so the new-run chokepoint's
|
|
8819
8884
|
// `closeAbandonedTurns` sweep excludes it. The prepare hook may have
|
|
8820
8885
|
// already emitted a "preparing" activity row for this turn above (it
|
|
@@ -9563,6 +9628,35 @@ var Outbox = class {
|
|
|
9563
9628
|
);
|
|
9564
9629
|
return entries;
|
|
9565
9630
|
}
|
|
9631
|
+
// CT1380: the highest `seq` this turn has queued but not yet delivered — the
|
|
9632
|
+
// half of a resumed turn's seq floor the SERVER CANNOT SEE. A commit that
|
|
9633
|
+
// failed transiently before the crash is sitting in this directory with its
|
|
9634
|
+
// seq already spent, and the restart's drain is fire-and-forget
|
|
9635
|
+
// (`drain.kick()`, never awaited), so `maxSeqForTurn` on the server can report
|
|
9636
|
+
// below it and the resumed run would mint that number a second time.
|
|
9637
|
+
//
|
|
9638
|
+
// Reads the directory rather than `list()` because only the filename matters:
|
|
9639
|
+
// `<turnId>__<seq>.json` carries both halves of the key, so a corrupt body
|
|
9640
|
+
// can't hide a spent seq from the floor. Returns 0 for a turn with nothing
|
|
9641
|
+
// queued, which is the overwhelmingly common case.
|
|
9642
|
+
// THROWS rather than returning 0 when the directory exists but cannot be
|
|
9643
|
+
// read. 0 is a claim ("this turn has nothing queued"), and an unreadable
|
|
9644
|
+
// directory cannot support it — a queued entry holding seq N would be
|
|
9645
|
+
// invisible and the resumed run would mint N again. A missing directory is
|
|
9646
|
+
// different: it is positive evidence that nothing was ever queued here.
|
|
9647
|
+
maxSeqForTurn(turnId) {
|
|
9648
|
+
const dir2 = this.dir();
|
|
9649
|
+
if (!existsSync12(dir2)) return 0;
|
|
9650
|
+
const names = readdirSync3(dir2);
|
|
9651
|
+
const prefix = `${encodeURIComponent(turnId)}__`;
|
|
9652
|
+
let max = 0;
|
|
9653
|
+
for (const name of names) {
|
|
9654
|
+
if (!name.startsWith(prefix) || !name.endsWith(".json")) continue;
|
|
9655
|
+
const seq = Number(name.slice(prefix.length, -".json".length));
|
|
9656
|
+
if (Number.isInteger(seq) && seq > max) max = seq;
|
|
9657
|
+
}
|
|
9658
|
+
return max;
|
|
9659
|
+
}
|
|
9566
9660
|
// Remove a delivered (or terminally-discarded) entry. No-op if already gone.
|
|
9567
9661
|
remove(turnId, seq) {
|
|
9568
9662
|
try {
|
|
@@ -10479,7 +10573,10 @@ var CompanionSupervisor = class {
|
|
|
10479
10573
|
"companion: resuming an interrupted turn under its original id"
|
|
10480
10574
|
);
|
|
10481
10575
|
}
|
|
10482
|
-
const result = await agent.dispatcher.handle(payload, {
|
|
10576
|
+
const result = await agent.dispatcher.handle(payload, {
|
|
10577
|
+
turnId,
|
|
10578
|
+
resumed: resumedTurnId !== null
|
|
10579
|
+
});
|
|
10483
10580
|
if (ev.id) markCompleted(workspaceId, ev.id);
|
|
10484
10581
|
if (ev.id) wr.cursor.settle(ev.id);
|
|
10485
10582
|
const durationS = (result.durationMs / 1e3).toFixed(1);
|
package/dist/runtime.js
CHANGED
|
@@ -1875,10 +1875,22 @@ var CabaneApi = class {
|
|
|
1875
1875
|
// token whose turn has ended. The dispatcher mints it before this call and
|
|
1876
1876
|
// reuses the same value on its active-run PATCH, so the token's turn id and the
|
|
1877
1877
|
// pair's `active_turn_id` agree.
|
|
1878
|
-
|
|
1879
|
-
|
|
1878
|
+
// CT1380: `resumed` says this dispatch is a REPLAY of an interrupted turn, so
|
|
1879
|
+
// the server should hand back `turnSeqFloor` — the turn's committed seq
|
|
1880
|
+
// high-water mark. Only the supervisor knows (it minted the id or recovered
|
|
1881
|
+
// it), so it is passed down rather than inferred here. It gates the COST of
|
|
1882
|
+
// the aggregate, never the tenancy scoping, which the route applies
|
|
1883
|
+
// unconditionally.
|
|
1884
|
+
getTurnContext(conversationId, messageId2, turnId, resumed) {
|
|
1885
|
+
const q = `conversationId=${encodeURIComponent(conversationId)}&messageId=${encodeURIComponent(messageId2)}` + (turnId ? `&turnId=${encodeURIComponent(turnId)}` : "") + (resumed ? "&resumed=1" : "");
|
|
1880
1886
|
return this.request("GET", `/api/agent/turn-context?${q}`);
|
|
1881
1887
|
}
|
|
1888
|
+
// CT1380: the local half of a resumed turn's seq floor (see
|
|
1889
|
+
// `Outbox.maxSeqForTurn`). 0 when no outbox is configured — a one-shot CLI or
|
|
1890
|
+
// test has no durable queue, so nothing can be hiding in it.
|
|
1891
|
+
outboxMaxSeqForTurn(turnId) {
|
|
1892
|
+
return this.opts.outbox?.maxSeqForTurn(turnId) ?? 0;
|
|
1893
|
+
}
|
|
1882
1894
|
// CT714: read a turn's recorded turn-control intent. An EXTERNAL adapter
|
|
1883
1895
|
// (Codex / opencode) records `reply_to` / `skip_turn` into `turn_intents`
|
|
1884
1896
|
// server-side (the URL MCP surface) rather than the dispatcher's in-memory
|
|
@@ -7464,6 +7476,39 @@ async function writeCodexInstructionsFile(contents) {
|
|
|
7464
7476
|
};
|
|
7465
7477
|
}
|
|
7466
7478
|
|
|
7479
|
+
// src/turn-seq-floor.ts
|
|
7480
|
+
var SeqFloorUnavailable = class extends Error {
|
|
7481
|
+
constructor(detail) {
|
|
7482
|
+
super(`seq_floor_unavailable: ${detail}`);
|
|
7483
|
+
this.detail = detail;
|
|
7484
|
+
this.name = "SeqFloorUnavailable";
|
|
7485
|
+
}
|
|
7486
|
+
detail;
|
|
7487
|
+
};
|
|
7488
|
+
function resolveSeqFloor(sources, ctx) {
|
|
7489
|
+
const { serverFloor, outboxFloor } = sources;
|
|
7490
|
+
if (serverFloor === void 0) {
|
|
7491
|
+
throw new SeqFloorUnavailable("server sent no committed floor for a resumed turn");
|
|
7492
|
+
}
|
|
7493
|
+
const floor = Math.max(serverFloor, outboxFloor);
|
|
7494
|
+
ctx.log.info(
|
|
7495
|
+
{ turnId: ctx.turnId, floor, outboxFloor, serverFloor },
|
|
7496
|
+
"companion: resumed turn \u2014 seq counter seeded above its committed high-water mark"
|
|
7497
|
+
);
|
|
7498
|
+
return floor;
|
|
7499
|
+
}
|
|
7500
|
+
function readOutboxFloor(read, turnId, log) {
|
|
7501
|
+
try {
|
|
7502
|
+
return read(turnId);
|
|
7503
|
+
} catch (err) {
|
|
7504
|
+
log.error(
|
|
7505
|
+
{ turnId, err: err instanceof Error ? err.message : String(err) },
|
|
7506
|
+
"companion: the on-disk outbox floor is unreadable; refusing to resume"
|
|
7507
|
+
);
|
|
7508
|
+
throw new SeqFloorUnavailable("outbox unreadable");
|
|
7509
|
+
}
|
|
7510
|
+
}
|
|
7511
|
+
|
|
7467
7512
|
// src/prepared.ts
|
|
7468
7513
|
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
|
|
7469
7514
|
import { join as join11 } from "path";
|
|
@@ -7914,6 +7959,7 @@ var TurnExecution = class {
|
|
|
7914
7959
|
this.opts = opts;
|
|
7915
7960
|
this.supervisor = supervisor;
|
|
7916
7961
|
this.payload = payload;
|
|
7962
|
+
this.resumed = handleOpts.resumed === true;
|
|
7917
7963
|
this.dispatchId = payload.messageId;
|
|
7918
7964
|
this.workspaceId = opts.workspaceId;
|
|
7919
7965
|
this.turnLog = opts.log.child({
|
|
@@ -7936,7 +7982,13 @@ var TurnExecution = class {
|
|
|
7936
7982
|
outcome = initialOutcome();
|
|
7937
7983
|
seqCounter = 0;
|
|
7938
7984
|
// CT11: per-turn monotonic counter mirroring the in-process dispatcher's.
|
|
7985
|
+
//
|
|
7986
|
+
// CT1380: 0 for a NEW turn only — a resume is seeded in `fetchContext` first.
|
|
7939
7987
|
nextSeq = () => ++this.seqCounter;
|
|
7988
|
+
// CT1380: a REPLAY, told to us by the supervisor — not derivable here, since
|
|
7989
|
+
// `turnId` is set on every dispatch. `resumedFromSeq` is what it seeded from.
|
|
7990
|
+
resumed;
|
|
7991
|
+
resumedFromSeq;
|
|
7940
7992
|
turnContext;
|
|
7941
7993
|
resolvedMcpServers;
|
|
7942
7994
|
effectiveCwd;
|
|
@@ -7982,6 +8034,10 @@ var TurnExecution = class {
|
|
|
7982
8034
|
await this.acquireLease();
|
|
7983
8035
|
await this.selectAdapter();
|
|
7984
8036
|
} catch (err) {
|
|
8037
|
+
if (err instanceof SeqFloorUnavailable) {
|
|
8038
|
+
this.supervisor.releaseAbort(this.turnId, this.abortController);
|
|
8039
|
+
return this.concludeBeforeRun(err.message, err.message);
|
|
8040
|
+
}
|
|
7985
8041
|
if (err instanceof TurnConcluded) {
|
|
7986
8042
|
this.supervisor.releaseAbort(this.turnId, this.abortController);
|
|
7987
8043
|
return this.admitted ? this.concludeAdmittedRun(err.reason, err.errorReason) : this.concludeBeforeRun(err.reason, err.errorReason);
|
|
@@ -8060,12 +8116,14 @@ var TurnExecution = class {
|
|
|
8060
8116
|
}
|
|
8061
8117
|
async fetchContext() {
|
|
8062
8118
|
const { payload, turnId, turnLog } = this;
|
|
8119
|
+
const outboxFloor = this.resumed ? readOutboxFloor((id) => this.opts.api.outboxMaxSeqForTurn(id), turnId, turnLog) : 0;
|
|
8063
8120
|
let turnContext;
|
|
8064
8121
|
try {
|
|
8065
8122
|
turnContext = await this.opts.api.getTurnContext(
|
|
8066
8123
|
payload.conversationId,
|
|
8067
8124
|
payload.messageId,
|
|
8068
|
-
turnId
|
|
8125
|
+
turnId,
|
|
8126
|
+
this.resumed
|
|
8069
8127
|
);
|
|
8070
8128
|
} catch (err) {
|
|
8071
8129
|
const status = err instanceof ApiError ? err.status : 0;
|
|
@@ -8082,6 +8140,11 @@ var TurnExecution = class {
|
|
|
8082
8140
|
throw this.concluded(fetchReason, fetchReason);
|
|
8083
8141
|
}
|
|
8084
8142
|
this.turnContext = turnContext;
|
|
8143
|
+
if (this.resumed) {
|
|
8144
|
+
const sources = { serverFloor: turnContext.turnSeqFloor, outboxFloor };
|
|
8145
|
+
this.resumedFromSeq = resolveSeqFloor(sources, { turnId, log: turnLog });
|
|
8146
|
+
this.seqCounter = Math.max(this.seqCounter, this.resumedFromSeq);
|
|
8147
|
+
}
|
|
8085
8148
|
}
|
|
8086
8149
|
gateTrigger() {
|
|
8087
8150
|
const { payload, turnLog } = this;
|
|
@@ -8312,6 +8375,8 @@ ${reason}`,
|
|
|
8312
8375
|
try {
|
|
8313
8376
|
await this.opts.api.setActiveRun(workspaceId, payload.conversationId, payload.agentId, {
|
|
8314
8377
|
activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8378
|
+
// CT1380: the durable record that this turn resumed, and from where.
|
|
8379
|
+
...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
|
|
8315
8380
|
// CT33: hand the server this turn's id so the new-run chokepoint's
|
|
8316
8381
|
// `closeAbandonedTurns` sweep excludes it. The prepare hook may have
|
|
8317
8382
|
// already emitted a "preparing" activity row for this turn above (it
|
|
@@ -9060,6 +9125,35 @@ var Outbox = class {
|
|
|
9060
9125
|
);
|
|
9061
9126
|
return entries;
|
|
9062
9127
|
}
|
|
9128
|
+
// CT1380: the highest `seq` this turn has queued but not yet delivered — the
|
|
9129
|
+
// half of a resumed turn's seq floor the SERVER CANNOT SEE. A commit that
|
|
9130
|
+
// failed transiently before the crash is sitting in this directory with its
|
|
9131
|
+
// seq already spent, and the restart's drain is fire-and-forget
|
|
9132
|
+
// (`drain.kick()`, never awaited), so `maxSeqForTurn` on the server can report
|
|
9133
|
+
// below it and the resumed run would mint that number a second time.
|
|
9134
|
+
//
|
|
9135
|
+
// Reads the directory rather than `list()` because only the filename matters:
|
|
9136
|
+
// `<turnId>__<seq>.json` carries both halves of the key, so a corrupt body
|
|
9137
|
+
// can't hide a spent seq from the floor. Returns 0 for a turn with nothing
|
|
9138
|
+
// queued, which is the overwhelmingly common case.
|
|
9139
|
+
// THROWS rather than returning 0 when the directory exists but cannot be
|
|
9140
|
+
// read. 0 is a claim ("this turn has nothing queued"), and an unreadable
|
|
9141
|
+
// directory cannot support it — a queued entry holding seq N would be
|
|
9142
|
+
// invisible and the resumed run would mint N again. A missing directory is
|
|
9143
|
+
// different: it is positive evidence that nothing was ever queued here.
|
|
9144
|
+
maxSeqForTurn(turnId) {
|
|
9145
|
+
const dir2 = this.dir();
|
|
9146
|
+
if (!existsSync12(dir2)) return 0;
|
|
9147
|
+
const names = readdirSync3(dir2);
|
|
9148
|
+
const prefix = `${encodeURIComponent(turnId)}__`;
|
|
9149
|
+
let max = 0;
|
|
9150
|
+
for (const name of names) {
|
|
9151
|
+
if (!name.startsWith(prefix) || !name.endsWith(".json")) continue;
|
|
9152
|
+
const seq = Number(name.slice(prefix.length, -".json".length));
|
|
9153
|
+
if (Number.isInteger(seq) && seq > max) max = seq;
|
|
9154
|
+
}
|
|
9155
|
+
return max;
|
|
9156
|
+
}
|
|
9063
9157
|
// Remove a delivered (or terminally-discarded) entry. No-op if already gone.
|
|
9064
9158
|
remove(turnId, seq) {
|
|
9065
9159
|
try {
|
|
@@ -9976,7 +10070,10 @@ var CompanionSupervisor = class {
|
|
|
9976
10070
|
"companion: resuming an interrupted turn under its original id"
|
|
9977
10071
|
);
|
|
9978
10072
|
}
|
|
9979
|
-
const result = await agent.dispatcher.handle(payload, {
|
|
10073
|
+
const result = await agent.dispatcher.handle(payload, {
|
|
10074
|
+
turnId,
|
|
10075
|
+
resumed: resumedTurnId !== null
|
|
10076
|
+
});
|
|
9980
10077
|
if (ev.id) markCompleted(workspaceId, ev.id);
|
|
9981
10078
|
if (ev.id) wr.cursor.settle(ev.id);
|
|
9982
10079
|
const durationS = (result.durationMs / 1e3).toFixed(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.89",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Cabane Companion (headless): connect a coding agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
|
|
6
6
|
"license": "UNLICENSED",
|