sortie-dogs 0.2.13 → 0.2.15
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 +26 -8
- package/dist/plugin/config.d.ts +2 -1
- package/dist/plugin/config.js +8 -2
- package/dist/plugin/continuation.d.ts +11 -6
- package/dist/plugin/continuation.js +113 -50
- package/dist/plugin/index.js +27 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -144,11 +144,22 @@ Optional settings in `.opencode/sortie-dogs.json`:
|
|
|
144
144
|
{
|
|
145
145
|
"operationManifestPath": "operation-manifest.json",
|
|
146
146
|
"handoffPaths": ["handoff.json"],
|
|
147
|
-
"readOnlyTools": ["my_mcp_search"],
|
|
148
|
-
"dedicatedWorkerModel": { "model": "provider/model", "variant": "deep" },
|
|
149
|
-
"continuation": { "enabled": true, "maxAutoContinues":
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
"readOnlyTools": ["my_mcp_search"],
|
|
148
|
+
"dedicatedWorkerModel": { "model": "provider/model", "variant": "deep" },
|
|
149
|
+
"continuation": { "enabled": true, "maxAutoContinues": 2 },
|
|
150
|
+
"reflection": {
|
|
151
|
+
"enabled": false,
|
|
152
|
+
"layers": { "run": true, "project": true, "global": false }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The same schema may be saved globally as
|
|
158
|
+
`~/.config/opencode/sortie-dogs.json` (on Windows,
|
|
159
|
+
`%USERPROFILE%\.config\opencode\sortie-dogs.json`). Precedence is built-in
|
|
160
|
+
defaults, global file, project file, `SORTIE_DOGS_CONFIG`, then plugin factory
|
|
161
|
+
options. OpenCode plugin normalization may omit factory options, so use the
|
|
162
|
+
global file for durable global settings.
|
|
152
163
|
|
|
153
164
|
- `operationManifestPath` moves the manifest; the path is project-relative.
|
|
154
165
|
- `handoffPaths` lists the handoff files the plugin inspects. A worker can only
|
|
@@ -169,9 +180,16 @@ Optional settings in `.opencode/sortie-dogs.json`:
|
|
|
169
180
|
and resumes the same root session on the next independent unit. Only a root
|
|
170
181
|
`dog-coordinator` session is ever resumed: a child session is never promoted and
|
|
171
182
|
another coordinator is never adopted. Set `enabled` to `false` to keep every
|
|
172
|
-
batch manual, raise or lower `maxAutoContinues` (default `
|
|
173
|
-
change the ceiling, and set `summarizeModel` to
|
|
174
|
-
|
|
183
|
+
batch manual, raise or lower `maxAutoContinues` (default `2`, maximum `10`) to
|
|
184
|
+
change the ceiling, and set `summarizeModel` to override the latest coordinator
|
|
185
|
+
model used for compaction. Normal OpenCode auto-compaction keeps the
|
|
186
|
+
host's auto-continue behavior; Sortie suppresses it only while its own
|
|
187
|
+
explicitly queued rollover owns the resume.
|
|
188
|
+
- `reflection` is an opt-in process-prevention companion for an activated root
|
|
189
|
+
`dog-coordinator`. It is disabled by default. Run and project layers default
|
|
190
|
+
to enabled after opt-in; the cross-project global storage layer remains
|
|
191
|
+
disabled unless explicitly enabled. Child and non-coordinator sessions fail
|
|
192
|
+
closed, and `SORTIE_REFLECTION=0` is an immediate kill switch.
|
|
175
193
|
|
|
176
194
|
## Why Sortie-dogs
|
|
177
195
|
|
package/dist/plugin/config.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface ContinuationConfiguration {
|
|
|
44
44
|
readonly agent: string;
|
|
45
45
|
readonly capability: string;
|
|
46
46
|
readonly maxAutoContinues: number;
|
|
47
|
-
/** Absent
|
|
47
|
+
/** Absent reuses the latest coordinator model observed for this session. */
|
|
48
48
|
readonly summarizeModel?: ModelTarget;
|
|
49
49
|
}
|
|
50
50
|
export interface ConsultationPolicyInput {
|
|
@@ -99,3 +99,4 @@ export declare const DEFAULT_PLUGIN_OPTIONS: Readonly<Omit<Required<SortieDogsPl
|
|
|
99
99
|
export declare function resolvePluginConfiguration(...values: readonly unknown[]): PluginConfiguration;
|
|
100
100
|
/** Resolve the plugin's fixed source boundaries: project-local first, environment and host global. */
|
|
101
101
|
export declare function resolvePluginConfigurationSources(projectValue: unknown, environmentValue: unknown, hostValue: unknown): PluginConfigurationSources;
|
|
102
|
+
export declare function resolvePluginConfigurationSourcesWithGlobal(globalValue: unknown, projectValue: unknown, environmentValue: unknown, hostValue: unknown): PluginConfigurationSources;
|
package/dist/plugin/config.js
CHANGED
|
@@ -372,17 +372,23 @@ export function resolvePluginConfiguration(...values) {
|
|
|
372
372
|
}
|
|
373
373
|
/** Resolve the plugin's fixed source boundaries: project-local first, environment and host global. */
|
|
374
374
|
export function resolvePluginConfigurationSources(projectValue, environmentValue, hostValue) {
|
|
375
|
-
|
|
375
|
+
return resolvePluginConfigurationSourcesWithGlobal(undefined, projectValue, environmentValue, hostValue);
|
|
376
|
+
}
|
|
377
|
+
export function resolvePluginConfigurationSourcesWithGlobal(globalValue, projectValue, environmentValue, hostValue) {
|
|
378
|
+
const configured = resolvePluginConfiguration(globalValue, projectValue, environmentValue, hostValue);
|
|
376
379
|
if (configured.kind === "invalid")
|
|
377
380
|
return configured;
|
|
381
|
+
const globalLayer = parseLayer(globalValue);
|
|
378
382
|
const projectLayer = parseLayer(projectValue);
|
|
379
383
|
const environmentLayer = parseLayer(environmentValue);
|
|
380
384
|
const hostLayer = parseLayer(hostValue);
|
|
381
|
-
if (
|
|
385
|
+
if (globalLayer === undefined || projectLayer === undefined ||
|
|
386
|
+
environmentLayer === undefined || hostLayer === undefined) {
|
|
382
387
|
return { kind: "invalid" };
|
|
383
388
|
}
|
|
384
389
|
const globalModelRouting = Object.fromEntries(Object.entries({
|
|
385
390
|
...recommendedRoleRouting(configured.dedicatedWorkerModel),
|
|
391
|
+
...(globalLayer.modelRouting ?? {}),
|
|
386
392
|
...(environmentLayer.modelRouting ?? {}),
|
|
387
393
|
...(hostLayer.modelRouting ?? {}),
|
|
388
394
|
}).filter(([role]) => !isFixedModelRole(role)));
|
|
@@ -22,7 +22,7 @@ export declare const ROLLOVER_MARKER = "<!-- SORTIE_COMPACT -->";
|
|
|
22
22
|
export declare const AUTO_CONTINUE_PREFIX = "SORTIE_AUTO_CONTINUE";
|
|
23
23
|
/** First line the rollover summary must emit, mirroring the batch target of three attempts. */
|
|
24
24
|
export declare const ROLLOVER_TOKEN = "SORTIE_ROLLOVER_COMPACTED";
|
|
25
|
-
export declare const DEFAULT_MAX_AUTO_CONTINUES =
|
|
25
|
+
export declare const DEFAULT_MAX_AUTO_CONTINUES = 2;
|
|
26
26
|
/**
|
|
27
27
|
* A coordinator that exhausted its step budget reports remaining work instead of continuing. That
|
|
28
28
|
* report is a continuation request in every respect except the marker, so it is treated as one.
|
|
@@ -34,12 +34,12 @@ declare const DEFAULT_TIMINGS: {
|
|
|
34
34
|
readonly cooldownMilliseconds: 60000;
|
|
35
35
|
/** Let the new summary's token state settle before a fresh turn can trigger overflow compaction. */
|
|
36
36
|
readonly settleMilliseconds: 1500;
|
|
37
|
-
/**
|
|
38
|
-
readonly scheduleMilliseconds:
|
|
37
|
+
/** Session idle starts rollover normally; this delayed path only recovers a missing idle event. */
|
|
38
|
+
readonly scheduleMilliseconds: 30000;
|
|
39
39
|
readonly scheduleAttempts: 2;
|
|
40
40
|
};
|
|
41
41
|
export type ContinuationTimings = typeof DEFAULT_TIMINGS;
|
|
42
|
-
export type ContinuationRejection = "identity-unavailable" | "child-session" | "agent-mismatch" | "capability-unavailable" | "continuation-disabled" | "limit-reached" | "pending-autocontinue";
|
|
42
|
+
export type ContinuationRejection = "identity-unavailable" | "child-session" | "agent-mismatch" | "capability-unavailable" | "continuation-disabled" | "limit-reached" | "pending-autocontinue" | "summarize-model-unavailable";
|
|
43
43
|
export interface ContinuationIdentity {
|
|
44
44
|
readonly agent?: string | undefined;
|
|
45
45
|
readonly parentID?: string | undefined;
|
|
@@ -82,7 +82,7 @@ export interface ContinuationClient {
|
|
|
82
82
|
query?: {
|
|
83
83
|
directory?: string;
|
|
84
84
|
};
|
|
85
|
-
body
|
|
85
|
+
body: {
|
|
86
86
|
providerID: string;
|
|
87
87
|
modelID: string;
|
|
88
88
|
};
|
|
@@ -125,7 +125,7 @@ export type ContinuationPolicySource = ContinuationPolicy | (() => ContinuationP
|
|
|
125
125
|
* without an agent field, or answers for a different directory, instead of failing silently.
|
|
126
126
|
*/
|
|
127
127
|
export type LocalIdentitySource = (sessionID: string) => ContinuationIdentity | undefined;
|
|
128
|
-
export type RolloverAbort = "identity-unavailable" | "child-session" | "summarize-unavailable" | "terminal-identity-rejected";
|
|
128
|
+
export type RolloverAbort = "identity-unavailable" | "child-session" | "summarize-unavailable" | "summarize-model-unavailable" | "retries-exhausted" | "terminal-identity-rejected";
|
|
129
129
|
export interface ContinuationToolContext {
|
|
130
130
|
readonly sessionID: string;
|
|
131
131
|
readonly agent?: string | undefined;
|
|
@@ -154,6 +154,11 @@ export interface ContinuationHooks {
|
|
|
154
154
|
}, output: {
|
|
155
155
|
enabled: boolean;
|
|
156
156
|
}): Promise<void>;
|
|
157
|
+
observeModel(sessionID: string, model: {
|
|
158
|
+
providerID: string;
|
|
159
|
+
modelID: string;
|
|
160
|
+
}): void;
|
|
161
|
+
blocksTool(sessionID: string): boolean;
|
|
157
162
|
sessionIdle(sessionID: string): Promise<void>;
|
|
158
163
|
forgetSession(sessionID: string): void;
|
|
159
164
|
}
|
|
@@ -22,7 +22,7 @@ export const ROLLOVER_MARKER = "<!-- SORTIE_COMPACT -->";
|
|
|
22
22
|
export const AUTO_CONTINUE_PREFIX = "SORTIE_AUTO_CONTINUE";
|
|
23
23
|
/** First line the rollover summary must emit, mirroring the batch target of three attempts. */
|
|
24
24
|
export const ROLLOVER_TOKEN = "SORTIE_ROLLOVER_COMPACTED";
|
|
25
|
-
export const DEFAULT_MAX_AUTO_CONTINUES =
|
|
25
|
+
export const DEFAULT_MAX_AUTO_CONTINUES = 2;
|
|
26
26
|
/**
|
|
27
27
|
* A coordinator that exhausted its step budget reports remaining work instead of continuing. That
|
|
28
28
|
* report is a continuation request in every respect except the marker, so it is treated as one.
|
|
@@ -35,8 +35,8 @@ const DEFAULT_TIMINGS = {
|
|
|
35
35
|
cooldownMilliseconds: 60_000,
|
|
36
36
|
/** Let the new summary's token state settle before a fresh turn can trigger overflow compaction. */
|
|
37
37
|
settleMilliseconds: 1_500,
|
|
38
|
-
/**
|
|
39
|
-
scheduleMilliseconds:
|
|
38
|
+
/** Session idle starts rollover normally; this delayed path only recovers a missing idle event. */
|
|
39
|
+
scheduleMilliseconds: 30_000,
|
|
40
40
|
scheduleAttempts: 2,
|
|
41
41
|
};
|
|
42
42
|
function nonEmpty(value) {
|
|
@@ -184,6 +184,7 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
184
184
|
attempts: 0,
|
|
185
185
|
pendingRollover: false,
|
|
186
186
|
active: false,
|
|
187
|
+
compactedRollover: false,
|
|
187
188
|
promptPending: false,
|
|
188
189
|
directUsed: false,
|
|
189
190
|
touched: Date.now(),
|
|
@@ -212,11 +213,30 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
212
213
|
return undefined;
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
|
-
function summarizeBody() {
|
|
216
|
+
function summarizeBody(state) {
|
|
216
217
|
const summarizeModel = policy().summarizeModel;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
return summarizeModel === undefined ? state.model : openCodeModel(summarizeModel.model);
|
|
219
|
+
}
|
|
220
|
+
function summarizeCallSucceeded(response) {
|
|
221
|
+
if (response === true)
|
|
222
|
+
return true;
|
|
223
|
+
if (response === null || typeof response !== "object" || !("data" in response))
|
|
224
|
+
return false;
|
|
225
|
+
return response.data === true;
|
|
226
|
+
}
|
|
227
|
+
function promptCallSucceeded(response) {
|
|
228
|
+
if (response === undefined || response === true)
|
|
229
|
+
return true;
|
|
230
|
+
if (response === null || typeof response !== "object")
|
|
231
|
+
return false;
|
|
232
|
+
const result = response;
|
|
233
|
+
if (result.error !== undefined)
|
|
234
|
+
return false;
|
|
235
|
+
if (result.response?.ok === false)
|
|
236
|
+
return false;
|
|
237
|
+
if (typeof result.response?.status === "number" && result.response.status >= 400)
|
|
238
|
+
return false;
|
|
239
|
+
return true;
|
|
220
240
|
}
|
|
221
241
|
async function runRollover(sessionID) {
|
|
222
242
|
const state = sessions.get(sessionID);
|
|
@@ -227,52 +247,65 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
227
247
|
warnRollover(sessionID, "summarize-unavailable");
|
|
228
248
|
return false;
|
|
229
249
|
}
|
|
230
|
-
|
|
231
|
-
? 0
|
|
232
|
-
: timings.cooldownMilliseconds - (Date.now() - state.lastRollover);
|
|
233
|
-
if (cooldownRemaining > 0) {
|
|
234
|
-
if (state.cooldownTimer === undefined) {
|
|
235
|
-
state.cooldownTimer = unrefTimer(setTimeout(() => {
|
|
236
|
-
const current = sessions.get(sessionID);
|
|
237
|
-
if (current !== undefined)
|
|
238
|
-
current.cooldownTimer = undefined;
|
|
239
|
-
void runRollover(sessionID);
|
|
240
|
-
}, cooldownRemaining));
|
|
241
|
-
}
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
// A child session must never compact or resume its parent's batch.
|
|
245
|
-
const identity = await readIdentity(sessionID);
|
|
246
|
-
if (identity === undefined || !nonEmpty(identity.agent)) {
|
|
247
|
-
warnRollover(sessionID, "identity-unavailable");
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
if (nonEmpty(identity.parentID)) {
|
|
251
|
-
warnRollover(sessionID, "child-session");
|
|
252
|
-
return false;
|
|
253
|
-
}
|
|
254
|
-
const continueReport = state.continueReport;
|
|
250
|
+
// Acquire the session lock before the first await so idle and fallback timers cannot overlap.
|
|
255
251
|
state.active = true;
|
|
256
|
-
|
|
257
|
-
let
|
|
252
|
+
let compactionStarted = false;
|
|
253
|
+
let compactionSucceeded = false;
|
|
258
254
|
try {
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
255
|
+
const cooldownRemaining = state.lastRollover === undefined || state.compactedRollover
|
|
256
|
+
? 0
|
|
257
|
+
: timings.cooldownMilliseconds - (Date.now() - state.lastRollover);
|
|
258
|
+
if (cooldownRemaining > 0) {
|
|
259
|
+
if (state.cooldownTimer === undefined) {
|
|
260
|
+
state.cooldownTimer = unrefTimer(setTimeout(() => {
|
|
261
|
+
const current = sessions.get(sessionID);
|
|
262
|
+
if (current !== undefined)
|
|
263
|
+
current.cooldownTimer = undefined;
|
|
264
|
+
void runRollover(sessionID);
|
|
265
|
+
}, cooldownRemaining));
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
// A child session must never compact or resume its parent's batch.
|
|
270
|
+
const identity = await readIdentity(sessionID);
|
|
271
|
+
if (identity === undefined || !nonEmpty(identity.agent)) {
|
|
272
|
+
warnRollover(sessionID, "identity-unavailable");
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
if (nonEmpty(identity.parentID)) {
|
|
276
|
+
warnRollover(sessionID, "child-session");
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
if (!state.compactedRollover) {
|
|
280
|
+
const body = summarizeBody(state);
|
|
281
|
+
if (body === undefined) {
|
|
282
|
+
warnRollover(sessionID, "summarize-model-unavailable");
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
state.promptPending = true;
|
|
286
|
+
compactionStarted = true;
|
|
287
|
+
const summarizedResponse = await summarize.call(client.session, {
|
|
288
|
+
path: { id: sessionID },
|
|
289
|
+
query: { directory },
|
|
290
|
+
body,
|
|
291
|
+
});
|
|
292
|
+
if (!summarizeCallSucceeded(summarizedResponse))
|
|
293
|
+
throw new Error("summarize request rejected");
|
|
294
|
+
state.compactedRollover = true;
|
|
295
|
+
compactionSucceeded = true;
|
|
296
|
+
state.lastRollover = Date.now();
|
|
297
|
+
}
|
|
298
|
+
const continueReport = state.continueReport;
|
|
299
|
+
if (continueReport === undefined) {
|
|
300
|
+
state.pendingRollover = false;
|
|
301
|
+
state.compactedRollover = false;
|
|
270
302
|
return true;
|
|
303
|
+
}
|
|
271
304
|
const resume = client?.session?.promptAsync;
|
|
272
305
|
if (resume === undefined)
|
|
273
|
-
|
|
274
|
-
await new Promise((settle) =>
|
|
275
|
-
await resume.call(client.session, {
|
|
306
|
+
throw new Error("resume capability unavailable");
|
|
307
|
+
await new Promise((settle) => setTimeout(settle, timings.settleMilliseconds));
|
|
308
|
+
const resumed = await resume.call(client.session, {
|
|
276
309
|
path: { id: sessionID },
|
|
277
310
|
query: { directory },
|
|
278
311
|
body: {
|
|
@@ -285,6 +318,11 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
285
318
|
}],
|
|
286
319
|
},
|
|
287
320
|
});
|
|
321
|
+
if (!promptCallSucceeded(resumed))
|
|
322
|
+
throw new Error("resume request rejected");
|
|
323
|
+
state.pendingRollover = false;
|
|
324
|
+
state.compactedRollover = false;
|
|
325
|
+
state.continueReport = undefined;
|
|
288
326
|
return true;
|
|
289
327
|
}
|
|
290
328
|
catch (error) {
|
|
@@ -295,7 +333,7 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
295
333
|
const current = sessions.get(sessionID);
|
|
296
334
|
if (current !== undefined) {
|
|
297
335
|
current.active = false;
|
|
298
|
-
if (!
|
|
336
|
+
if (compactionStarted && !compactionSucceeded)
|
|
299
337
|
current.promptPending = false;
|
|
300
338
|
}
|
|
301
339
|
}
|
|
@@ -304,14 +342,24 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
304
342
|
unrefTimer(setTimeout(async () => {
|
|
305
343
|
const completed = await runRollover(sessionID);
|
|
306
344
|
const state = sessions.get(sessionID);
|
|
345
|
+
if (!completed && (state?.cooldownTimer !== undefined || state?.active === true))
|
|
346
|
+
return;
|
|
307
347
|
if (!completed && state?.pendingRollover === true && attempt < timings.scheduleAttempts) {
|
|
308
348
|
scheduleRollover(sessionID, attempt + 1);
|
|
309
349
|
}
|
|
350
|
+
else if (!completed && state?.pendingRollover === true) {
|
|
351
|
+
state.pendingRollover = false;
|
|
352
|
+
state.compactedRollover = false;
|
|
353
|
+
state.promptPending = false;
|
|
354
|
+
state.continueReport = undefined;
|
|
355
|
+
warnRollover(sessionID, "retries-exhausted");
|
|
356
|
+
}
|
|
310
357
|
}, timings.scheduleMilliseconds * (attempt + 1)));
|
|
311
358
|
}
|
|
312
359
|
function queueRollover(sessionID, report, resume) {
|
|
313
360
|
const state = stateFor(sessionID);
|
|
314
361
|
state.pendingRollover = true;
|
|
362
|
+
state.compactedRollover = false;
|
|
315
363
|
state.latestReport = report;
|
|
316
364
|
state.continueReport = resume ? report : undefined;
|
|
317
365
|
if (resume)
|
|
@@ -370,6 +418,12 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
370
418
|
});
|
|
371
419
|
if (!resolution.compact)
|
|
372
420
|
return `SORTIE_CONTINUATION_REJECTED: ${resolution.reason}`;
|
|
421
|
+
if (client?.session?.summarize === undefined) {
|
|
422
|
+
return "SORTIE_CONTINUATION_REJECTED: capability-unavailable";
|
|
423
|
+
}
|
|
424
|
+
if (summarizeBody(state) === undefined) {
|
|
425
|
+
return "SORTIE_CONTINUATION_REJECTED: summarize-model-unavailable";
|
|
426
|
+
}
|
|
373
427
|
// The direct capability and the marker fallback are mutually exclusive within one turn.
|
|
374
428
|
state.directUsed = true;
|
|
375
429
|
queueRollover(context.sessionID, "Tool-requested Sortie rollover. Preserve task identity, both manifests, validation history, " +
|
|
@@ -446,9 +500,18 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
446
500
|
identity.agent !== policy().agent)
|
|
447
501
|
return;
|
|
448
502
|
}
|
|
449
|
-
if (
|
|
503
|
+
if (pending)
|
|
450
504
|
output.enabled = false;
|
|
451
505
|
},
|
|
506
|
+
observeModel(sessionID, model) {
|
|
507
|
+
if (!nonEmpty(model.providerID) || !nonEmpty(model.modelID))
|
|
508
|
+
return;
|
|
509
|
+
stateFor(sessionID).model = { providerID: model.providerID, modelID: model.modelID };
|
|
510
|
+
},
|
|
511
|
+
blocksTool(sessionID) {
|
|
512
|
+
const state = sessions.get(sessionID);
|
|
513
|
+
return state?.pendingRollover === true || state?.active === true || state?.promptPending === true;
|
|
514
|
+
},
|
|
452
515
|
async sessionIdle(sessionID) {
|
|
453
516
|
await runRollover(sessionID);
|
|
454
517
|
},
|
package/dist/plugin/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { RUNTIME_ASSET_VERSION } from "../asset-version.js";
|
|
|
5
5
|
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
|
-
import { DEFAULT_PLUGIN_OPTIONS,
|
|
8
|
+
import { DEFAULT_PLUGIN_OPTIONS, resolvePluginConfiguration, resolvePluginConfigurationSourcesWithGlobal, } from "./config.js";
|
|
9
9
|
import { CONTINUATION_CAPABILITY, createContinuationHooks, } from "./continuation.js";
|
|
10
10
|
import { WriteDeniedError, createProjectPaths, createWriteGate, describeUnclassifiedCommand, isKnownReadOnlyTool, normalizeCommand, resolveProjectRoot, safePath, } from "./gate.js";
|
|
11
11
|
import { createModelRoutingHook, } from "./model-routing-hook.js";
|
|
@@ -190,6 +190,22 @@ async function readOptionalProjectConfig(project) {
|
|
|
190
190
|
throw error;
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
|
+
async function readOptionalGlobalConfig() {
|
|
194
|
+
try {
|
|
195
|
+
const value = await readJson(join(configRoot(), "sortie-dogs.json"), INPUT_LIMITS.config);
|
|
196
|
+
if (resolvePluginConfiguration(value).kind === "invalid") {
|
|
197
|
+
console.warn("[sortie-dogs] global configuration ignored: invalid or unavailable");
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (isAbsentPathError(error))
|
|
204
|
+
return undefined;
|
|
205
|
+
console.warn("[sortie-dogs] global configuration ignored: invalid or unavailable");
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
193
209
|
function readEnvironmentConfig() {
|
|
194
210
|
const source = process.env[ENV_CONFIG];
|
|
195
211
|
if (source === undefined || source.length === 0)
|
|
@@ -411,11 +427,12 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
411
427
|
let loading;
|
|
412
428
|
let manifestAbsent = false;
|
|
413
429
|
let assetVersionReported = false;
|
|
430
|
+
const globalConfig = await readOptionalGlobalConfig();
|
|
414
431
|
// Project config read is required discovery for its opt-in; no reflection storage/version read
|
|
415
432
|
// occurs unless that resolved config enables reflection. It stays isolated from write-gate load.
|
|
416
433
|
try {
|
|
417
434
|
project = await createProjectPaths(resolveProjectRoot(input));
|
|
418
|
-
const probed =
|
|
435
|
+
const probed = resolvePluginConfigurationSourcesWithGlobal(globalConfig, await readOptionalProjectConfig(project), readEnvironmentConfig(), options);
|
|
419
436
|
if (probed.kind === "configured" && reflectionEnabled(probed.reflection)) {
|
|
420
437
|
reflectionVersion = await nearestPackageVersion();
|
|
421
438
|
reflectionConfiguration = probed.reflection;
|
|
@@ -463,7 +480,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
463
480
|
await reportAssetVersionSkew(project);
|
|
464
481
|
const projectConfig = await readOptionalProjectConfig(project);
|
|
465
482
|
const environmentConfig = readEnvironmentConfig();
|
|
466
|
-
const parsed =
|
|
483
|
+
const parsed = resolvePluginConfigurationSourcesWithGlobal(globalConfig, projectConfig, environmentConfig, options);
|
|
467
484
|
if (parsed.kind === "invalid")
|
|
468
485
|
throw new WriteDeniedError("manifest-unavailable", "<unknown>");
|
|
469
486
|
loaded = loadConfigured(parsed, input.worktree ?? project.root, input.client);
|
|
@@ -1357,6 +1374,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1357
1374
|
*/
|
|
1358
1375
|
await ensureLoaded();
|
|
1359
1376
|
await loaded?.modelRoutingHook?.(chatInput, output);
|
|
1377
|
+
if (coordinatorOrigin)
|
|
1378
|
+
continuation.observeModel(chatInput.sessionID, output.message.model);
|
|
1360
1379
|
},
|
|
1361
1380
|
...(reflectionStartup ? { "experimental.chat.system.transform": async (transformInput, transformOutput) => {
|
|
1362
1381
|
if (!(await beginReflection(transformInput.sessionID)))
|
|
@@ -1416,8 +1435,12 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1416
1435
|
await inspectSuccessfulRead(toolInput);
|
|
1417
1436
|
},
|
|
1418
1437
|
"tool.execute.before": async (toolInput, output) => {
|
|
1419
|
-
if (isCoordinatorSession(toolInput.sessionID))
|
|
1438
|
+
if (isCoordinatorSession(toolInput.sessionID)) {
|
|
1439
|
+
if (continuation.blocksTool(toolInput.sessionID)) {
|
|
1440
|
+
throw new Error("SORTIE_ROLLOVER_PENDING: stop this turn and wait for compaction");
|
|
1441
|
+
}
|
|
1420
1442
|
return;
|
|
1443
|
+
}
|
|
1421
1444
|
const status = activeSessionStatus(toolInput.sessionID);
|
|
1422
1445
|
if (status === "inactive")
|
|
1423
1446
|
return;
|