sortie-dogs 0.5.17 → 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 +15 -1
- package/dist/plugin/index.js +122 -16
- package/dist/plugin/run-metrics.d.ts +40 -0
- package/dist/plugin/run-metrics.js +169 -0
- 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 +14 -94
- 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.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
|
@@ -3,12 +3,26 @@ import { type ContinuationClient } from "./continuation.js";
|
|
|
3
3
|
import { type ToolExecuteBeforeInput, type ToolExecuteBeforeOutput } from "./gate.js";
|
|
4
4
|
import { type OpenCodeChatMessageHook, type OpenCodeModelAvailabilityClient } from "./model-routing-hook.js";
|
|
5
5
|
import { type SessionMessageReader } from "./task-result-repair.js";
|
|
6
|
+
import type { RunMetricsClient } from "./run-metrics.js";
|
|
6
7
|
export declare const PARALLEL_COMMIT_ARTIFACT_CAPABILITY = "sortie_create_parallel_commit_artifact";
|
|
7
8
|
export interface OpenCodePluginInput {
|
|
8
9
|
directory: string;
|
|
9
10
|
worktree?: string;
|
|
10
11
|
/** The host SDK client. Absent in hosts that construct the plugin without one. */
|
|
11
|
-
client?: SessionMessageReader & ContinuationClient & OpenCodeModelAvailabilityClient & {
|
|
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
|
+
};
|
|
12
26
|
tui?: {
|
|
13
27
|
showToast?: (request: {
|
|
14
28
|
body: {
|
package/dist/plugin/index.js
CHANGED
|
@@ -17,7 +17,8 @@ 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
|
+
import { collectRunMetrics, insertRunMetrics, isDoneTerminalText } from "./run-metrics.js";
|
|
21
22
|
const INPUT_LIMITS = { config: 64 * 1024, manifest: 512 * 1024, handoff: 2 * 1024 * 1024, parallel: 512 * 1024 };
|
|
22
23
|
const INSPECTION_CACHE = { maximum: 256, ttlMilliseconds: 30 * 60 * 1000 };
|
|
23
24
|
const ACTIVE_SESSION_CACHE = { maximum: 256, ttlMilliseconds: 30 * 60 * 1000 };
|
|
@@ -802,6 +803,83 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
802
803
|
let bootstrapCompleted = false;
|
|
803
804
|
let assetVersionReported = false;
|
|
804
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
|
+
}
|
|
805
883
|
// Project config read is required discovery for its opt-in; no reflection storage/version read
|
|
806
884
|
// occurs unless that resolved config enables reflection. It stays isolated from write-gate load.
|
|
807
885
|
try {
|
|
@@ -843,7 +921,12 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
843
921
|
*/
|
|
844
922
|
(sessionID) => isCoordinatorSession(sessionID)
|
|
845
923
|
? { agent: COORDINATOR_AGENT, parentID: undefined }
|
|
846
|
-
: 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
|
+
}));
|
|
847
930
|
const completedCoordinatorMessages = new Set();
|
|
848
931
|
const completedCoordinatorParts = new Set();
|
|
849
932
|
async function ensureLoaded() {
|
|
@@ -2360,6 +2443,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2360
2443
|
}
|
|
2361
2444
|
function expireSession(sessionID) {
|
|
2362
2445
|
activeSessions.delete(sessionID);
|
|
2446
|
+
sessionOperationMetrics.delete(sessionID);
|
|
2363
2447
|
abandonSessionLease(sessionID);
|
|
2364
2448
|
sessionAuthorizations.delete(sessionID);
|
|
2365
2449
|
if (!childHasInFlightParentTask(sessionID)) {
|
|
@@ -2380,6 +2464,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2380
2464
|
}
|
|
2381
2465
|
function evictSession(sessionID) {
|
|
2382
2466
|
activeSessions.delete(sessionID);
|
|
2467
|
+
sessionOperationMetrics.delete(sessionID);
|
|
2383
2468
|
abandonSessionLease(sessionID);
|
|
2384
2469
|
sessionAuthorizations.delete(sessionID);
|
|
2385
2470
|
bindingPins.delete(sessionID);
|
|
@@ -2478,10 +2563,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2478
2563
|
if (get === undefined)
|
|
2479
2564
|
return undefined;
|
|
2480
2565
|
try {
|
|
2481
|
-
const response = await get.call(input.client.session, {
|
|
2566
|
+
const response = await measureSessionOperation(sessionID, "hostSessionIdentity", () => get.call(input.client.session, {
|
|
2482
2567
|
path: { id: sessionID },
|
|
2483
2568
|
query: { directory: input.directory },
|
|
2484
|
-
});
|
|
2569
|
+
}));
|
|
2485
2570
|
const payload = isRecord(response) && "data" in response ? response.data : response;
|
|
2486
2571
|
if (!isRecord(payload))
|
|
2487
2572
|
return undefined;
|
|
@@ -2903,6 +2988,16 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2903
2988
|
.replaceAll(CONTINUATION_MARKER, "")
|
|
2904
2989
|
.trimEnd();
|
|
2905
2990
|
}
|
|
2991
|
+
if ((isCoordinatorSession(textInput.sessionID) || await recoverCoordinatorRoot(textInput.sessionID)) &&
|
|
2992
|
+
isDoneTerminalText(textOutput.text)) {
|
|
2993
|
+
const metrics = await measureSessionOperation(textInput.sessionID, "collectRunMetrics", () => collectRunMetrics(input.client, textInput.sessionID, input.directory).catch(() => undefined));
|
|
2994
|
+
if (metrics !== undefined)
|
|
2995
|
+
textOutput.text = insertRunMetrics(textOutput.text, metrics);
|
|
2996
|
+
appLogInfo("run-metrics.snapshot", textInput.sessionID, {
|
|
2997
|
+
available: metrics !== undefined,
|
|
2998
|
+
...operationMetricsSnapshot(textInput.sessionID),
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
2906
3001
|
await completeContinuationText(textInput.sessionID, textOutput.text, false);
|
|
2907
3002
|
},
|
|
2908
3003
|
"experimental.session.compacting": async (compactInput, compactOutput) => {
|
|
@@ -3050,22 +3145,33 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
3050
3145
|
}
|
|
3051
3146
|
}
|
|
3052
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
|
+
}
|
|
3053
3155
|
if (!reflectionStartup || !(await beginReflection(transformInput.sessionID)))
|
|
3054
3156
|
return;
|
|
3055
3157
|
const config = reflectionConfiguration;
|
|
3056
3158
|
try {
|
|
3057
3159
|
if (!config)
|
|
3058
3160
|
return;
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
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
|
+
}
|
|
3069
3175
|
finally {
|
|
3070
3176
|
endReflection(transformInput.sessionID);
|
|
3071
3177
|
}
|
|
@@ -3220,7 +3326,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
3220
3326
|
const bootstrap = bootstrapRequired && !exactCoordinatorDirectOperation &&
|
|
3221
3327
|
!coordinatorCapability && !sessionGateCapability &&
|
|
3222
3328
|
!sessionAuthorizations.has(toolInput.sessionID) && (coordinatorRoot || coordinatorRoots.size > 0)
|
|
3223
|
-
? await bootstrapControlState
|
|
3329
|
+
? await measureSessionOperation(toolInput.sessionID, "bootstrapControlState", bootstrapControlState)
|
|
3224
3330
|
: undefined;
|
|
3225
3331
|
if (coordinatorRoot && bootstrapRequired && !exactCoordinatorDirectOperation &&
|
|
3226
3332
|
!coordinatorCapability && !sessionGateCapability) {
|
|
@@ -3630,7 +3736,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
3630
3736
|
if (event.type === "session.compacted")
|
|
3631
3737
|
await continuation.sessionCompacted(eventSessionID);
|
|
3632
3738
|
if (event.type === "session.idle" && isCoordinatorSession(eventSessionID)) {
|
|
3633
|
-
const bootstrap = await bootstrapControlState
|
|
3739
|
+
const bootstrap = await measureSessionOperation(eventSessionID, "bootstrapControlState", bootstrapControlState);
|
|
3634
3740
|
if (bootstrapRequired && bootstrap?.usable === true && bootstrap.missing.length > 0) {
|
|
3635
3741
|
if (!bootstrapIdleWarnings.has(eventSessionID)) {
|
|
3636
3742
|
bootstrapIdleWarnings.add(eventSessionID);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface RunMetricsClient {
|
|
2
|
+
readonly session?: {
|
|
3
|
+
readonly get?: (request: {
|
|
4
|
+
path: {
|
|
5
|
+
id: string;
|
|
6
|
+
};
|
|
7
|
+
query?: {
|
|
8
|
+
directory?: string;
|
|
9
|
+
};
|
|
10
|
+
}) => Promise<unknown>;
|
|
11
|
+
readonly children?: (request: {
|
|
12
|
+
path: {
|
|
13
|
+
id: string;
|
|
14
|
+
};
|
|
15
|
+
query?: {
|
|
16
|
+
directory?: string;
|
|
17
|
+
};
|
|
18
|
+
}) => Promise<unknown>;
|
|
19
|
+
readonly messages?: (request: {
|
|
20
|
+
path: {
|
|
21
|
+
id: string;
|
|
22
|
+
};
|
|
23
|
+
query?: {
|
|
24
|
+
directory?: string;
|
|
25
|
+
};
|
|
26
|
+
}) => Promise<unknown>;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface RunMetrics {
|
|
30
|
+
readonly durationMilliseconds: number | undefined;
|
|
31
|
+
readonly tokens: number | undefined;
|
|
32
|
+
readonly cost: number | undefined;
|
|
33
|
+
readonly steps: number | undefined;
|
|
34
|
+
readonly sessions: number | undefined;
|
|
35
|
+
readonly cacheRatio: number | undefined;
|
|
36
|
+
}
|
|
37
|
+
export declare function collectRunMetrics(client: RunMetricsClient | undefined, rootSessionID: string, directory?: string, now?: number): Promise<RunMetrics | undefined>;
|
|
38
|
+
export declare function formatRunMetrics(metrics: RunMetrics): string;
|
|
39
|
+
export declare function isDoneTerminalText(text: string): boolean;
|
|
40
|
+
export declare function insertRunMetrics(text: string, metrics: RunMetrics): string;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const MAX_SESSIONS = 128;
|
|
2
|
+
function record(value) {
|
|
3
|
+
return value !== null && typeof value === "object" ? value : undefined;
|
|
4
|
+
}
|
|
5
|
+
function unwrap(value) {
|
|
6
|
+
const object = record(value);
|
|
7
|
+
return object !== undefined && "data" in object ? object.data : value;
|
|
8
|
+
}
|
|
9
|
+
function number(value) {
|
|
10
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
11
|
+
}
|
|
12
|
+
function messageTokens(message) {
|
|
13
|
+
const info = record(message.info) ?? message;
|
|
14
|
+
const tokens = record(info.tokens) ?? record(message.tokens);
|
|
15
|
+
if (tokens === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
const input = number(tokens.input);
|
|
18
|
+
const output = number(tokens.output);
|
|
19
|
+
const reasoning = number(tokens.reasoning);
|
|
20
|
+
const cache = record(tokens.cache);
|
|
21
|
+
const cacheRead = number(cache?.read) ?? number(tokens.cacheRead) ?? number(tokens.cache_read);
|
|
22
|
+
const cacheWrite = number(cache?.write) ?? number(tokens.cacheWrite) ?? number(tokens.cache_write);
|
|
23
|
+
if (input === undefined || output === undefined || reasoning === undefined || cacheRead === undefined || cacheWrite === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
return [input + output + reasoning + cacheRead + cacheWrite, cacheRead];
|
|
26
|
+
}
|
|
27
|
+
function assistantMessages(value) {
|
|
28
|
+
const payload = unwrap(value);
|
|
29
|
+
if (!Array.isArray(payload))
|
|
30
|
+
return undefined;
|
|
31
|
+
return payload.filter((entry) => {
|
|
32
|
+
const item = record(entry);
|
|
33
|
+
const info = item === undefined ? undefined : record(item.info);
|
|
34
|
+
return item !== undefined && (info?.role ?? item.role) === "assistant";
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export async function collectRunMetrics(client, rootSessionID, directory, now = Date.now()) {
|
|
38
|
+
const session = client?.session;
|
|
39
|
+
if (session?.messages === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
const ids = [rootSessionID];
|
|
42
|
+
const visited = new Set(ids);
|
|
43
|
+
let hierarchyComplete = session.children !== undefined;
|
|
44
|
+
for (let index = 0; index < ids.length && ids.length < MAX_SESSIONS; index += 1) {
|
|
45
|
+
if (session.children === undefined)
|
|
46
|
+
break;
|
|
47
|
+
try {
|
|
48
|
+
const children = unwrap(await session.children.call(session, { path: { id: ids[index] }, query: { directory } }));
|
|
49
|
+
if (!Array.isArray(children)) {
|
|
50
|
+
hierarchyComplete = false;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
for (const child of children) {
|
|
54
|
+
const item = record(child);
|
|
55
|
+
const id = typeof item?.id === "string" ? item.id : typeof item?.sessionID === "string" ? item.sessionID : undefined;
|
|
56
|
+
if (id === undefined) {
|
|
57
|
+
hierarchyComplete = false;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!visited.has(id) && ids.length < MAX_SESSIONS) {
|
|
61
|
+
visited.add(id);
|
|
62
|
+
ids.push(id);
|
|
63
|
+
}
|
|
64
|
+
else if (!visited.has(id))
|
|
65
|
+
hierarchyComplete = false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
hierarchyComplete = false;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (ids.length >= MAX_SESSIONS)
|
|
74
|
+
hierarchyComplete = false;
|
|
75
|
+
const uniqueMessages = new Set();
|
|
76
|
+
let totalTokens = 0;
|
|
77
|
+
let cacheRead = 0;
|
|
78
|
+
let tokensAvailable = true;
|
|
79
|
+
let messagesComplete = true;
|
|
80
|
+
let steps = 0;
|
|
81
|
+
let cost = 0;
|
|
82
|
+
let costAvailable = true;
|
|
83
|
+
for (const id of ids) {
|
|
84
|
+
try {
|
|
85
|
+
const messages = assistantMessages(await session.messages.call(session, { path: { id }, query: { directory } }));
|
|
86
|
+
if (messages === undefined)
|
|
87
|
+
return undefined;
|
|
88
|
+
for (const message of messages) {
|
|
89
|
+
const info = record(message.info) ?? message;
|
|
90
|
+
const time = record(info.time) ?? record(message.time);
|
|
91
|
+
if (time !== undefined && number(time.completed) === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
const messageID = typeof info.id === "string" ? info.id : typeof message.id === "string" ? message.id : undefined;
|
|
94
|
+
if (messageID === undefined) {
|
|
95
|
+
messagesComplete = false;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (uniqueMessages.has(messageID))
|
|
99
|
+
continue;
|
|
100
|
+
uniqueMessages.add(messageID);
|
|
101
|
+
steps += 1;
|
|
102
|
+
const tokens = messageTokens(message);
|
|
103
|
+
if (tokens !== undefined) {
|
|
104
|
+
totalTokens += tokens[0];
|
|
105
|
+
cacheRead += tokens[1];
|
|
106
|
+
}
|
|
107
|
+
else
|
|
108
|
+
tokensAvailable = false;
|
|
109
|
+
const reportedCost = number(info.cost) ?? number(message.cost);
|
|
110
|
+
if (reportedCost === undefined)
|
|
111
|
+
costAvailable = false;
|
|
112
|
+
else
|
|
113
|
+
cost += reportedCost;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let created;
|
|
121
|
+
if (session.get !== undefined) {
|
|
122
|
+
try {
|
|
123
|
+
const root = record(unwrap(await session.get.call(session, { path: { id: rootSessionID }, query: { directory } })));
|
|
124
|
+
const time = record(root?.time);
|
|
125
|
+
created = number(time?.created);
|
|
126
|
+
}
|
|
127
|
+
catch { /* fallback below */ }
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
durationMilliseconds: created === undefined ? undefined : Math.max(0, now - created),
|
|
131
|
+
tokens: hierarchyComplete && messagesComplete && tokensAvailable ? totalTokens : undefined,
|
|
132
|
+
cost: hierarchyComplete && messagesComplete && costAvailable ? cost : undefined,
|
|
133
|
+
steps: hierarchyComplete && messagesComplete ? steps : undefined,
|
|
134
|
+
sessions: hierarchyComplete ? ids.length : undefined,
|
|
135
|
+
cacheRatio: hierarchyComplete && messagesComplete && tokensAvailable && totalTokens > 0 ? cacheRead / totalTokens : undefined,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function duration(milliseconds) {
|
|
139
|
+
const seconds = Math.floor(milliseconds / 1000);
|
|
140
|
+
if (seconds < 60)
|
|
141
|
+
return `${seconds}s`;
|
|
142
|
+
const minutes = Math.floor(seconds / 60);
|
|
143
|
+
return `${minutes}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
144
|
+
}
|
|
145
|
+
export function formatRunMetrics(metrics) {
|
|
146
|
+
const elapsed = metrics.durationMilliseconds === undefined ? "duration unavailable" : `${duration(metrics.durationMilliseconds)} wall-clock`;
|
|
147
|
+
const cost = metrics.cost === undefined ? "cost unavailable" : `$${metrics.cost.toFixed(4)}`;
|
|
148
|
+
const tokens = metrics.tokens === undefined ? "tokens unavailable" : `${metrics.tokens.toLocaleString("en-US")} tokens`;
|
|
149
|
+
const steps = metrics.steps === undefined ? "steps unavailable" : `${metrics.steps} completed assistant model step${metrics.steps === 1 ? "" : "s"}`;
|
|
150
|
+
const sessions = metrics.sessions === undefined ? "sessions unavailable" : `${metrics.sessions} session${metrics.sessions === 1 ? "" : "s"}`;
|
|
151
|
+
const cache = metrics.cacheRatio === undefined ? "cache ratio unavailable" : `${(metrics.cacheRatio * 100).toFixed(1)}% cache ratio`;
|
|
152
|
+
return `**Run:** pre-terminal host snapshot · ${elapsed} · ${tokens} · ${cost} · ${steps} · ${sessions} · ${cache}`;
|
|
153
|
+
}
|
|
154
|
+
export function isDoneTerminalText(text) {
|
|
155
|
+
const first = text.split(/\r?\n/u).find((line) => line.trim().length > 0) ?? "";
|
|
156
|
+
return /^✅\s+\*\*DONE\*\*(?:\s|$)/u.test(first) || /^status:\s*DONE(?:\s|$)/u.test(first);
|
|
157
|
+
}
|
|
158
|
+
export function insertRunMetrics(text, metrics) {
|
|
159
|
+
if (/\*\*Run:\*\*/u.test(text) || !isDoneTerminalText(text))
|
|
160
|
+
return text;
|
|
161
|
+
const newline = text.includes("\r\n") ? "\r\n" : "\n";
|
|
162
|
+
const statusStart = text.search(/^(?:✅\s+\*\*DONE\*\*|status:\s*DONE).*$/mu);
|
|
163
|
+
if (statusStart < 0)
|
|
164
|
+
return text;
|
|
165
|
+
const statusEnd = text.indexOf(newline, statusStart);
|
|
166
|
+
if (statusEnd < 0)
|
|
167
|
+
return `${text}${newline}${newline}${formatRunMetrics(metrics)}`;
|
|
168
|
+
return `${text.slice(0, statusEnd + newline.length)}${newline}${formatRunMetrics(metrics)}${newline}${text.slice(statusEnd + newline.length)}`;
|
|
169
|
+
}
|
|
@@ -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