sortie-dogs 0.9.3 → 0.9.4

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/README.md CHANGED
@@ -24,7 +24,7 @@ Requirements: Node.js 22.6 or newer, npm, and OpenCode.
24
24
 
25
25
  Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [CLI testing](docs/cli-testing.md)
26
26
 
27
- Release: [v0.9.3](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.3)
27
+ Release: [v0.9.4](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.4)
28
28
 
29
29
  ## Quick start
30
30
 
@@ -4,6 +4,15 @@ export declare class FastLaneDeniedError extends Error {
4
4
  readonly code: FastLaneDenialCode;
5
5
  constructor(code: FastLaneDenialCode);
6
6
  }
7
+ export interface WorkerAccounting {
8
+ readonly totalWorkerDispatches: number;
9
+ readonly workerDispatches: number;
10
+ readonly workerInFlight: boolean;
11
+ readonly workerResumeUsed: boolean;
12
+ readonly workerResumeSessionID?: string;
13
+ readonly workerResumeTaskID?: string;
14
+ readonly workerTaskID?: string;
15
+ }
7
16
  export interface FastLaneToolOptions {
8
17
  readonly readonlyDiagnosisAuthorized?: boolean;
9
18
  readonly consultationFallbackAuthorized?: boolean;
@@ -25,5 +34,8 @@ export declare class FastLaneController {
25
34
  continuationQueued(sessionID: string): void;
26
35
  authorizeRecoverableWorkerResume(sessionID: string, taskID: string, childSessionID: string): boolean;
27
36
  workerCompleted(sessionID: string): void;
37
+ /** Snapshot worker accounting so a later denial in the same dispatch cannot leak the slot. */
38
+ snapshotWorkerAccounting(sessionID: string): WorkerAccounting | undefined;
39
+ restoreWorkerAccounting(sessionID: string, snapshot: WorkerAccounting | undefined): void;
28
40
  beforeTool(sessionID: string, tool: string, args: unknown, options?: FastLaneToolOptions): string | undefined;
29
41
  }
@@ -199,6 +199,32 @@ export class FastLaneController {
199
199
  if (state !== undefined)
200
200
  state.workerInFlight = false;
201
201
  }
202
+ /** Snapshot worker accounting so a later denial in the same dispatch cannot leak the slot. */
203
+ snapshotWorkerAccounting(sessionID) {
204
+ const state = this.sessions.get(sessionID);
205
+ if (state === undefined)
206
+ return undefined;
207
+ return { totalWorkerDispatches: state.totalWorkerDispatches, workerDispatches: state.workerDispatches,
208
+ workerInFlight: state.workerInFlight, workerResumeUsed: state.workerResumeUsed,
209
+ workerResumeSessionID: state.workerResumeSessionID, workerResumeTaskID: state.workerResumeTaskID,
210
+ workerTaskID: state.workerTaskID };
211
+ }
212
+ restoreWorkerAccounting(sessionID, snapshot) {
213
+ const state = this.sessions.get(sessionID);
214
+ if (state === undefined || snapshot === undefined)
215
+ return;
216
+ state.totalWorkerDispatches = snapshot.totalWorkerDispatches;
217
+ state.workerDispatches = snapshot.workerDispatches;
218
+ state.workerInFlight = snapshot.workerInFlight;
219
+ state.workerResumeUsed = snapshot.workerResumeUsed;
220
+ for (const [key, value] of Object.entries({ workerResumeSessionID: snapshot.workerResumeSessionID,
221
+ workerResumeTaskID: snapshot.workerResumeTaskID, workerTaskID: snapshot.workerTaskID })) {
222
+ if (value === undefined)
223
+ delete state[key];
224
+ else
225
+ state[key] = value;
226
+ }
227
+ }
202
228
  beforeTool(sessionID, tool, args, options = {}) {
203
229
  const state = this.sessions.get(sessionID);
204
230
  if (state === undefined) {
@@ -6231,46 +6231,55 @@ export const SortieDogsPlugin = async (input, options) => {
6231
6231
  // therefore be repaired by a corrected Task call in this same coordinator turn.
6232
6232
  await bindGoalDeclaration(toolInput.sessionID, typeof output.args.prompt === "string" ? output.args.prompt : "");
6233
6233
  }
6234
+ // Accounting is provisional until this dispatch is fully admitted. A later denial such as a
6235
+ // budget stop must release the serial slot, or no worker could be dispatched or resumed again.
6236
+ const workerAccounting = fastLane.snapshotWorkerAccounting(toolInput.sessionID);
6234
6237
  const resumedWorkerSessionID = fastLane.beforeTool(toolInput.sessionID, toolInput.tool, output.args, {
6235
6238
  readonlyDiagnosisAuthorized: readonlyDiagnosis,
6236
6239
  consultationFallbackAuthorized,
6237
6240
  parallelWorkerAlreadyBound,
6238
6241
  parallelWorkerAuthorized,
6239
6242
  });
6240
- if (toolInput.tool === "task" && taskRole !== undefined && IMPLEMENTATION_AGENTS.has(taskRole) &&
6241
- isRecord(output.args)) {
6242
- await reserveGoalDispatch(toolInput.sessionID, toolInput.callID, typeof output.args.prompt === "string" ? output.args.prompt : "");
6243
- }
6244
- if (validatedRootAcceptance !== undefined && reservedParallelDescriptor === undefined) {
6245
- rootAcceptanceContinuity.delete(toolInput.sessionID);
6246
- rootAcceptanceContinuity.set(toolInput.sessionID, validatedRootAcceptance);
6247
- while (rootAcceptanceContinuity.size > ACTIVE_SESSION_CACHE.maximum) {
6248
- rootAcceptanceContinuity.delete(rootAcceptanceContinuity.keys().next().value);
6243
+ try {
6244
+ if (toolInput.tool === "task" && taskRole !== undefined && IMPLEMENTATION_AGENTS.has(taskRole) &&
6245
+ isRecord(output.args)) {
6246
+ await reserveGoalDispatch(toolInput.sessionID, toolInput.callID, typeof output.args.prompt === "string" ? output.args.prompt : "");
6249
6247
  }
6250
- }
6251
- if (resumedWorkerSessionID !== undefined) {
6252
- const recoverableParallel = parallelRecoverableChildren.get(resumedWorkerSessionID);
6253
- if (recoverableParallel !== undefined) {
6254
- parallelCalls.set(toolInput.callID, recoverableParallel);
6255
- parallelRecoverableChildren.delete(resumedWorkerSessionID);
6248
+ if (validatedRootAcceptance !== undefined && reservedParallelDescriptor === undefined) {
6249
+ rootAcceptanceContinuity.delete(toolInput.sessionID);
6250
+ rootAcceptanceContinuity.set(toolInput.sessionID, validatedRootAcceptance);
6251
+ while (rootAcceptanceContinuity.size > ACTIVE_SESSION_CACHE.maximum) {
6252
+ rootAcceptanceContinuity.delete(rootAcceptanceContinuity.keys().next().value);
6253
+ }
6256
6254
  }
6257
- recoverableWorkerChildren.delete(resumedWorkerSessionID);
6258
- }
6259
- if (toolInput.tool === "task" && taskRole !== undefined && IMPLEMENTATION_AGENTS.has(taskRole)) {
6260
- if (readonlyDiagnosis && isRecord(output.args))
6261
- await claimDiagnosisTask(toolInput.sessionID, toolInput.callID, output.args, true);
6262
- bootstrapRequired = false;
6263
- bootstrapCompleted = true;
6264
- bootstrapIdleWarnings.delete(toolInput.sessionID);
6265
- beginCoordinatorTask(toolInput.sessionID, toolInput.callID);
6266
- if (reservedParallelDescriptor !== undefined) {
6267
- parallelCalls.set(toolInput.callID, {
6268
- ownerRoot: toolInput.sessionID,
6269
- descriptor: reservedParallelDescriptor,
6270
- completionCallID: toolInput.callID,
6271
- });
6255
+ if (resumedWorkerSessionID !== undefined) {
6256
+ const recoverableParallel = parallelRecoverableChildren.get(resumedWorkerSessionID);
6257
+ if (recoverableParallel !== undefined) {
6258
+ parallelCalls.set(toolInput.callID, recoverableParallel);
6259
+ parallelRecoverableChildren.delete(resumedWorkerSessionID);
6260
+ }
6261
+ recoverableWorkerChildren.delete(resumedWorkerSessionID);
6262
+ }
6263
+ if (toolInput.tool === "task" && taskRole !== undefined && IMPLEMENTATION_AGENTS.has(taskRole)) {
6264
+ if (readonlyDiagnosis && isRecord(output.args))
6265
+ await claimDiagnosisTask(toolInput.sessionID, toolInput.callID, output.args, true);
6266
+ bootstrapRequired = false;
6267
+ bootstrapCompleted = true;
6268
+ bootstrapIdleWarnings.delete(toolInput.sessionID);
6269
+ beginCoordinatorTask(toolInput.sessionID, toolInput.callID);
6270
+ if (reservedParallelDescriptor !== undefined) {
6271
+ parallelCalls.set(toolInput.callID, {
6272
+ ownerRoot: toolInput.sessionID,
6273
+ descriptor: reservedParallelDescriptor,
6274
+ completionCallID: toolInput.callID,
6275
+ });
6276
+ }
6272
6277
  }
6273
6278
  }
6279
+ catch (error) {
6280
+ fastLane.restoreWorkerAccounting(toolInput.sessionID, workerAccounting);
6281
+ throw error;
6282
+ }
6274
6283
  return;
6275
6284
  }
6276
6285
  const status = activeSessionStatus(toolInput.sessionID);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",