sortie-dogs 0.6.0 → 0.6.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 +7 -2
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/plugin/config.d.ts +1 -0
- package/dist/plugin/continuation.d.ts +12 -1
- package/dist/plugin/continuation.js +47 -3
- package/dist/plugin/index.d.ts +13 -0
- package/dist/plugin/index.js +116 -17
- package/dist/reflection/index.d.ts +1 -0
- package/dist/reflection/index.js +1 -0
- package/dist/reflection/policy.d.ts +1 -0
- package/dist/reflection/policy.js +83 -0
- package/dist/runtime-assets.d.ts +7 -7
- package/dist/runtime-assets.js +9 -92
- 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.6.
|
|
23
|
+
Release: [v0.6.1](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.6.1)
|
|
24
24
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
|
@@ -151,7 +151,8 @@ Optional settings in `.opencode/sortie-dogs.json`:
|
|
|
151
151
|
"continuation": { "enabled": true, "maxAutoContinues": 10 },
|
|
152
152
|
"reflection": {
|
|
153
153
|
"enabled": false,
|
|
154
|
-
"layers": { "run": true, "project": true, "global": false }
|
|
154
|
+
"layers": { "run": true, "project": true, "global": false },
|
|
155
|
+
"maxInjectedTokens": 500
|
|
155
156
|
}
|
|
156
157
|
}
|
|
157
158
|
```
|
|
@@ -201,6 +202,10 @@ global file for durable global settings.
|
|
|
201
202
|
to enabled after opt-in; the cross-project global storage layer remains
|
|
202
203
|
disabled unless explicitly enabled. Child and non-coordinator sessions fail
|
|
203
204
|
closed, and `SORTIE_REFLECTION=0` is an immediate kill switch. The coordinator
|
|
205
|
+
injects the governing `REFLECTION_POLICY` only while reflection is enabled.
|
|
206
|
+
`maxInjectedTokens` budgets the dynamic `SORTIE_PROCESS_REFLECTIONS` heading
|
|
207
|
+
and persisted entry lines; the policy is outside that entry budget.
|
|
208
|
+
The coordinator
|
|
204
209
|
evaluates it only after a resolved blocker/review defect and at a terminal
|
|
205
210
|
unit, with a maximum of three records per run; routine bugs and external
|
|
206
211
|
failures are never journaled.
|
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.50-observability-reflection-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.50-observability-reflection-v1";
|
package/dist/plugin/config.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface ReflectionConfiguration {
|
|
|
7
7
|
readonly global: boolean;
|
|
8
8
|
};
|
|
9
9
|
readonly maxInjectedEntries: number;
|
|
10
|
+
/** Token budget for the dynamic SORTIE_PROCESS_REFLECTIONS heading and persisted entry lines; excludes the governing REFLECTION_POLICY. */
|
|
10
11
|
readonly maxInjectedTokens: number;
|
|
11
12
|
}
|
|
12
13
|
export type ReflectionPolicyInput = Partial<Omit<ReflectionConfiguration, "layers">> & {
|
|
@@ -170,10 +170,21 @@ export interface ContinuationHooks {
|
|
|
170
170
|
sessionIdle(sessionID: string): Promise<void>;
|
|
171
171
|
forgetSession(sessionID: string): void;
|
|
172
172
|
}
|
|
173
|
+
export type ContinuationTransitionType = "continuation.queued" | "continuation.compacted" | "continuation.resumed" | "continuation.not_required";
|
|
174
|
+
export type ContinuationTransitionReason = "continuation-requested" | "compaction-only" | "summarize-accepted" | "prompt-accepted" | "terminal-checkpoint";
|
|
175
|
+
export interface ContinuationTransition {
|
|
176
|
+
readonly type: ContinuationTransitionType;
|
|
177
|
+
readonly sessionID: string;
|
|
178
|
+
readonly epoch: number;
|
|
179
|
+
readonly reason: ContinuationTransitionReason;
|
|
180
|
+
readonly attempts: number;
|
|
181
|
+
readonly resumeAttempts: number;
|
|
182
|
+
}
|
|
183
|
+
export type ContinuationTransitionObserver = (transition: ContinuationTransition) => void;
|
|
173
184
|
/**
|
|
174
185
|
* Build the continuation runtime. Every hook fails closed: an unreadable session, an absent client,
|
|
175
186
|
* or a rejected identity leaves the coordinator exactly where it was, with manual continuation
|
|
176
187
|
* still available to the user.
|
|
177
188
|
*/
|
|
178
|
-
export declare function createContinuationHooks(client: ContinuationClient | undefined, directory: string, policySource: ContinuationPolicySource, timings?: ContinuationTimings, localIdentity?: LocalIdentitySource): ContinuationHooks;
|
|
189
|
+
export declare function createContinuationHooks(client: ContinuationClient | undefined, directory: string, policySource: ContinuationPolicySource, timings?: ContinuationTimings, localIdentity?: LocalIdentitySource, transitionObserver?: ContinuationTransitionObserver): ContinuationHooks;
|
|
179
190
|
export {};
|
|
@@ -214,9 +214,38 @@ function sessionInfo(response) {
|
|
|
214
214
|
* or a rejected identity leaves the coordinator exactly where it was, with manual continuation
|
|
215
215
|
* still available to the user.
|
|
216
216
|
*/
|
|
217
|
-
export function createContinuationHooks(client, directory, policySource, timings = DEFAULT_TIMINGS, localIdentity) {
|
|
217
|
+
export function createContinuationHooks(client, directory, policySource, timings = DEFAULT_TIMINGS, localIdentity, transitionObserver) {
|
|
218
218
|
const sessions = new Map();
|
|
219
219
|
const warned = new Set();
|
|
220
|
+
function observeTransition(type, sessionID, state, reason) {
|
|
221
|
+
try {
|
|
222
|
+
transitionObserver?.({
|
|
223
|
+
type,
|
|
224
|
+
sessionID,
|
|
225
|
+
epoch: state.rolloverEpoch,
|
|
226
|
+
reason,
|
|
227
|
+
attempts: state.attempts,
|
|
228
|
+
resumeAttempts: state.resumeAttempts,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Lifecycle telemetry is best effort and must never affect continuation.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function observeResumed(sessionID, state, epoch) {
|
|
236
|
+
if (state.compactedTransitionEpoch !== epoch || state.resumedTransitionEpoch === epoch)
|
|
237
|
+
return;
|
|
238
|
+
state.resumedTransitionEpoch = epoch;
|
|
239
|
+
observeTransition("continuation.resumed", sessionID, state, "prompt-accepted");
|
|
240
|
+
}
|
|
241
|
+
function observeCompacted(sessionID, state, epoch) {
|
|
242
|
+
if (state.compactedTransitionEpoch === epoch)
|
|
243
|
+
return;
|
|
244
|
+
state.compactedTransitionEpoch = epoch;
|
|
245
|
+
observeTransition("continuation.compacted", sessionID, state, "summarize-accepted");
|
|
246
|
+
if (state.resumeIssuedEpoch === epoch)
|
|
247
|
+
observeResumed(sessionID, state, epoch);
|
|
248
|
+
}
|
|
220
249
|
/**
|
|
221
250
|
* A rollover that cannot start is otherwise indistinguishable from a coordinator that never asked
|
|
222
251
|
* for one, so every abort names its own reason exactly once per session.
|
|
@@ -532,6 +561,7 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
532
561
|
state.recoverySummaryValidated = false;
|
|
533
562
|
// The accepted prompt now belongs to the host loop, not this rollover request.
|
|
534
563
|
state.active = false;
|
|
564
|
+
observeResumed(sessionID, state, epoch);
|
|
535
565
|
return true;
|
|
536
566
|
}
|
|
537
567
|
catch (error) {
|
|
@@ -608,6 +638,7 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
608
638
|
state.promptPending = false;
|
|
609
639
|
state.compactedRollover = true;
|
|
610
640
|
state.lastRollover = Date.now();
|
|
641
|
+
observeCompacted(sessionID, state, operationEpoch);
|
|
611
642
|
}
|
|
612
643
|
if (state.resumeIssuingEpoch === operationEpoch ||
|
|
613
644
|
state.resumeIssuedEpoch === operationEpoch) {
|
|
@@ -694,6 +725,7 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
694
725
|
if (resume && countAttempt)
|
|
695
726
|
state.attempts += 1;
|
|
696
727
|
const epoch = state.rolloverEpoch;
|
|
728
|
+
observeTransition("continuation.queued", sessionID, state, resume ? "continuation-requested" : "compaction-only");
|
|
697
729
|
// session.idle can be lost when a one-shot CLI host exits. Keep this zero-delay timer referenced
|
|
698
730
|
// so the rollover reaches the host after the current plugin hook returns but before process exit.
|
|
699
731
|
setTimeout(async () => {
|
|
@@ -835,8 +867,11 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
835
867
|
* One-shot CLI hosts can exit as soon as the compaction assistant finishes, before the
|
|
836
868
|
* compacted event or summarize response. Its text-complete hook is the last awaited boundary
|
|
837
869
|
* where the summary message already exists and a resume prompt can still join the same loop.
|
|
838
|
-
|
|
870
|
+
*/
|
|
839
871
|
if (ownedCompactionSummary || trimmed.startsWith(ROLLOVER_TOKEN)) {
|
|
872
|
+
if (ownedCompactionSummary) {
|
|
873
|
+
observeCompacted(input.sessionID, state, state.rolloverEpoch);
|
|
874
|
+
}
|
|
840
875
|
await arbitrateResume(input.sessionID, state);
|
|
841
876
|
if (state.resumeIssuedEpoch === state.rolloverEpoch)
|
|
842
877
|
return;
|
|
@@ -850,6 +885,12 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
850
885
|
clearTimer(state.stepRecoveryTimer);
|
|
851
886
|
state.stepRecoveryTimer = undefined;
|
|
852
887
|
resetRecoveryStall(state);
|
|
888
|
+
if (state.notRequiredRevision !== state.turnRevision &&
|
|
889
|
+
!output.text.includes(ROLLOVER_MARKER) &&
|
|
890
|
+
!output.text.includes(CONTINUATION_MARKER)) {
|
|
891
|
+
state.notRequiredRevision = state.turnRevision;
|
|
892
|
+
observeTransition("continuation.not_required", input.sessionID, state, "terminal-checkpoint");
|
|
893
|
+
}
|
|
853
894
|
}
|
|
854
895
|
if (batchCheckpointNeedsContinuation(state.latestCoordinatorReport)) {
|
|
855
896
|
if (input.allowCheckpointContinuation === false) {
|
|
@@ -951,8 +992,11 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
951
992
|
},
|
|
952
993
|
async sessionCompacted(sessionID) {
|
|
953
994
|
const state = sessions.get(sessionID);
|
|
954
|
-
if (state
|
|
995
|
+
if (state?.pendingRollover === true &&
|
|
996
|
+
(state.active || state.promptPending || state.compactingEpoch === state.rolloverEpoch)) {
|
|
997
|
+
observeCompacted(sessionID, state, state.rolloverEpoch);
|
|
955
998
|
await arbitrateResume(sessionID, state);
|
|
999
|
+
}
|
|
956
1000
|
},
|
|
957
1001
|
async compactionAutoContinue(input, output) {
|
|
958
1002
|
const state = sessions.get(input.sessionID);
|
package/dist/plugin/index.d.ts
CHANGED
|
@@ -10,6 +10,19 @@ export interface OpenCodePluginInput {
|
|
|
10
10
|
worktree?: string;
|
|
11
11
|
/** The host SDK client. Absent in hosts that construct the plugin without one. */
|
|
12
12
|
client?: SessionMessageReader & RunMetricsClient & ContinuationClient & OpenCodeModelAvailabilityClient & {
|
|
13
|
+
app?: {
|
|
14
|
+
log?: (request: {
|
|
15
|
+
body: {
|
|
16
|
+
service: string;
|
|
17
|
+
level: "debug" | "info" | "error" | "warn";
|
|
18
|
+
message: string;
|
|
19
|
+
extra?: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
query?: {
|
|
22
|
+
directory?: string;
|
|
23
|
+
};
|
|
24
|
+
}) => unknown;
|
|
25
|
+
};
|
|
13
26
|
tui?: {
|
|
14
27
|
showToast?: (request: {
|
|
15
28
|
body: {
|
package/dist/plugin/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import { WriteDeniedError, canonicalManifestReadScopes, canonicalManifestWriteSc
|
|
|
17
17
|
import { BACKLOG_DRAIN_CAPABILITY, FastLaneController } from "./fast-lane.js";
|
|
18
18
|
import { createModelRoutingHook, } from "./model-routing-hook.js";
|
|
19
19
|
import { createTaskResultRepairHook, lastAssistantText, markConsultationFallbackRetry, taskChildSessionID, } from "./task-result-repair.js";
|
|
20
|
-
import { configRoot, nearestPackageVersion, reflectionEnabled, ReflectionError, ReflectionStore } from "../reflection/index.js";
|
|
20
|
+
import { configRoot, nearestPackageVersion, REFLECTION_POLICY, reflectionEnabled, ReflectionError, ReflectionStore } from "../reflection/index.js";
|
|
21
21
|
import { collectRunMetrics, insertRunMetrics, isDoneTerminalText } from "./run-metrics.js";
|
|
22
22
|
const INPUT_LIMITS = { config: 64 * 1024, manifest: 512 * 1024, handoff: 2 * 1024 * 1024, parallel: 512 * 1024 };
|
|
23
23
|
const INSPECTION_CACHE = { maximum: 256, ttlMilliseconds: 30 * 60 * 1000 };
|
|
@@ -803,6 +803,83 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
803
803
|
let bootstrapCompleted = false;
|
|
804
804
|
let assetVersionReported = false;
|
|
805
805
|
const globalConfig = await readOptionalGlobalConfig();
|
|
806
|
+
const sessionOperationMetrics = new Map();
|
|
807
|
+
function appLogInfo(message, sessionID, extra) {
|
|
808
|
+
const app = input.client?.app;
|
|
809
|
+
const log = app?.log;
|
|
810
|
+
if (log === undefined)
|
|
811
|
+
return;
|
|
812
|
+
try {
|
|
813
|
+
const result = log.call(app, {
|
|
814
|
+
body: {
|
|
815
|
+
service: "sortie-dogs",
|
|
816
|
+
level: "info",
|
|
817
|
+
message,
|
|
818
|
+
extra: { sessionID: sessionID.slice(0, 128), ...extra },
|
|
819
|
+
},
|
|
820
|
+
query: { directory: input.directory },
|
|
821
|
+
});
|
|
822
|
+
void Promise.resolve(result).catch(() => undefined);
|
|
823
|
+
}
|
|
824
|
+
catch {
|
|
825
|
+
// Host lifecycle telemetry is best effort.
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function pruneSessionOperationMetrics(now, reserveSlot = false) {
|
|
829
|
+
for (const [sessionID, metrics] of sessionOperationMetrics) {
|
|
830
|
+
if (metrics.touched + ACTIVE_SESSION_CACHE.ttlMilliseconds <= now) {
|
|
831
|
+
sessionOperationMetrics.delete(sessionID);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
const limit = ACTIVE_SESSION_CACHE.maximum - (reserveSlot ? 1 : 0);
|
|
835
|
+
while (sessionOperationMetrics.size > limit) {
|
|
836
|
+
sessionOperationMetrics.delete(sessionOperationMetrics.keys().next().value);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function operationMetricsFor(sessionID) {
|
|
840
|
+
const now = Date.now();
|
|
841
|
+
pruneSessionOperationMetrics(now);
|
|
842
|
+
const existing = sessionOperationMetrics.get(sessionID);
|
|
843
|
+
if (existing !== undefined) {
|
|
844
|
+
existing.touched = now;
|
|
845
|
+
sessionOperationMetrics.delete(sessionID);
|
|
846
|
+
sessionOperationMetrics.set(sessionID, existing);
|
|
847
|
+
return existing;
|
|
848
|
+
}
|
|
849
|
+
pruneSessionOperationMetrics(now, true);
|
|
850
|
+
const created = {
|
|
851
|
+
touched: now,
|
|
852
|
+
operations: {
|
|
853
|
+
hostSessionIdentity: { count: 0, elapsedMilliseconds: 0 },
|
|
854
|
+
bootstrapControlState: { count: 0, elapsedMilliseconds: 0 },
|
|
855
|
+
collectRunMetrics: { count: 0, elapsedMilliseconds: 0 },
|
|
856
|
+
},
|
|
857
|
+
};
|
|
858
|
+
sessionOperationMetrics.set(sessionID, created);
|
|
859
|
+
return created;
|
|
860
|
+
}
|
|
861
|
+
async function measureSessionOperation(sessionID, operation, run) {
|
|
862
|
+
const started = performance.now();
|
|
863
|
+
try {
|
|
864
|
+
return await run();
|
|
865
|
+
}
|
|
866
|
+
finally {
|
|
867
|
+
const measurement = operationMetricsFor(sessionID).operations[operation];
|
|
868
|
+
measurement.count += 1;
|
|
869
|
+
measurement.elapsedMilliseconds += Math.max(0, performance.now() - started);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function operationMetricsSnapshot(sessionID) {
|
|
873
|
+
const operations = sessionOperationMetrics.get(sessionID)?.operations;
|
|
874
|
+
return {
|
|
875
|
+
hostSessionIdentityCount: operations?.hostSessionIdentity.count ?? 0,
|
|
876
|
+
hostSessionIdentityElapsedMilliseconds: Math.round(operations?.hostSessionIdentity.elapsedMilliseconds ?? 0),
|
|
877
|
+
bootstrapControlStateCount: operations?.bootstrapControlState.count ?? 0,
|
|
878
|
+
bootstrapControlStateElapsedMilliseconds: Math.round(operations?.bootstrapControlState.elapsedMilliseconds ?? 0),
|
|
879
|
+
collectRunMetricsCount: operations?.collectRunMetrics.count ?? 0,
|
|
880
|
+
collectRunMetricsElapsedMilliseconds: Math.round(operations?.collectRunMetrics.elapsedMilliseconds ?? 0),
|
|
881
|
+
};
|
|
882
|
+
}
|
|
806
883
|
// Project config read is required discovery for its opt-in; no reflection storage/version read
|
|
807
884
|
// occurs unless that resolved config enables reflection. It stays isolated from write-gate load.
|
|
808
885
|
try {
|
|
@@ -844,7 +921,12 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
844
921
|
*/
|
|
845
922
|
(sessionID) => isCoordinatorSession(sessionID)
|
|
846
923
|
? { agent: COORDINATOR_AGENT, parentID: undefined }
|
|
847
|
-
: undefined)
|
|
924
|
+
: undefined, (transition) => appLogInfo(transition.type, transition.sessionID, {
|
|
925
|
+
epoch: transition.epoch,
|
|
926
|
+
reason: transition.reason,
|
|
927
|
+
attempts: transition.attempts,
|
|
928
|
+
resumeAttempts: transition.resumeAttempts,
|
|
929
|
+
}));
|
|
848
930
|
const completedCoordinatorMessages = new Set();
|
|
849
931
|
const completedCoordinatorParts = new Set();
|
|
850
932
|
async function ensureLoaded() {
|
|
@@ -2361,6 +2443,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2361
2443
|
}
|
|
2362
2444
|
function expireSession(sessionID) {
|
|
2363
2445
|
activeSessions.delete(sessionID);
|
|
2446
|
+
sessionOperationMetrics.delete(sessionID);
|
|
2364
2447
|
abandonSessionLease(sessionID);
|
|
2365
2448
|
sessionAuthorizations.delete(sessionID);
|
|
2366
2449
|
if (!childHasInFlightParentTask(sessionID)) {
|
|
@@ -2381,6 +2464,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2381
2464
|
}
|
|
2382
2465
|
function evictSession(sessionID) {
|
|
2383
2466
|
activeSessions.delete(sessionID);
|
|
2467
|
+
sessionOperationMetrics.delete(sessionID);
|
|
2384
2468
|
abandonSessionLease(sessionID);
|
|
2385
2469
|
sessionAuthorizations.delete(sessionID);
|
|
2386
2470
|
bindingPins.delete(sessionID);
|
|
@@ -2479,10 +2563,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2479
2563
|
if (get === undefined)
|
|
2480
2564
|
return undefined;
|
|
2481
2565
|
try {
|
|
2482
|
-
const response = await get.call(input.client.session, {
|
|
2566
|
+
const response = await measureSessionOperation(sessionID, "hostSessionIdentity", () => get.call(input.client.session, {
|
|
2483
2567
|
path: { id: sessionID },
|
|
2484
2568
|
query: { directory: input.directory },
|
|
2485
|
-
});
|
|
2569
|
+
}));
|
|
2486
2570
|
const payload = isRecord(response) && "data" in response ? response.data : response;
|
|
2487
2571
|
if (!isRecord(payload))
|
|
2488
2572
|
return undefined;
|
|
@@ -2906,9 +2990,13 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2906
2990
|
}
|
|
2907
2991
|
if ((isCoordinatorSession(textInput.sessionID) || await recoverCoordinatorRoot(textInput.sessionID)) &&
|
|
2908
2992
|
isDoneTerminalText(textOutput.text)) {
|
|
2909
|
-
const metrics = await collectRunMetrics(input.client, textInput.sessionID, input.directory).catch(() => undefined);
|
|
2993
|
+
const metrics = await measureSessionOperation(textInput.sessionID, "collectRunMetrics", () => collectRunMetrics(input.client, textInput.sessionID, input.directory).catch(() => undefined));
|
|
2910
2994
|
if (metrics !== undefined)
|
|
2911
2995
|
textOutput.text = insertRunMetrics(textOutput.text, metrics);
|
|
2996
|
+
appLogInfo("run-metrics.snapshot", textInput.sessionID, {
|
|
2997
|
+
available: metrics !== undefined,
|
|
2998
|
+
...operationMetricsSnapshot(textInput.sessionID),
|
|
2999
|
+
});
|
|
2912
3000
|
}
|
|
2913
3001
|
await completeContinuationText(textInput.sessionID, textOutput.text, false);
|
|
2914
3002
|
},
|
|
@@ -3057,22 +3145,33 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
3057
3145
|
}
|
|
3058
3146
|
}
|
|
3059
3147
|
}
|
|
3148
|
+
const heading = "SORTIE_PROCESS_REFLECTIONS";
|
|
3149
|
+
const prefix = `${REFLECTION_POLICY}\n\n${heading}`;
|
|
3150
|
+
if (transformOutput.system !== undefined) {
|
|
3151
|
+
const retained = transformOutput.system.filter((item) => item !== REFLECTION_POLICY && !item.startsWith(`${prefix}\n`));
|
|
3152
|
+
if (retained.length !== transformOutput.system.length)
|
|
3153
|
+
transformOutput.system = retained;
|
|
3154
|
+
}
|
|
3060
3155
|
if (!reflectionStartup || !(await beginReflection(transformInput.sessionID)))
|
|
3061
3156
|
return;
|
|
3062
3157
|
const config = reflectionConfiguration;
|
|
3063
3158
|
try {
|
|
3064
3159
|
if (!config)
|
|
3065
3160
|
return;
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3161
|
+
let element = REFLECTION_POLICY;
|
|
3162
|
+
try {
|
|
3163
|
+
const buckets = ["run", "project", "global"]
|
|
3164
|
+
.filter((layer) => config.layers[layer])
|
|
3165
|
+
.map((layer) => ({ layer, ...(layer === "global" ? {} : { run: transformInput.sessionID }) }));
|
|
3166
|
+
// Historical persisted configs budget the dynamic heading and entry payload, not policy.
|
|
3167
|
+
const entryBudget = Math.max(0, config.maxInjectedTokens - Buffer.byteLength(`${heading}\n`, "utf8"));
|
|
3168
|
+
const text = await reflectionStore.injectBuckets(buckets, config.maxInjectedEntries, entryBudget, reflectionVersion);
|
|
3169
|
+
if (text)
|
|
3170
|
+
element = `${prefix}\n${text}`;
|
|
3171
|
+
}
|
|
3172
|
+
catch { /* persisted entries are best effort; the active policy still applies */ }
|
|
3173
|
+
transformOutput.system = [...(transformOutput.system ?? []), element];
|
|
3174
|
+
}
|
|
3076
3175
|
finally {
|
|
3077
3176
|
endReflection(transformInput.sessionID);
|
|
3078
3177
|
}
|
|
@@ -3227,7 +3326,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
3227
3326
|
const bootstrap = bootstrapRequired && !exactCoordinatorDirectOperation &&
|
|
3228
3327
|
!coordinatorCapability && !sessionGateCapability &&
|
|
3229
3328
|
!sessionAuthorizations.has(toolInput.sessionID) && (coordinatorRoot || coordinatorRoots.size > 0)
|
|
3230
|
-
? await bootstrapControlState
|
|
3329
|
+
? await measureSessionOperation(toolInput.sessionID, "bootstrapControlState", bootstrapControlState)
|
|
3231
3330
|
: undefined;
|
|
3232
3331
|
if (coordinatorRoot && bootstrapRequired && !exactCoordinatorDirectOperation &&
|
|
3233
3332
|
!coordinatorCapability && !sessionGateCapability) {
|
|
@@ -3637,7 +3736,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
3637
3736
|
if (event.type === "session.compacted")
|
|
3638
3737
|
await continuation.sessionCompacted(eventSessionID);
|
|
3639
3738
|
if (event.type === "session.idle" && isCoordinatorSession(eventSessionID)) {
|
|
3640
|
-
const bootstrap = await bootstrapControlState
|
|
3739
|
+
const bootstrap = await measureSessionOperation(eventSessionID, "bootstrapControlState", bootstrapControlState);
|
|
3641
3740
|
if (bootstrapRequired && bootstrap?.usable === true && bootstrap.missing.length > 0) {
|
|
3642
3741
|
if (!bootstrapIdleWarnings.has(eventSessionID)) {
|
|
3643
3742
|
bootstrapIdleWarnings.add(eventSessionID);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { DEFAULT_REFLECTION, configRoot, nearestPackageVersion, projectKey, reflectionEnabled } from "./config.js";
|
|
2
|
+
export { REFLECTION_POLICY } from "./policy.js";
|
|
2
3
|
export { ReflectionStore, ReflectionError, estimateInjectionTokens, normalizeField } from "./store.js";
|
|
3
4
|
export type { ReflectionLayer } from "./config.js";
|
package/dist/reflection/index.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const REFLECTION_POLICY = "## Bounded process reflection\n\nReflection is an opt-in prevention checkpoint, not routine journaling. If the\nsortie_reflection capability is unavailable, continue without it and never block the task. When\navailable, record a user correction immediately after acknowledging it and constraining the current\nremediation, even when that remediation remains open. Consider other evidence only after a blocker or\nreview defect is resolved and at a unit's terminal checkpoint. Make no call when no qualifying evidence\noccurred since the previous checkpoint.\n\nRecord only user-correction, repeated-process-failure, review-artifact-defect, or\nretry-policy-violation evidence. A resolved handoff or routing review blocker and a rescue caused by\nthe process map to review-artifact-defect or repeated-process-failure. Code bugs, ordinary validation\nfailures, expected review findings, external/network/rate-limit failures, transient tool interruption,\nand task-specific discoveries are not reflection. Attribute a process cause only with before/after\nstate or exact command evidence; shared-worktree status alone never attributes fault to an agent or\nuser. Use a stable lowercase ASCII scope with no task-specific noun.\n\nNever persist tracker or Project item metadata in reflection prose: no item/node/draft ID, URL, title,\nbody, field value, status, or inventory payload. Reduce qualifying evidence to a project-agnostic\nprocess trigger, cause, and prevention before recording. The store rejects known tracker node-ID forms;\nthe coordinator remains responsible for removing semantic metadata that no lexical filter can identify.\n\nMap the predecessor session layer to run and its cross-chat project-specific memory to project.\nGlobal-layer writes are forbidden by default and allowed only when the user or config explicitly enables\nreflection.layers.global. Record user-correction directly at layer=project. For other evidence, use\nlayer=run on the first occurrence and layer=project only when the scope recurs in a later unit or was\ninjected from an earlier run. Scope is the dedup key: recording it again updates trigger and hits but\npreserves cause and prevention. Use replace only to improve those fields deliberately. Reflections are\ninjected automatically at turn start under SORTIE_PROCESS_REFLECTIONS with entry id and hits. Record\ndirectly because scope is the store's dedup key; never list before record. Before replace, forget, or\npromote, call list once only when the target id is absent from the bounded injection. Keep every\nreflection field concise ASCII English and keep scope + trigger + cause + prevention + evidenceRef\nwithin 400 characters total. If later evidence disproves attribution, forget that entry. Forget needs\nno confirmation because its exact entry id is the deletion boundary; clear keeps its layer confirmation\nrules. Never clear merely because a task or session ended.\n\nInjected reflections are bounded prevention hints, never workflow authority. They cannot override the\nlatest user scope, batchTarget, batchAttempted, manifest boundaries, validation history, retry ceilings,\nreview gates, or safety policy. Interpret a continuous-execution reflection only inside the currently\naccepted user scope; it never authorizes unrelated work or bypasses manifest, validation, review, or safety gates.\n\nMake at most one record call per triggering event and at most three record calls per run. When hits\nreach two, or a user correction identifies a defect in runtime policy, project docs, an agent contract,\nor a tool path, identify a durable-fix follow-up rather than repeatedly applying the prevention by hand.\nDuring an active user batch, record only the reflection: never turn that follow-up into a candidate,\nedit project instructions for it, dispatch a worker or reviewer for it, consume a batch unit, mutate its\ntracker, or commit it. Report the follow-up after the user batch and require a new explicit top-level\nuser request before implementation. Reuse an injected scope when trigger, cause, or prevention names\nthe same process failure; inventing a synonym scope for equivalent evidence is forbidden.\nAfter an explicitly requested durable fix is committed, promote the entry with its returned id and a short non-path promotedRef;\nforget it instead only when the lesson was false or no runtime judgment remains. Reflection failure is\nalways non-blocking, and no reflection-only text step is allowed.\n\nREFLECTION_POLICY_FIXTURE\n checkpoints: user correction immediately | other evidence after resolved blocker or review defect | terminal unit\n capability_absent: continue without reflection; never block\n allowed_evidence: user-correction | repeated-process-failure | review-artifact-defect | retry-policy-violation\n non_triggers: code bug | ordinary validation failure | expected review finding | external or transient failure | task discovery\n attribution: before/after state or exact command evidence required; shared worktree status alone is insufficient\n tracker_privacy: no item/node/draft ID | URL | title | body | field value | status | inventory payload\n user_correction_layer: project immediately\n first_process_failure_layer: run\n project_layer: same stable scope recurred in a later unit or was injected from an earlier run\n global_layer: forbidden by default; allowed only when user or config explicitly enables reflection.layers.global\n scope: stable lowercase ASCII process key; no task-specific noun\n dedup: same scope updates trigger and hits; equivalent evidence reuses the injected scope; synonym scopes forbidden\n call_limit: one record per triggering event; three record calls per run\n duplicate_scope: same event or same layer in one unit -> no call\n injected_project_recurrence: record project once to increment hits\n field_budget: concise ASCII English; scope + trigger + cause + prevention + evidenceRef <=400 characters total\n scope_format: lowercase kebab-case [a-z0-9-]+; underscores forbidden\n list: never before record; once before replace | forget | promote only when target id is absent from bounded injection\n call: sortie_reflection { action: record, layer: <run|project|global>, scope: <scope>, trigger: <event>, cause: <verified process cause>, prevention: <one reusable imperative>, evidence: <allowed enum>, evidenceRef: <short non-path reference> }\n correction: improved cause or prevention -> replace; disproved attribution -> forget\n forget_confirmation: none; exact entry id is the deletion boundary\n durable_fix: hits>=2 or policy-related user correction -> report follow-up after active batch; new explicit top-level request required\n active_batch_quarantine: no process-only candidate | instruction edit | Task | review | batch unit | tracker mutation | commit\n promotion: durable fix committed -> promote with returned id and short non-path reference; false or fully obsolete lesson -> forget\n read: automatic injection with id and hits under SORTIE_PROCESS_REFLECTIONS at turn start\n precedence: prevention hint only; never overrides user scope | batch counters | manifests | validation history | retry ceilings | review | safety\n continuous_execution: continue inside accepted user scope until complete | user decision | proven blocker | no progress\n extra_step: reflection-only text or tool step forbidden\nEND_REFLECTION_POLICY_FIXTURE";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export const REFLECTION_POLICY = `## Bounded process reflection
|
|
2
|
+
|
|
3
|
+
Reflection is an opt-in prevention checkpoint, not routine journaling. If the
|
|
4
|
+
sortie_reflection capability is unavailable, continue without it and never block the task. When
|
|
5
|
+
available, record a user correction immediately after acknowledging it and constraining the current
|
|
6
|
+
remediation, even when that remediation remains open. Consider other evidence only after a blocker or
|
|
7
|
+
review defect is resolved and at a unit's terminal checkpoint. Make no call when no qualifying evidence
|
|
8
|
+
occurred since the previous checkpoint.
|
|
9
|
+
|
|
10
|
+
Record only user-correction, repeated-process-failure, review-artifact-defect, or
|
|
11
|
+
retry-policy-violation evidence. A resolved handoff or routing review blocker and a rescue caused by
|
|
12
|
+
the process map to review-artifact-defect or repeated-process-failure. Code bugs, ordinary validation
|
|
13
|
+
failures, expected review findings, external/network/rate-limit failures, transient tool interruption,
|
|
14
|
+
and task-specific discoveries are not reflection. Attribute a process cause only with before/after
|
|
15
|
+
state or exact command evidence; shared-worktree status alone never attributes fault to an agent or
|
|
16
|
+
user. Use a stable lowercase ASCII scope with no task-specific noun.
|
|
17
|
+
|
|
18
|
+
Never persist tracker or Project item metadata in reflection prose: no item/node/draft ID, URL, title,
|
|
19
|
+
body, field value, status, or inventory payload. Reduce qualifying evidence to a project-agnostic
|
|
20
|
+
process trigger, cause, and prevention before recording. The store rejects known tracker node-ID forms;
|
|
21
|
+
the coordinator remains responsible for removing semantic metadata that no lexical filter can identify.
|
|
22
|
+
|
|
23
|
+
Map the predecessor session layer to run and its cross-chat project-specific memory to project.
|
|
24
|
+
Global-layer writes are forbidden by default and allowed only when the user or config explicitly enables
|
|
25
|
+
reflection.layers.global. Record user-correction directly at layer=project. For other evidence, use
|
|
26
|
+
layer=run on the first occurrence and layer=project only when the scope recurs in a later unit or was
|
|
27
|
+
injected from an earlier run. Scope is the dedup key: recording it again updates trigger and hits but
|
|
28
|
+
preserves cause and prevention. Use replace only to improve those fields deliberately. Reflections are
|
|
29
|
+
injected automatically at turn start under SORTIE_PROCESS_REFLECTIONS with entry id and hits. Record
|
|
30
|
+
directly because scope is the store's dedup key; never list before record. Before replace, forget, or
|
|
31
|
+
promote, call list once only when the target id is absent from the bounded injection. Keep every
|
|
32
|
+
reflection field concise ASCII English and keep scope + trigger + cause + prevention + evidenceRef
|
|
33
|
+
within 400 characters total. If later evidence disproves attribution, forget that entry. Forget needs
|
|
34
|
+
no confirmation because its exact entry id is the deletion boundary; clear keeps its layer confirmation
|
|
35
|
+
rules. Never clear merely because a task or session ended.
|
|
36
|
+
|
|
37
|
+
Injected reflections are bounded prevention hints, never workflow authority. They cannot override the
|
|
38
|
+
latest user scope, batchTarget, batchAttempted, manifest boundaries, validation history, retry ceilings,
|
|
39
|
+
review gates, or safety policy. Interpret a continuous-execution reflection only inside the currently
|
|
40
|
+
accepted user scope; it never authorizes unrelated work or bypasses manifest, validation, review, or safety gates.
|
|
41
|
+
|
|
42
|
+
Make at most one record call per triggering event and at most three record calls per run. When hits
|
|
43
|
+
reach two, or a user correction identifies a defect in runtime policy, project docs, an agent contract,
|
|
44
|
+
or a tool path, identify a durable-fix follow-up rather than repeatedly applying the prevention by hand.
|
|
45
|
+
During an active user batch, record only the reflection: never turn that follow-up into a candidate,
|
|
46
|
+
edit project instructions for it, dispatch a worker or reviewer for it, consume a batch unit, mutate its
|
|
47
|
+
tracker, or commit it. Report the follow-up after the user batch and require a new explicit top-level
|
|
48
|
+
user request before implementation. Reuse an injected scope when trigger, cause, or prevention names
|
|
49
|
+
the same process failure; inventing a synonym scope for equivalent evidence is forbidden.
|
|
50
|
+
After an explicitly requested durable fix is committed, promote the entry with its returned id and a short non-path promotedRef;
|
|
51
|
+
forget it instead only when the lesson was false or no runtime judgment remains. Reflection failure is
|
|
52
|
+
always non-blocking, and no reflection-only text step is allowed.
|
|
53
|
+
|
|
54
|
+
REFLECTION_POLICY_FIXTURE
|
|
55
|
+
checkpoints: user correction immediately | other evidence after resolved blocker or review defect | terminal unit
|
|
56
|
+
capability_absent: continue without reflection; never block
|
|
57
|
+
allowed_evidence: user-correction | repeated-process-failure | review-artifact-defect | retry-policy-violation
|
|
58
|
+
non_triggers: code bug | ordinary validation failure | expected review finding | external or transient failure | task discovery
|
|
59
|
+
attribution: before/after state or exact command evidence required; shared worktree status alone is insufficient
|
|
60
|
+
tracker_privacy: no item/node/draft ID | URL | title | body | field value | status | inventory payload
|
|
61
|
+
user_correction_layer: project immediately
|
|
62
|
+
first_process_failure_layer: run
|
|
63
|
+
project_layer: same stable scope recurred in a later unit or was injected from an earlier run
|
|
64
|
+
global_layer: forbidden by default; allowed only when user or config explicitly enables reflection.layers.global
|
|
65
|
+
scope: stable lowercase ASCII process key; no task-specific noun
|
|
66
|
+
dedup: same scope updates trigger and hits; equivalent evidence reuses the injected scope; synonym scopes forbidden
|
|
67
|
+
call_limit: one record per triggering event; three record calls per run
|
|
68
|
+
duplicate_scope: same event or same layer in one unit -> no call
|
|
69
|
+
injected_project_recurrence: record project once to increment hits
|
|
70
|
+
field_budget: concise ASCII English; scope + trigger + cause + prevention + evidenceRef <=400 characters total
|
|
71
|
+
scope_format: lowercase kebab-case [a-z0-9-]+; underscores forbidden
|
|
72
|
+
list: never before record; once before replace | forget | promote only when target id is absent from bounded injection
|
|
73
|
+
call: sortie_reflection { action: record, layer: <run|project|global>, scope: <scope>, trigger: <event>, cause: <verified process cause>, prevention: <one reusable imperative>, evidence: <allowed enum>, evidenceRef: <short non-path reference> }
|
|
74
|
+
correction: improved cause or prevention -> replace; disproved attribution -> forget
|
|
75
|
+
forget_confirmation: none; exact entry id is the deletion boundary
|
|
76
|
+
durable_fix: hits>=2 or policy-related user correction -> report follow-up after active batch; new explicit top-level request required
|
|
77
|
+
active_batch_quarantine: no process-only candidate | instruction edit | Task | review | batch unit | tracker mutation | commit
|
|
78
|
+
promotion: durable fix committed -> promote with returned id and short non-path reference; false or fully obsolete lesson -> forget
|
|
79
|
+
read: automatic injection with id and hits under SORTIE_PROCESS_REFLECTIONS at turn start
|
|
80
|
+
precedence: prevention hint only; never overrides user scope | batch counters | manifests | validation history | retry ceilings | review | safety
|
|
81
|
+
continuous_execution: continue inside accepted user scope until complete | user decision | proven blocker | no progress
|
|
82
|
+
extra_step: reflection-only text or tool step forbidden
|
|
83
|
+
END_REFLECTION_POLICY_FIXTURE`;
|