sortie-dogs 0.3.21 → 0.4.1
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 +1 -1
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/plugin/fast-lane.d.ts +21 -0
- package/dist/plugin/fast-lane.js +230 -0
- package/dist/plugin/index.js +159 -30
- package/dist/plugin/task-result-repair.d.ts +3 -0
- package/dist/plugin/task-result-repair.js +2 -2
- package/dist/runtime-assets.d.ts +10 -10
- package/dist/runtime-assets.js +133 -221
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Requirements: Node.js 22.6 or newer, npm, and OpenCode.
|
|
|
20
20
|
|
|
21
21
|
Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [CLI testing](docs/cli-testing.md)
|
|
22
22
|
|
|
23
|
-
Release: [v0.
|
|
23
|
+
Release: [v0.4.1](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.4.1)
|
|
24
24
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
|
-
export declare const RUNTIME_ASSET_VERSION = "0.3.
|
|
5
|
+
export declare const RUNTIME_ASSET_VERSION = "0.3.7-fast-lane-v1";
|
|
6
6
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
|
-
export const RUNTIME_ASSET_VERSION = "0.3.
|
|
5
|
+
export const RUNTIME_ASSET_VERSION = "0.3.7-fast-lane-v1";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const BACKLOG_DRAIN_CAPABILITY = "sortie_enable_backlog_drain";
|
|
2
|
+
export type FastLaneDenialCode = "TURN_STATE_REQUIRED" | "ROLE_FORBIDDEN" | "WORKER_LIMIT" | "SCOUT_GAP_REQUIRED" | "SCOUT_LIMIT" | "SCOUT_TOO_LATE" | "REVIEW_EVIDENCE_REQUIRED" | "REVIEW_PHASE_INVALID" | "REVIEW_LIMIT" | "ADVISOR_TRIGGER_REQUIRED" | "ADVISOR_LIMIT" | "CONSULTATION_RETRY_INVALID" | "CONSULTATION_RETRY_UNAUTHORIZED" | "CONSULTATION_RETRY_MISMATCH" | "BACKLOG_DRAIN_INVALID" | "BACKLOG_DRAIN_TOO_LATE" | "MANUAL_COMPACTION_FORBIDDEN";
|
|
3
|
+
export declare class FastLaneDeniedError extends Error {
|
|
4
|
+
readonly code: FastLaneDenialCode;
|
|
5
|
+
constructor(code: FastLaneDenialCode);
|
|
6
|
+
}
|
|
7
|
+
export interface FastLaneToolOptions {
|
|
8
|
+
readonly consultationFallbackAuthorized?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare class FastLaneController {
|
|
11
|
+
private readonly sessions;
|
|
12
|
+
private setSession;
|
|
13
|
+
beginTurn(sessionID: string, synthetic: boolean): void;
|
|
14
|
+
forget(sessionID: string): void;
|
|
15
|
+
manualCompactionForbidden(sessionID: string): boolean;
|
|
16
|
+
backlogDrainEnabled(sessionID: string): boolean;
|
|
17
|
+
backlogContinuationAllowed(sessionID: string): boolean;
|
|
18
|
+
enableBacklogDrain(sessionID: string, maxUnits: number): void;
|
|
19
|
+
continuationQueued(sessionID: string): void;
|
|
20
|
+
beforeTool(sessionID: string, tool: string, args: unknown, options?: FastLaneToolOptions): void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { isSourceReviewRiskTag, STRATEGY_TRIGGERS } from "../core/consultation.js";
|
|
2
|
+
const MAX_SESSIONS = 256;
|
|
3
|
+
const GAP_CODES = new Set(["manifest", "validation", "owner-risk"]);
|
|
4
|
+
const STRATEGY_TRIGGER_SET = new Set(STRATEGY_TRIGGERS);
|
|
5
|
+
const MANUAL_COMPACTION_TOOLS = new Set(["compact_and_continue", "sortie_compact_and_continue"]);
|
|
6
|
+
export const BACKLOG_DRAIN_CAPABILITY = "sortie_enable_backlog_drain";
|
|
7
|
+
export class FastLaneDeniedError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code) {
|
|
10
|
+
super(`SORTIE_FAST_LANE_DENIED: ${code}`);
|
|
11
|
+
this.name = "FastLaneDeniedError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function taskArgument(args, key) {
|
|
16
|
+
if (args === null || typeof args !== "object" || Array.isArray(args))
|
|
17
|
+
return undefined;
|
|
18
|
+
const value = args[key];
|
|
19
|
+
return typeof value === "string" ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
function lineValue(prompt, key) {
|
|
22
|
+
const values = [...prompt.matchAll(new RegExp(`^\\s*${key}\\s*:\\s*(.+?)\\s*$`, "gmu"))];
|
|
23
|
+
return values.length === 1 ? values[0][1] : undefined;
|
|
24
|
+
}
|
|
25
|
+
function hasReviewEvidence(prompt) {
|
|
26
|
+
if (lineValue(prompt, "canonical_validation_exit") !== "0")
|
|
27
|
+
return false;
|
|
28
|
+
const rawTags = lineValue(prompt, "risk_tags");
|
|
29
|
+
if (rawTags === undefined || !/^\[[^\[\]]+\]$/u.test(rawTags))
|
|
30
|
+
return false;
|
|
31
|
+
const tags = rawTags.slice(1, -1).split(",").map((tag) => tag.trim());
|
|
32
|
+
return tags.length > 0 && tags.every((tag) => tag.length > 0) &&
|
|
33
|
+
new Set(tags).size === tags.length && tags.every(isSourceReviewRiskTag);
|
|
34
|
+
}
|
|
35
|
+
function freshState() {
|
|
36
|
+
return {
|
|
37
|
+
advisorDispatches: 0,
|
|
38
|
+
backlogDrain: false,
|
|
39
|
+
continuationPending: false,
|
|
40
|
+
initialReviewDispatches: 0,
|
|
41
|
+
verificationReviewDispatches: 0,
|
|
42
|
+
scoutDispatches: 0,
|
|
43
|
+
totalWorkerDispatches: 0,
|
|
44
|
+
workerLimit: 1,
|
|
45
|
+
workerDispatches: 0,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function lockedState() {
|
|
49
|
+
return {
|
|
50
|
+
advisorDispatches: 2,
|
|
51
|
+
backlogDrain: false,
|
|
52
|
+
continuationPending: false,
|
|
53
|
+
initialReviewDispatches: 2,
|
|
54
|
+
verificationReviewDispatches: 2,
|
|
55
|
+
scoutDispatches: 1,
|
|
56
|
+
totalWorkerDispatches: 1,
|
|
57
|
+
workerLimit: 1,
|
|
58
|
+
workerDispatches: 1,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function fallbackBasis(prompt) {
|
|
62
|
+
return prompt.replace(/\r\n?/gu, "\n")
|
|
63
|
+
.split("\n")
|
|
64
|
+
.filter((line) => !/^\s*fallback_retry\s*:\s*true\s*$/u.test(line))
|
|
65
|
+
.join("\n")
|
|
66
|
+
.trimEnd();
|
|
67
|
+
}
|
|
68
|
+
export class FastLaneController {
|
|
69
|
+
sessions = new Map();
|
|
70
|
+
setSession(sessionID, state) {
|
|
71
|
+
this.sessions.delete(sessionID);
|
|
72
|
+
this.sessions.set(sessionID, state);
|
|
73
|
+
while (this.sessions.size > MAX_SESSIONS)
|
|
74
|
+
this.sessions.delete(this.sessions.keys().next().value);
|
|
75
|
+
}
|
|
76
|
+
beginTurn(sessionID, synthetic) {
|
|
77
|
+
if (synthetic) {
|
|
78
|
+
const state = this.sessions.get(sessionID);
|
|
79
|
+
if (state === undefined) {
|
|
80
|
+
this.setSession(sessionID, lockedState());
|
|
81
|
+
}
|
|
82
|
+
else if (state.backlogDrain && state.continuationPending) {
|
|
83
|
+
state.advisorDispatches = 0;
|
|
84
|
+
delete state.advisorPrompt;
|
|
85
|
+
state.continuationPending = false;
|
|
86
|
+
state.initialReviewDispatches = 0;
|
|
87
|
+
delete state.initialReviewPrompt;
|
|
88
|
+
state.verificationReviewDispatches = 0;
|
|
89
|
+
delete state.verificationReviewPrompt;
|
|
90
|
+
state.scoutDispatches = 0;
|
|
91
|
+
state.workerDispatches = 0;
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.setSession(sessionID, freshState());
|
|
96
|
+
}
|
|
97
|
+
forget(sessionID) {
|
|
98
|
+
this.sessions.delete(sessionID);
|
|
99
|
+
}
|
|
100
|
+
manualCompactionForbidden(sessionID) {
|
|
101
|
+
const state = this.sessions.get(sessionID);
|
|
102
|
+
return state !== undefined && state.workerDispatches > 0 &&
|
|
103
|
+
(!state.backlogDrain || state.totalWorkerDispatches >= state.workerLimit);
|
|
104
|
+
}
|
|
105
|
+
backlogDrainEnabled(sessionID) {
|
|
106
|
+
return this.sessions.get(sessionID)?.backlogDrain === true;
|
|
107
|
+
}
|
|
108
|
+
backlogContinuationAllowed(sessionID) {
|
|
109
|
+
const state = this.sessions.get(sessionID);
|
|
110
|
+
return state?.backlogDrain === true && state.workerDispatches === 1 &&
|
|
111
|
+
state.totalWorkerDispatches < state.workerLimit && !state.continuationPending;
|
|
112
|
+
}
|
|
113
|
+
enableBacklogDrain(sessionID, maxUnits) {
|
|
114
|
+
const state = this.sessions.get(sessionID);
|
|
115
|
+
if (state === undefined)
|
|
116
|
+
throw new FastLaneDeniedError("TURN_STATE_REQUIRED");
|
|
117
|
+
if (!Number.isInteger(maxUnits) || maxUnits < 4 || maxUnits > 11) {
|
|
118
|
+
throw new FastLaneDeniedError("BACKLOG_DRAIN_INVALID");
|
|
119
|
+
}
|
|
120
|
+
if (state.backlogDrain || state.totalWorkerDispatches > 0) {
|
|
121
|
+
throw new FastLaneDeniedError("BACKLOG_DRAIN_TOO_LATE");
|
|
122
|
+
}
|
|
123
|
+
state.backlogDrain = true;
|
|
124
|
+
state.workerLimit = maxUnits;
|
|
125
|
+
}
|
|
126
|
+
continuationQueued(sessionID) {
|
|
127
|
+
const state = this.sessions.get(sessionID);
|
|
128
|
+
if (this.backlogContinuationAllowed(sessionID) && state !== undefined) {
|
|
129
|
+
state.continuationPending = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
beforeTool(sessionID, tool, args, options = {}) {
|
|
133
|
+
const state = this.sessions.get(sessionID);
|
|
134
|
+
if (state === undefined) {
|
|
135
|
+
if (tool === "task" || MANUAL_COMPACTION_TOOLS.has(tool)) {
|
|
136
|
+
throw new FastLaneDeniedError("TURN_STATE_REQUIRED");
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (MANUAL_COMPACTION_TOOLS.has(tool)) {
|
|
141
|
+
if (!this.backlogContinuationAllowed(sessionID)) {
|
|
142
|
+
throw new FastLaneDeniedError("MANUAL_COMPACTION_FORBIDDEN");
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (tool !== "task")
|
|
147
|
+
return;
|
|
148
|
+
const role = taskArgument(args, "subagent_type");
|
|
149
|
+
const prompt = taskArgument(args, "prompt") ?? "";
|
|
150
|
+
if (role === "dog-worker") {
|
|
151
|
+
if (state.workerDispatches >= 1 || state.totalWorkerDispatches >= state.workerLimit) {
|
|
152
|
+
throw new FastLaneDeniedError("WORKER_LIMIT");
|
|
153
|
+
}
|
|
154
|
+
state.workerDispatches += 1;
|
|
155
|
+
state.totalWorkerDispatches += 1;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (role === "dog-scout") {
|
|
159
|
+
if (state.workerDispatches > 0)
|
|
160
|
+
throw new FastLaneDeniedError("SCOUT_TOO_LATE");
|
|
161
|
+
const gap = lineValue(prompt, "missing_evidence_code");
|
|
162
|
+
if (gap === undefined || !GAP_CODES.has(gap))
|
|
163
|
+
throw new FastLaneDeniedError("SCOUT_GAP_REQUIRED");
|
|
164
|
+
if (state.scoutDispatches >= 1)
|
|
165
|
+
throw new FastLaneDeniedError("SCOUT_LIMIT");
|
|
166
|
+
state.scoutDispatches += 1;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (role === "dog-reviewer") {
|
|
170
|
+
if (!hasReviewEvidence(prompt))
|
|
171
|
+
throw new FastLaneDeniedError("REVIEW_EVIDENCE_REQUIRED");
|
|
172
|
+
const phase = lineValue(prompt, "review_phase");
|
|
173
|
+
if (phase !== "initial" && phase !== "verification") {
|
|
174
|
+
throw new FastLaneDeniedError("REVIEW_PHASE_INVALID");
|
|
175
|
+
}
|
|
176
|
+
const retry = lineValue(prompt, "fallback_retry");
|
|
177
|
+
const count = phase === "initial" ? state.initialReviewDispatches : state.verificationReviewDispatches;
|
|
178
|
+
const previousPrompt = phase === "initial" ? state.initialReviewPrompt : state.verificationReviewPrompt;
|
|
179
|
+
if (phase === "initial" && state.verificationReviewDispatches > 0) {
|
|
180
|
+
throw new FastLaneDeniedError("REVIEW_PHASE_INVALID");
|
|
181
|
+
}
|
|
182
|
+
if (phase === "verification" && state.initialReviewDispatches === 0) {
|
|
183
|
+
throw new FastLaneDeniedError("REVIEW_PHASE_INVALID");
|
|
184
|
+
}
|
|
185
|
+
if ((count === 0 && retry !== undefined) || (count === 1 && retry !== "true")) {
|
|
186
|
+
throw new FastLaneDeniedError("CONSULTATION_RETRY_INVALID");
|
|
187
|
+
}
|
|
188
|
+
if (count === 1 && options.consultationFallbackAuthorized !== true) {
|
|
189
|
+
throw new FastLaneDeniedError("CONSULTATION_RETRY_UNAUTHORIZED");
|
|
190
|
+
}
|
|
191
|
+
if (count === 1 && previousPrompt !== fallbackBasis(prompt)) {
|
|
192
|
+
throw new FastLaneDeniedError("CONSULTATION_RETRY_MISMATCH");
|
|
193
|
+
}
|
|
194
|
+
if (count >= 2)
|
|
195
|
+
throw new FastLaneDeniedError("REVIEW_LIMIT");
|
|
196
|
+
if (phase === "initial") {
|
|
197
|
+
state.initialReviewPrompt ??= fallbackBasis(prompt);
|
|
198
|
+
state.initialReviewDispatches += 1;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
state.verificationReviewPrompt ??= fallbackBasis(prompt);
|
|
202
|
+
state.verificationReviewDispatches += 1;
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (role === "dog-advisor") {
|
|
207
|
+
const trigger = lineValue(prompt, "strategy_trigger");
|
|
208
|
+
if (trigger === undefined || !STRATEGY_TRIGGER_SET.has(trigger)) {
|
|
209
|
+
throw new FastLaneDeniedError("ADVISOR_TRIGGER_REQUIRED");
|
|
210
|
+
}
|
|
211
|
+
const retry = lineValue(prompt, "fallback_retry");
|
|
212
|
+
if ((state.advisorDispatches === 0 && retry !== undefined) ||
|
|
213
|
+
(state.advisorDispatches === 1 && retry !== "true")) {
|
|
214
|
+
throw new FastLaneDeniedError("CONSULTATION_RETRY_INVALID");
|
|
215
|
+
}
|
|
216
|
+
if (state.advisorDispatches === 1 && options.consultationFallbackAuthorized !== true) {
|
|
217
|
+
throw new FastLaneDeniedError("CONSULTATION_RETRY_UNAUTHORIZED");
|
|
218
|
+
}
|
|
219
|
+
if (state.advisorDispatches === 1 && state.advisorPrompt !== fallbackBasis(prompt)) {
|
|
220
|
+
throw new FastLaneDeniedError("CONSULTATION_RETRY_MISMATCH");
|
|
221
|
+
}
|
|
222
|
+
if (state.advisorDispatches >= 2)
|
|
223
|
+
throw new FastLaneDeniedError("ADVISOR_LIMIT");
|
|
224
|
+
state.advisorPrompt ??= fallbackBasis(prompt);
|
|
225
|
+
state.advisorDispatches += 1;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
throw new FastLaneDeniedError("ROLE_FORBIDDEN");
|
|
229
|
+
}
|
|
230
|
+
}
|
package/dist/plugin/index.js
CHANGED
|
@@ -6,10 +6,11 @@ import { normalizeRelativePath, RelativePathError } from "../core/path.js";
|
|
|
6
6
|
import { validateManifest } from "../core/validate-manifest.js";
|
|
7
7
|
import { safeSchemaPointer, validateHandoffSchema, validateOperationManifestSchema, } from "../core/validate-schema.js";
|
|
8
8
|
import { DEFAULT_PLUGIN_OPTIONS, resolvePluginConfiguration, resolvePluginConfigurationSourcesWithGlobal, } from "./config.js";
|
|
9
|
-
import { CONTINUATION_CAPABILITY, createContinuationHooks, } from "./continuation.js";
|
|
9
|
+
import { CONTINUATION_CAPABILITY, CONTINUATION_MARKER, ROLLOVER_MARKER, createContinuationHooks, } from "./continuation.js";
|
|
10
10
|
import { WriteDeniedError, canonicalManifestReadScopes, canonicalManifestWriteScopes, createProjectPaths, createWriteGate, describeUnclassifiedCommand, isGitMutation, isKnownReadOnlyTool, isRemoteMutation, normalizeCommand, resolveProjectRoot, safePath, writeScopesOverlap, } from "./gate.js";
|
|
11
|
+
import { BACKLOG_DRAIN_CAPABILITY, FastLaneController } from "./fast-lane.js";
|
|
11
12
|
import { createModelRoutingHook, } from "./model-routing-hook.js";
|
|
12
|
-
import { createTaskResultRepairHook, markConsultationFallbackRetry, } from "./task-result-repair.js";
|
|
13
|
+
import { createTaskResultRepairHook, markConsultationFallbackRetry, taskChildSessionID, } from "./task-result-repair.js";
|
|
13
14
|
import { configRoot, nearestPackageVersion, reflectionEnabled, ReflectionError, ReflectionStore } from "../reflection/index.js";
|
|
14
15
|
const INPUT_LIMITS = { config: 64 * 1024, manifest: 512 * 1024, handoff: 2 * 1024 * 1024 };
|
|
15
16
|
const INSPECTION_CACHE = { maximum: 256, ttlMilliseconds: 30 * 60 * 1000 };
|
|
@@ -592,6 +593,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
592
593
|
const bindingOperations = new Set();
|
|
593
594
|
const activeSessions = new Map();
|
|
594
595
|
const coordinatorRoots = new Map();
|
|
596
|
+
const coordinatorTaskCalls = new Map();
|
|
595
597
|
const reflectionOwnedRoots = new Set();
|
|
596
598
|
const reflectionClosingRoots = new Set();
|
|
597
599
|
const reflectionInFlight = new Map();
|
|
@@ -602,6 +604,34 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
602
604
|
const sessionRoots = new Map();
|
|
603
605
|
const consultationRetries = new Map();
|
|
604
606
|
const taskResultRepair = createTaskResultRepairHook(input.client);
|
|
607
|
+
const fastLane = new FastLaneController();
|
|
608
|
+
function beginCoordinatorTask(sessionID, callID) {
|
|
609
|
+
const calls = coordinatorTaskCalls.get(sessionID) ?? new Set();
|
|
610
|
+
calls.add(callID);
|
|
611
|
+
coordinatorTaskCalls.set(sessionID, calls);
|
|
612
|
+
}
|
|
613
|
+
function finishCoordinatorTask(sessionID, callID) {
|
|
614
|
+
if (sessionID === undefined || callID === undefined)
|
|
615
|
+
return;
|
|
616
|
+
const calls = coordinatorTaskCalls.get(sessionID);
|
|
617
|
+
if (calls === undefined)
|
|
618
|
+
return;
|
|
619
|
+
calls.delete(callID);
|
|
620
|
+
if (calls.size === 0)
|
|
621
|
+
coordinatorTaskCalls.delete(sessionID);
|
|
622
|
+
}
|
|
623
|
+
function childHasInFlightParentTask(sessionID) {
|
|
624
|
+
const parentID = sessionParents.get(sessionID);
|
|
625
|
+
return parentID !== undefined && (coordinatorTaskCalls.get(parentID)?.size ?? 0) > 0;
|
|
626
|
+
}
|
|
627
|
+
function abortCoordinatorTasks(sessionID) {
|
|
628
|
+
if (!coordinatorTaskCalls.delete(sessionID))
|
|
629
|
+
return;
|
|
630
|
+
for (const [childID, parentID] of [...sessionParents]) {
|
|
631
|
+
if (parentID === sessionID)
|
|
632
|
+
evictSession(childID);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
605
635
|
function hasSessionEnforcementState(sessionID) {
|
|
606
636
|
return sessionAuthorizations.has(sessionID) || bindingPins.has(sessionID);
|
|
607
637
|
}
|
|
@@ -1325,6 +1355,47 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1325
1355
|
return undefined;
|
|
1326
1356
|
}
|
|
1327
1357
|
}
|
|
1358
|
+
async function hostSessionUserTurn(sessionID) {
|
|
1359
|
+
const messages = input.client?.session?.messages;
|
|
1360
|
+
if (messages === undefined)
|
|
1361
|
+
return undefined;
|
|
1362
|
+
try {
|
|
1363
|
+
const response = await messages.call(input.client.session, { path: { id: sessionID } });
|
|
1364
|
+
const payload = isRecord(response) && "data" in response ? response.data : response;
|
|
1365
|
+
if (!Array.isArray(payload))
|
|
1366
|
+
return undefined;
|
|
1367
|
+
for (let index = payload.length - 1; index >= 0; index -= 1) {
|
|
1368
|
+
const message = payload[index];
|
|
1369
|
+
if ((message.info?.role ?? message.role) !== "user")
|
|
1370
|
+
continue;
|
|
1371
|
+
const agent = message.info?.agent ?? message.agent;
|
|
1372
|
+
if (typeof agent !== "string")
|
|
1373
|
+
return undefined;
|
|
1374
|
+
return {
|
|
1375
|
+
agent,
|
|
1376
|
+
synthetic: (message.parts ?? []).some((part) => part.synthetic === true),
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
return undefined;
|
|
1380
|
+
}
|
|
1381
|
+
catch {
|
|
1382
|
+
return undefined;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
async function recoverCoordinatorRoot(sessionID) {
|
|
1386
|
+
if (isCoordinatorSession(sessionID))
|
|
1387
|
+
return true;
|
|
1388
|
+
const identity = await hostSessionIdentity(sessionID);
|
|
1389
|
+
if (identity === undefined || identity.parentPresent)
|
|
1390
|
+
return false;
|
|
1391
|
+
const persistedTurn = await hostSessionUserTurn(sessionID);
|
|
1392
|
+
if (persistedTurn?.agent !== COORDINATOR_AGENT)
|
|
1393
|
+
return false;
|
|
1394
|
+
await rememberCoordinatorRoot(sessionID);
|
|
1395
|
+
releaseSessionEnforcement(sessionID);
|
|
1396
|
+
fastLane.beginTurn(sessionID, persistedTurn?.synthetic ?? false);
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1328
1399
|
function consultationRetryKey(parentID, role) {
|
|
1329
1400
|
return `${parentID}\u0000${role}`;
|
|
1330
1401
|
}
|
|
@@ -1357,10 +1428,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1357
1428
|
const child = await hostSessionIdentity(sessionID);
|
|
1358
1429
|
if (child?.parentID === undefined)
|
|
1359
1430
|
return undefined;
|
|
1360
|
-
|
|
1361
|
-
if (parent?.agent !== COORDINATOR_AGENT || parent.parentPresent)
|
|
1431
|
+
if (!await recoverCoordinatorRoot(child.parentID))
|
|
1362
1432
|
return undefined;
|
|
1363
|
-
await rememberCoordinatorRoot(child.parentID);
|
|
1364
1433
|
rememberParent(sessionID, child.parentID);
|
|
1365
1434
|
return child.parentID;
|
|
1366
1435
|
}
|
|
@@ -1465,7 +1534,20 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1465
1534
|
async execute(_args, context) {
|
|
1466
1535
|
// A configuration failure must not remove the loop; the shipped default still resolves.
|
|
1467
1536
|
await ensureLoaded().catch(() => undefined);
|
|
1468
|
-
|
|
1537
|
+
const result = await continuation.tool.execute({}, context);
|
|
1538
|
+
if (result === "SORTIE_COMPACT_AND_CONTINUE_QUEUED") {
|
|
1539
|
+
fastLane.continuationQueued(context.sessionID);
|
|
1540
|
+
}
|
|
1541
|
+
return result;
|
|
1542
|
+
},
|
|
1543
|
+
}),
|
|
1544
|
+
[BACKLOG_DRAIN_CAPABILITY]: defineTool({
|
|
1545
|
+
description: "Enable one explicit bounded backlog drain before its first worker dispatch.",
|
|
1546
|
+
args: { max_units: defineTool.schema.string() },
|
|
1547
|
+
async execute(args, context) {
|
|
1548
|
+
const maxUnits = Number(args.max_units);
|
|
1549
|
+
fastLane.enableBacklogDrain(context.sessionID, maxUnits);
|
|
1550
|
+
return JSON.stringify({ status: "enabled", max_units: maxUnits });
|
|
1469
1551
|
},
|
|
1470
1552
|
}),
|
|
1471
1553
|
...(reflectionStartup ? {
|
|
@@ -1506,7 +1588,19 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1506
1588
|
} : {}),
|
|
1507
1589
|
},
|
|
1508
1590
|
"experimental.text.complete": async (textInput, textOutput) => {
|
|
1591
|
+
const backlogMarker = fastLane.backlogContinuationAllowed(textInput.sessionID) &&
|
|
1592
|
+
textOutput.text.includes(CONTINUATION_MARKER);
|
|
1593
|
+
if (fastLane.manualCompactionForbidden(textInput.sessionID)) {
|
|
1594
|
+
textOutput.text = textOutput.text
|
|
1595
|
+
.replaceAll(ROLLOVER_MARKER, "")
|
|
1596
|
+
.replaceAll(CONTINUATION_MARKER, "")
|
|
1597
|
+
.trimEnd();
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1509
1600
|
await continuation.textComplete(textInput, textOutput);
|
|
1601
|
+
if (backlogMarker && continuation.blocksTool(textInput.sessionID)) {
|
|
1602
|
+
fastLane.continuationQueued(textInput.sessionID);
|
|
1603
|
+
}
|
|
1510
1604
|
},
|
|
1511
1605
|
"experimental.session.compacting": async (compactInput, compactOutput) => {
|
|
1512
1606
|
await continuation.sessionCompacting(compactInput, compactOutput);
|
|
@@ -1516,10 +1610,12 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1516
1610
|
},
|
|
1517
1611
|
"chat.message": async (chatInput, output) => {
|
|
1518
1612
|
const parentID = chatParentID(chatInput);
|
|
1613
|
+
const synthetic = output.parts.some((part) => isRecord(part) && part.synthetic === true);
|
|
1519
1614
|
if (parentID !== undefined)
|
|
1520
1615
|
rememberParent(chatInput.sessionID, parentID);
|
|
1521
1616
|
const coordinatorOrigin = chatInput.agent === COORDINATOR_AGENT || output.message.agent === COORDINATOR_AGENT;
|
|
1522
1617
|
if (coordinatorOrigin) {
|
|
1618
|
+
fastLane.beginTurn(chatInput.sessionID, synthetic);
|
|
1523
1619
|
releaseSessionEnforcement(chatInput.sessionID);
|
|
1524
1620
|
await rememberCoordinatorRoot(chatInput.sessionID);
|
|
1525
1621
|
}
|
|
@@ -1566,31 +1662,36 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1566
1662
|
}
|
|
1567
1663
|
}
|
|
1568
1664
|
if (coordinatorOrigin) {
|
|
1569
|
-
const synthetic = output.parts.some((part) => isRecord(part) && part.synthetic === true);
|
|
1570
1665
|
continuation.observeModel(chatInput.sessionID, output.message.model, synthetic);
|
|
1571
1666
|
}
|
|
1572
1667
|
},
|
|
1573
|
-
|
|
1574
|
-
|
|
1668
|
+
"experimental.chat.system.transform": async (transformInput, transformOutput) => {
|
|
1669
|
+
if (fastLane.manualCompactionForbidden(transformInput.sessionID)) {
|
|
1670
|
+
transformOutput.system = [...(transformOutput.system ?? []),
|
|
1671
|
+
"SORTIE_FAST_LANE_TERMINAL\nA worker was already dispatched in this normal lane. " +
|
|
1672
|
+
"Do not call a compaction capability or emit a continuation marker. " +
|
|
1673
|
+
"After any required risk-based review or coordinator-owned finalization, return the terminal report and stop."];
|
|
1674
|
+
}
|
|
1675
|
+
if (!reflectionStartup || !(await beginReflection(transformInput.sessionID)))
|
|
1676
|
+
return;
|
|
1677
|
+
const config = reflectionConfiguration;
|
|
1678
|
+
try {
|
|
1679
|
+
if (!config)
|
|
1575
1680
|
return;
|
|
1576
|
-
const
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
finally {
|
|
1591
|
-
endReflection(transformInput.sessionID);
|
|
1592
|
-
}
|
|
1593
|
-
} } : {}),
|
|
1681
|
+
const heading = "SORTIE_PROCESS_REFLECTIONS";
|
|
1682
|
+
const buckets = ["run", "project", "global"]
|
|
1683
|
+
.filter((layer) => config.layers[layer])
|
|
1684
|
+
.map((layer) => ({ layer, ...(layer === "global" ? {} : { run: transformInput.sessionID }) }));
|
|
1685
|
+
const budget = Math.max(0, config.maxInjectedTokens - Buffer.byteLength(`${heading}\n`, "utf8"));
|
|
1686
|
+
const text = await reflectionStore.injectBuckets(buckets, config.maxInjectedEntries, budget, reflectionVersion);
|
|
1687
|
+
if (text)
|
|
1688
|
+
transformOutput.system = [...(transformOutput.system ?? []), `${heading}\n${text}`];
|
|
1689
|
+
}
|
|
1690
|
+
catch { /* reflection is strictly non-invasive */ }
|
|
1691
|
+
finally {
|
|
1692
|
+
endReflection(transformInput.sessionID);
|
|
1693
|
+
}
|
|
1694
|
+
},
|
|
1594
1695
|
"permission.ask": async (permission) => {
|
|
1595
1696
|
if (permission.permission !== "edit")
|
|
1596
1697
|
return;
|
|
@@ -1626,6 +1727,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1626
1727
|
* erases an answer the worker already produced and the coordinator re-dispatches the same work.
|
|
1627
1728
|
*/
|
|
1628
1729
|
"tool.execute.after": async (toolInput, output) => {
|
|
1730
|
+
const completedChildSessionID = toolInput.tool === "task" ? taskChildSessionID(output) : undefined;
|
|
1629
1731
|
try {
|
|
1630
1732
|
const repair = await taskResultRepair(toolInput, output);
|
|
1631
1733
|
if (repair.kind === "unrecoverable-empty" && toolInput.sessionID !== undefined) {
|
|
@@ -1643,13 +1745,32 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1643
1745
|
}
|
|
1644
1746
|
finally {
|
|
1645
1747
|
activeSessions.get(toolInput.sessionID ?? "")?.inFlightCalls.delete(toolInput.callID ?? "");
|
|
1748
|
+
if (toolInput.tool === "task")
|
|
1749
|
+
finishCoordinatorTask(toolInput.sessionID, toolInput.callID);
|
|
1750
|
+
if (completedChildSessionID !== undefined)
|
|
1751
|
+
evictSession(completedChildSessionID);
|
|
1646
1752
|
}
|
|
1647
1753
|
},
|
|
1648
1754
|
"tool.execute.before": async (toolInput, output) => {
|
|
1649
|
-
|
|
1755
|
+
const coordinatorCapability = toolInput.tool === "task" ||
|
|
1756
|
+
toolInput.tool === CONTINUATION_CAPABILITY || toolInput.tool === BACKLOG_DRAIN_CAPABILITY;
|
|
1757
|
+
if (isCoordinatorSession(toolInput.sessionID) ||
|
|
1758
|
+
(coordinatorCapability && await recoverCoordinatorRoot(toolInput.sessionID))) {
|
|
1650
1759
|
if (continuation.blocksTool(toolInput.sessionID)) {
|
|
1651
1760
|
throw new Error("SORTIE_ROLLOVER_PENDING: stop this turn and wait for compaction");
|
|
1652
1761
|
}
|
|
1762
|
+
const taskRole = isRecord(output.args) && typeof output.args.subagent_type === "string"
|
|
1763
|
+
? output.args.subagent_type
|
|
1764
|
+
: undefined;
|
|
1765
|
+
const role = consultationAgent(taskRole);
|
|
1766
|
+
const consultationFallbackAuthorized = role !== undefined &&
|
|
1767
|
+
consultationRetries.get(consultationRetryKey(toolInput.sessionID, role))?.phase === "pending";
|
|
1768
|
+
fastLane.beforeTool(toolInput.sessionID, toolInput.tool, output.args, {
|
|
1769
|
+
consultationFallbackAuthorized,
|
|
1770
|
+
});
|
|
1771
|
+
if (toolInput.tool === "task" && taskRole === "dog-worker") {
|
|
1772
|
+
beginCoordinatorTask(toolInput.sessionID, toolInput.callID);
|
|
1773
|
+
}
|
|
1653
1774
|
return;
|
|
1654
1775
|
}
|
|
1655
1776
|
const status = activeSessionStatus(toolInput.sessionID);
|
|
@@ -1747,6 +1868,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1747
1868
|
return;
|
|
1748
1869
|
}
|
|
1749
1870
|
if (event.type === "session.deleted") {
|
|
1871
|
+
fastLane.forget(eventSessionID);
|
|
1872
|
+
abortCoordinatorTasks(eventSessionID);
|
|
1750
1873
|
if (reflectionStore !== undefined && reflectionConfiguration?.layers.run && reflectionOwnedRoots.has(eventSessionID)) {
|
|
1751
1874
|
reflectionClosingRoots.add(eventSessionID);
|
|
1752
1875
|
await waitForReflections(eventSessionID);
|
|
@@ -1785,18 +1908,24 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1785
1908
|
await continuation.sessionCompacted(eventSessionID);
|
|
1786
1909
|
if (event.type === "session.idle")
|
|
1787
1910
|
await continuation.sessionIdle(eventSessionID);
|
|
1911
|
+
if (event.type === "session.idle" && isCoordinatorSession(eventSessionID)) {
|
|
1912
|
+
abortCoordinatorTasks(eventSessionID);
|
|
1913
|
+
}
|
|
1788
1914
|
if (!isActiveSession(eventSessionID))
|
|
1789
1915
|
return;
|
|
1790
1916
|
if (event.type !== "session.idle")
|
|
1791
1917
|
touchActiveSession(eventSessionID);
|
|
1792
1918
|
if (event.type === "session.idle" && eventSessionID !== undefined) {
|
|
1793
1919
|
activeSessions.get(eventSessionID)?.inFlightCalls.clear();
|
|
1920
|
+
if (childHasInFlightParentTask(eventSessionID)) {
|
|
1921
|
+
touchActiveSession(eventSessionID);
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1794
1924
|
const authorization = sessionAuthorizations.get(eventSessionID);
|
|
1795
1925
|
if (authorization === undefined)
|
|
1796
1926
|
return;
|
|
1797
1927
|
try {
|
|
1798
|
-
// Idle
|
|
1799
|
-
// worker must bind again, so a completed serial worker cannot block later work.
|
|
1928
|
+
// Idle is the abnormal-exit fallback when the parent Task completion hook never arrives.
|
|
1800
1929
|
await inspect(authorization.handoffPath, eventSessionID);
|
|
1801
1930
|
}
|
|
1802
1931
|
catch {
|
|
@@ -27,8 +27,10 @@ export interface SessionMessagePart {
|
|
|
27
27
|
export interface SessionMessage {
|
|
28
28
|
readonly info?: {
|
|
29
29
|
readonly role?: unknown;
|
|
30
|
+
readonly agent?: unknown;
|
|
30
31
|
} | undefined;
|
|
31
32
|
readonly role?: unknown;
|
|
33
|
+
readonly agent?: unknown;
|
|
32
34
|
readonly parts?: readonly SessionMessagePart[] | undefined;
|
|
33
35
|
}
|
|
34
36
|
/** The subset of the OpenCode SDK client this repair depends on. */
|
|
@@ -56,6 +58,7 @@ export type TaskResultRepairOutcome = {
|
|
|
56
58
|
export declare const CONSULTATION_FALLBACK_RETRY_MARKER = "SORTIE_CONSULTATION_FALLBACK_RETRY";
|
|
57
59
|
/** Assistant text the child actually produced, ignoring the empty tail that caused the defect. */
|
|
58
60
|
export declare function lastAssistantText(messages: readonly SessionMessage[]): string | undefined;
|
|
61
|
+
export declare function taskChildSessionID(output: TaskToolExecuteOutput): string | undefined;
|
|
59
62
|
/**
|
|
60
63
|
* Repair only the exact defect: a completed `task` call whose result body is empty while the child
|
|
61
64
|
* session holds real assistant text. Everything else is left byte-identical.
|
|
@@ -36,7 +36,7 @@ export function lastAssistantText(messages) {
|
|
|
36
36
|
}
|
|
37
37
|
return undefined;
|
|
38
38
|
}
|
|
39
|
-
function
|
|
39
|
+
export function taskChildSessionID(output) {
|
|
40
40
|
const metadata = output.metadata;
|
|
41
41
|
if (metadata === null || typeof metadata !== "object")
|
|
42
42
|
return undefined;
|
|
@@ -72,7 +72,7 @@ export function createTaskResultRepairHook(client) {
|
|
|
72
72
|
const match = emptyResultMatch(output);
|
|
73
73
|
if (match === undefined)
|
|
74
74
|
return unchanged;
|
|
75
|
-
const sessionID =
|
|
75
|
+
const sessionID = taskChildSessionID(output);
|
|
76
76
|
if (sessionID === undefined)
|
|
77
77
|
return unchanged;
|
|
78
78
|
let recovered;
|