dsh-decision-layer 0.7.1 → 0.7.3
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.en.md +14 -14
- package/README.md +16 -16
- package/docs/en/DETAIL.md +2 -2
- package/docs/zh/DETAIL.md +2 -2
- package/lib/client.js +54 -23
- package/lib/host/backend.d.ts +15 -0
- package/lib/host/backend.js +149 -0
- package/lib/host/config-store.d.ts +12 -0
- package/lib/host/config-store.js +254 -0
- package/lib/host/danger-gate.d.ts +45 -0
- package/lib/host/danger-gate.js +196 -0
- package/lib/host/decision-context.d.ts +21 -0
- package/lib/host/decision-context.js +187 -0
- package/lib/host/excerpt.d.ts +26 -0
- package/lib/host/excerpt.js +76 -0
- package/lib/host/index.d.ts +6 -0
- package/lib/host/index.js +287 -0
- package/lib/host/log-analyze.d.ts +121 -0
- package/lib/host/log-analyze.js +254 -0
- package/lib/host/log-read.d.ts +4 -0
- package/lib/host/log-read.js +34 -0
- package/lib/host/log-store.d.ts +15 -0
- package/lib/host/log-store.js +98 -0
- package/lib/host/loop-guard.d.ts +10 -0
- package/lib/host/loop-guard.js +52 -0
- package/lib/host/manager-api.d.ts +20 -0
- package/lib/host/manager-api.js +174 -0
- package/lib/host/narrowing.d.ts +51 -0
- package/lib/host/narrowing.js +214 -0
- package/lib/host/quality-trend.d.ts +14 -0
- package/lib/host/quality-trend.js +22 -0
- package/lib/host/self-check.d.ts +28 -0
- package/lib/host/self-check.js +111 -0
- package/lib/host/session-state.d.ts +19 -0
- package/lib/host/session-state.js +289 -0
- package/lib/host/state-store.d.ts +14 -0
- package/lib/host/state-store.js +109 -0
- package/lib/host/steer-once.d.ts +13 -0
- package/lib/host/steer-once.js +25 -0
- package/lib/host/task-completion.d.ts +30 -0
- package/lib/host/task-completion.js +166 -0
- package/lib/host/types.d.ts +365 -0
- package/lib/host/types.js +11 -0
- package/package.json +17 -10
- package/service/.gitkeep +0 -0
- package/service/backend.mjs +0 -127
- package/service/config-store.mjs +0 -311
- package/service/danger-gate.mjs +0 -182
- package/service/dsh.mjs +0 -288
- package/service/log-analyze.mjs +0 -257
- package/service/log-read.mjs +0 -27
- package/service/log-store.mjs +0 -82
- package/service/loop-guard.mjs +0 -51
- package/service/manager-api.mjs +0 -137
- package/service/narrowing.mjs +0 -226
- package/service/quality-trend.mjs +0 -29
- package/service/self-check.mjs +0 -131
- package/service/session-state.mjs +0 -260
- package/service/state-store.mjs +0 -100
- package/service/steer-once.mjs +0 -25
- package/service/task-completion.mjs +0 -163
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { LogRow, SessionSeed, TrendConfig } from './types.js';
|
|
2
|
+
export type PersistedRecord = Record<string, unknown>;
|
|
3
|
+
export type Annotation = {
|
|
4
|
+
rating?: unknown;
|
|
5
|
+
at: number;
|
|
6
|
+
};
|
|
7
|
+
export type TrendBacktest = {
|
|
8
|
+
warnHits: number;
|
|
9
|
+
severeHits: number;
|
|
10
|
+
maxRun: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function foldSession(records: Iterable<unknown>): {
|
|
13
|
+
decisions: Map<string, PersistedRecord>;
|
|
14
|
+
annotations: Map<string, Annotation>;
|
|
15
|
+
};
|
|
16
|
+
export type Profile = {
|
|
17
|
+
gate: {
|
|
18
|
+
attempts: number;
|
|
19
|
+
allow: number;
|
|
20
|
+
ask: number;
|
|
21
|
+
deny: number;
|
|
22
|
+
error: number;
|
|
23
|
+
};
|
|
24
|
+
check: {
|
|
25
|
+
attempts: number;
|
|
26
|
+
ok: number;
|
|
27
|
+
low: number;
|
|
28
|
+
error: number;
|
|
29
|
+
scoreSum: number;
|
|
30
|
+
scoreCount: number;
|
|
31
|
+
confidence: {
|
|
32
|
+
lt40: number;
|
|
33
|
+
lt60: number;
|
|
34
|
+
lt80: number;
|
|
35
|
+
gte80: number;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
narrow: {
|
|
39
|
+
attempts: number;
|
|
40
|
+
applied: number;
|
|
41
|
+
error: number;
|
|
42
|
+
droppedSum: number;
|
|
43
|
+
tooManyCandidates: number;
|
|
44
|
+
};
|
|
45
|
+
complete: {
|
|
46
|
+
attempts: number;
|
|
47
|
+
satisfied: number;
|
|
48
|
+
unsatisfied: number;
|
|
49
|
+
insufficient: number;
|
|
50
|
+
error: number;
|
|
51
|
+
steered: number;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
export declare function buildProfile(decisions: ReadonlyMap<string, PersistedRecord> | Iterable<PersistedRecord>): Profile;
|
|
55
|
+
export declare function backtestTrend(checkRecords: Iterable<unknown>, { trendMinConfidence, trendRun, trendSevereRun, lowScoreThreshold }?: TrendConfig): TrendBacktest;
|
|
56
|
+
export declare function annotationSummary(decisions: ReadonlyMap<string, PersistedRecord>, annotations: ReadonlyMap<string, Annotation>): {
|
|
57
|
+
summary: {
|
|
58
|
+
rated: number;
|
|
59
|
+
good: number;
|
|
60
|
+
bad: number;
|
|
61
|
+
unsure: number;
|
|
62
|
+
};
|
|
63
|
+
ratings: Record<string, unknown>;
|
|
64
|
+
};
|
|
65
|
+
export declare function analyzeSession(records: Iterable<unknown>, trendConfig?: TrendConfig): {
|
|
66
|
+
totalDecisions: number;
|
|
67
|
+
profile: Profile;
|
|
68
|
+
trendBacktest: TrendBacktest;
|
|
69
|
+
annotations: {
|
|
70
|
+
rated: number;
|
|
71
|
+
good: number;
|
|
72
|
+
bad: number;
|
|
73
|
+
unsure: number;
|
|
74
|
+
};
|
|
75
|
+
ratings: Record<string, unknown>;
|
|
76
|
+
};
|
|
77
|
+
export type ReadStream = (options: {
|
|
78
|
+
dir?: string;
|
|
79
|
+
port?: number;
|
|
80
|
+
}) => AsyncIterable<unknown>;
|
|
81
|
+
export declare function analyzeSessionLogs(sessionId: unknown, { dir, port, trendConfig, readStream }?: {
|
|
82
|
+
dir?: string;
|
|
83
|
+
port?: number;
|
|
84
|
+
trendConfig?: TrendConfig;
|
|
85
|
+
readStream?: ReadStream;
|
|
86
|
+
}): Promise<{
|
|
87
|
+
totalDecisions: number;
|
|
88
|
+
profile: Profile;
|
|
89
|
+
trendBacktest: TrendBacktest;
|
|
90
|
+
annotations: {
|
|
91
|
+
rated: number;
|
|
92
|
+
good: number;
|
|
93
|
+
bad: number;
|
|
94
|
+
unsure: number;
|
|
95
|
+
};
|
|
96
|
+
ratings: Record<string, unknown>;
|
|
97
|
+
}>;
|
|
98
|
+
export declare function buildSeed(decisions: ReadonlyMap<string, PersistedRecord>, { maxLog, trendConfig }?: {
|
|
99
|
+
maxLog?: number;
|
|
100
|
+
trendConfig?: TrendConfig;
|
|
101
|
+
}): SessionSeed;
|
|
102
|
+
export declare function reconstructSessionSeed(sessionId: unknown, { dir, port, trendConfig, maxLog, readStream }?: {
|
|
103
|
+
dir?: string;
|
|
104
|
+
port?: number;
|
|
105
|
+
trendConfig?: TrendConfig;
|
|
106
|
+
maxLog?: number;
|
|
107
|
+
readStream?: ReadStream;
|
|
108
|
+
}): Promise<SessionSeed | undefined>;
|
|
109
|
+
export declare function listSessionDecisions(sessionId: unknown, { dir, port, page, pageSize, readStream }?: {
|
|
110
|
+
dir?: string;
|
|
111
|
+
port?: number;
|
|
112
|
+
page?: number;
|
|
113
|
+
pageSize?: number;
|
|
114
|
+
readStream?: ReadStream;
|
|
115
|
+
}): Promise<{
|
|
116
|
+
entries: LogRow[];
|
|
117
|
+
total: number;
|
|
118
|
+
page: number;
|
|
119
|
+
pageSize: number;
|
|
120
|
+
pages: number;
|
|
121
|
+
}>;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { createReadableStream } from "./log-read.js";
|
|
2
|
+
import { advanceRun, severityOf } from "./quality-trend.js";
|
|
3
|
+
import { narrowApplied } from "./narrowing.js";
|
|
4
|
+
import { isObject } from "./types.js";
|
|
5
|
+
const DECISION_KINDS = /* @__PURE__ */ new Set(["gate", "check", "narrow", "complete"]);
|
|
6
|
+
const atOf = (record) => {
|
|
7
|
+
const value = record.at;
|
|
8
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
9
|
+
};
|
|
10
|
+
function foldSession(records) {
|
|
11
|
+
const decisions = /* @__PURE__ */ new Map();
|
|
12
|
+
const annotations = /* @__PURE__ */ new Map();
|
|
13
|
+
for (const record of records) {
|
|
14
|
+
if (!isObject(record)) continue;
|
|
15
|
+
if (record.kind === "annotation") {
|
|
16
|
+
const target = typeof record.target === "string" ? record.target : void 0;
|
|
17
|
+
if (!target) continue;
|
|
18
|
+
const prev = annotations.get(target);
|
|
19
|
+
if (!prev || Number.isFinite(record.at) && record.at >= (prev.at ?? 0)) {
|
|
20
|
+
annotations.set(target, { rating: record.rating, at: Number.isFinite(record.at) ? record.at : 0 });
|
|
21
|
+
}
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (DECISION_KINDS.has(record.kind) && typeof record.id === "string") {
|
|
25
|
+
decisions.set(record.id, record);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { decisions, annotations };
|
|
29
|
+
}
|
|
30
|
+
function confidenceBucket(value) {
|
|
31
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
32
|
+
if (value < 0.4) return "lt40";
|
|
33
|
+
if (value < 0.6) return "lt60";
|
|
34
|
+
if (value < 0.8) return "lt80";
|
|
35
|
+
return "gte80";
|
|
36
|
+
}
|
|
37
|
+
function emptyProfile() {
|
|
38
|
+
return {
|
|
39
|
+
gate: { attempts: 0, allow: 0, ask: 0, deny: 0, error: 0 },
|
|
40
|
+
check: { attempts: 0, ok: 0, low: 0, error: 0, scoreSum: 0, scoreCount: 0, confidence: { lt40: 0, lt60: 0, lt80: 0, gte80: 0 } },
|
|
41
|
+
narrow: { attempts: 0, applied: 0, error: 0, droppedSum: 0, tooManyCandidates: 0 },
|
|
42
|
+
complete: { attempts: 0, satisfied: 0, unsatisfied: 0, insufficient: 0, error: 0, steered: 0 }
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function buildProfile(decisions) {
|
|
46
|
+
const profile = emptyProfile();
|
|
47
|
+
const rows = decisions instanceof Map ? [...decisions.values()] : [...decisions];
|
|
48
|
+
for (const record of rows) {
|
|
49
|
+
const kind = record.kind;
|
|
50
|
+
switch (kind) {
|
|
51
|
+
case "gate": {
|
|
52
|
+
const bucket = profile.gate;
|
|
53
|
+
bucket.attempts++;
|
|
54
|
+
if (record.outcome === "error") bucket.error++;
|
|
55
|
+
else if (record.outcome === "allow") bucket.allow++;
|
|
56
|
+
else if (record.outcome === "ask") bucket.ask++;
|
|
57
|
+
else if (record.outcome === "deny") bucket.deny++;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case "check": {
|
|
61
|
+
const bucket = profile.check;
|
|
62
|
+
bucket.attempts++;
|
|
63
|
+
if (record.outcome === "error") bucket.error++;
|
|
64
|
+
else if (record.outcome === "low") bucket.low++;
|
|
65
|
+
else bucket.ok++;
|
|
66
|
+
if (typeof record.score === "number" && Number.isFinite(record.score)) {
|
|
67
|
+
bucket.scoreSum += record.score;
|
|
68
|
+
bucket.scoreCount++;
|
|
69
|
+
}
|
|
70
|
+
const cb = confidenceBucket(record.confidence);
|
|
71
|
+
if (cb) bucket.confidence[cb]++;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "narrow": {
|
|
75
|
+
const bucket = profile.narrow;
|
|
76
|
+
bucket.attempts++;
|
|
77
|
+
if (record.outcome === "error") bucket.error++;
|
|
78
|
+
else if (narrowApplied(record)) bucket.applied++;
|
|
79
|
+
if (Number.isSafeInteger(record.dropped) && record.dropped > 0) bucket.droppedSum += record.dropped;
|
|
80
|
+
if (record.reason === "too-many-candidates") bucket.tooManyCandidates++;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case "complete": {
|
|
84
|
+
const bucket = profile.complete;
|
|
85
|
+
bucket.attempts++;
|
|
86
|
+
if (record.outcome === "error") bucket.error++;
|
|
87
|
+
if (Number.isSafeInteger(record.satisfied)) bucket.satisfied += record.satisfied;
|
|
88
|
+
if (Number.isSafeInteger(record.unsatisfied)) bucket.unsatisfied += record.unsatisfied;
|
|
89
|
+
if (Number.isSafeInteger(record.insufficient)) bucket.insufficient += record.insufficient;
|
|
90
|
+
if (record.steered === true) bucket.steered++;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
default:
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return profile;
|
|
98
|
+
}
|
|
99
|
+
function backtestTrend(checkRecords, { trendMinConfidence = 0.6, trendRun = 3, trendSevereRun = 5, lowScoreThreshold = 1 } = {}) {
|
|
100
|
+
const ordered = [...checkRecords].filter((record) => isObject(record) && record.kind === "check").sort((a, b) => atOf(a) - atOf(b));
|
|
101
|
+
let run = 0, warnHits = 0, severeHits = 0, maxRun = 0;
|
|
102
|
+
for (const record of ordered) {
|
|
103
|
+
if (record.outcome === "error") continue;
|
|
104
|
+
const isLow = record.outcome === "low" || typeof record.score === "number" && record.score <= lowScoreThreshold;
|
|
105
|
+
if (!isLow) {
|
|
106
|
+
run = 0;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const confident = typeof record.confidence === "number" && Number.isFinite(record.confidence) && record.confidence >= trendMinConfidence;
|
|
110
|
+
if (!confident) continue;
|
|
111
|
+
run++;
|
|
112
|
+
maxRun = Math.max(maxRun, run);
|
|
113
|
+
if (run === trendSevereRun) severeHits++;
|
|
114
|
+
else if (run === trendRun) warnHits++;
|
|
115
|
+
}
|
|
116
|
+
return { warnHits, severeHits, maxRun };
|
|
117
|
+
}
|
|
118
|
+
function annotationSummary(decisions, annotations) {
|
|
119
|
+
const summary = { rated: 0, good: 0, bad: 0, unsure: 0 };
|
|
120
|
+
const ratings = /* @__PURE__ */ Object.create(null);
|
|
121
|
+
for (const [id] of decisions) {
|
|
122
|
+
const annotation = annotations.get(id);
|
|
123
|
+
if (!annotation || typeof annotation.rating !== "string") continue;
|
|
124
|
+
summary.rated++;
|
|
125
|
+
if (annotation.rating === "good") summary.good++;
|
|
126
|
+
else if (annotation.rating === "bad") summary.bad++;
|
|
127
|
+
else if (annotation.rating === "unsure") summary.unsure++;
|
|
128
|
+
ratings[id] = annotation.rating;
|
|
129
|
+
}
|
|
130
|
+
return { summary, ratings };
|
|
131
|
+
}
|
|
132
|
+
function analyzeSession(records, trendConfig) {
|
|
133
|
+
const { decisions, annotations } = foldSession(records);
|
|
134
|
+
const annotation = annotationSummary(decisions, annotations);
|
|
135
|
+
return {
|
|
136
|
+
totalDecisions: decisions.size,
|
|
137
|
+
profile: buildProfile(decisions),
|
|
138
|
+
trendBacktest: backtestTrend([...decisions.values()], trendConfig),
|
|
139
|
+
annotations: annotation.summary,
|
|
140
|
+
ratings: annotation.ratings
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async function analyzeSessionLogs(sessionId, { dir, port, trendConfig, readStream = createReadableStream } = {}) {
|
|
144
|
+
if (typeof sessionId !== "string" || !sessionId.trim()) throw new Error("Invalid session id");
|
|
145
|
+
const records = [];
|
|
146
|
+
for await (const record of readStream({ dir, port })) {
|
|
147
|
+
if (!isObject(record)) continue;
|
|
148
|
+
if (record.sessionId === sessionId) records.push(record);
|
|
149
|
+
}
|
|
150
|
+
return analyzeSession(records, trendConfig);
|
|
151
|
+
}
|
|
152
|
+
function buildSeed(decisions, { maxLog = 50, trendConfig = {} } = {}) {
|
|
153
|
+
const ordered = [...decisions.values()].filter((record) => DECISION_KINDS.has(record.kind)).sort((a, b) => atOf(a) - atOf(b));
|
|
154
|
+
const seed = {
|
|
155
|
+
attempts: 0,
|
|
156
|
+
failures: 0,
|
|
157
|
+
gate: { attempts: 0, failures: 0, ask: 0, deny: 0, allow: 0, actual: { allow: 0, deny: 0, error: 0 } },
|
|
158
|
+
check: { attempts: 0, failures: 0, low: 0, run: 0, severity: "normal" },
|
|
159
|
+
narrow: { attempts: 0, failures: 0, applied: 0, dropped: 0 },
|
|
160
|
+
complete: { attempts: 0, failures: 0, unsatisfied: 0, satisfied: 0, insufficient: 0, steered: 0 },
|
|
161
|
+
log: []
|
|
162
|
+
};
|
|
163
|
+
const minConfidence = typeof trendConfig.trendMinConfidence === "number" ? trendConfig.trendMinConfidence : 0.6;
|
|
164
|
+
const thresholds = { trendRun: trendConfig.trendRun ?? 3, trendSevereRun: trendConfig.trendSevereRun ?? 5 };
|
|
165
|
+
let run = 0;
|
|
166
|
+
for (const record of ordered) {
|
|
167
|
+
seed.attempts++;
|
|
168
|
+
const isError = record.outcome === "error";
|
|
169
|
+
if (isError) seed.failures++;
|
|
170
|
+
if (record.kind === "gate") {
|
|
171
|
+
seed.gate.attempts++;
|
|
172
|
+
if (isError) seed.gate.failures++;
|
|
173
|
+
if (record.outcome === "allow") seed.gate.allow++;
|
|
174
|
+
else if (record.outcome === "ask") seed.gate.ask++;
|
|
175
|
+
else if (record.outcome === "deny") seed.gate.deny++;
|
|
176
|
+
} else if (record.kind === "check") {
|
|
177
|
+
seed.check.attempts++;
|
|
178
|
+
if (isError) seed.check.failures++;
|
|
179
|
+
if (!isError) {
|
|
180
|
+
if (record.outcome === "low") seed.check.low++;
|
|
181
|
+
const confident = typeof record.confidence === "number" && Number.isFinite(record.confidence) && record.confidence >= minConfidence;
|
|
182
|
+
run = advanceRun(run, { lowScore: record.outcome === "low", confident });
|
|
183
|
+
}
|
|
184
|
+
} else if (record.kind === "narrow") {
|
|
185
|
+
seed.narrow.attempts++;
|
|
186
|
+
if (isError) seed.narrow.failures++;
|
|
187
|
+
if (record.outcome === "applied") {
|
|
188
|
+
if (narrowApplied(record)) seed.narrow.applied++;
|
|
189
|
+
if (Number.isSafeInteger(record.dropped) && record.dropped > 0) seed.narrow.dropped += record.dropped;
|
|
190
|
+
}
|
|
191
|
+
} else if (record.kind === "complete") {
|
|
192
|
+
seed.complete.attempts++;
|
|
193
|
+
if (isError) seed.complete.failures++;
|
|
194
|
+
if (!isError) {
|
|
195
|
+
if (Number.isSafeInteger(record.satisfied)) seed.complete.satisfied += record.satisfied;
|
|
196
|
+
if (Number.isSafeInteger(record.unsatisfied)) seed.complete.unsatisfied += record.unsatisfied;
|
|
197
|
+
if (Number.isSafeInteger(record.insufficient)) seed.complete.insufficient += record.insufficient;
|
|
198
|
+
if (record.steered === true) seed.complete.steered++;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
seed.check.run = run;
|
|
203
|
+
seed.check.severity = severityOf(run, thresholds);
|
|
204
|
+
seed.log = ordered.slice(-maxLog).map((record) => {
|
|
205
|
+
const rest = { ...record };
|
|
206
|
+
delete rest.sessionId;
|
|
207
|
+
return rest;
|
|
208
|
+
});
|
|
209
|
+
return seed;
|
|
210
|
+
}
|
|
211
|
+
async function reconstructSessionSeed(sessionId, { dir, port, trendConfig, maxLog, readStream = createReadableStream } = {}) {
|
|
212
|
+
if (typeof sessionId !== "string" || !sessionId.trim()) return void 0;
|
|
213
|
+
const records = [];
|
|
214
|
+
for await (const record of readStream({ dir, port })) {
|
|
215
|
+
if (!isObject(record)) continue;
|
|
216
|
+
if (record.sessionId === sessionId) records.push(record);
|
|
217
|
+
}
|
|
218
|
+
if (records.length === 0) return void 0;
|
|
219
|
+
const { decisions } = foldSession(records);
|
|
220
|
+
if (decisions.size === 0) return void 0;
|
|
221
|
+
return buildSeed(decisions, { maxLog, trendConfig });
|
|
222
|
+
}
|
|
223
|
+
async function listSessionDecisions(sessionId, { dir, port, page = 1, pageSize = 100, readStream = createReadableStream } = {}) {
|
|
224
|
+
if (typeof sessionId !== "string" || !sessionId.trim()) throw new Error("Invalid session id");
|
|
225
|
+
const size = Number.isSafeInteger(pageSize) && pageSize > 0 && pageSize <= 500 ? pageSize : 100;
|
|
226
|
+
const records = [];
|
|
227
|
+
for await (const record of readStream({ dir, port })) {
|
|
228
|
+
if (!isObject(record)) continue;
|
|
229
|
+
if (record.sessionId === sessionId) records.push(record);
|
|
230
|
+
}
|
|
231
|
+
const { decisions } = foldSession(records);
|
|
232
|
+
const rows = [...decisions.values()].sort((a, b) => atOf(b) - atOf(a));
|
|
233
|
+
const total = rows.length;
|
|
234
|
+
const pages = Math.max(1, Math.ceil(total / size));
|
|
235
|
+
const current = Math.min(Math.max(1, Number.isSafeInteger(page) ? page : 1), pages);
|
|
236
|
+
const start = (current - 1) * size;
|
|
237
|
+
const entries = rows.slice(start, start + size).map((record) => {
|
|
238
|
+
const rest = { ...record };
|
|
239
|
+
delete rest.sessionId;
|
|
240
|
+
return rest;
|
|
241
|
+
});
|
|
242
|
+
return { entries, total, page: current, pageSize: size, pages };
|
|
243
|
+
}
|
|
244
|
+
export {
|
|
245
|
+
analyzeSession,
|
|
246
|
+
analyzeSessionLogs,
|
|
247
|
+
annotationSummary,
|
|
248
|
+
backtestTrend,
|
|
249
|
+
buildProfile,
|
|
250
|
+
buildSeed,
|
|
251
|
+
foldSession,
|
|
252
|
+
listSessionDecisions,
|
|
253
|
+
reconstructSessionSeed
|
|
254
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { logsDir } from "./log-store.js";
|
|
6
|
+
async function* createReadableStream({ dir = logsDir(), port } = {}) {
|
|
7
|
+
const prefix = Number.isInteger(port) && port > 0 ? `decisions-${port}-` : "decisions-";
|
|
8
|
+
let names;
|
|
9
|
+
try {
|
|
10
|
+
names = await readdir(dir);
|
|
11
|
+
} catch {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const files = names.filter((name) => name.startsWith(prefix) && name.endsWith(".jsonl")).sort();
|
|
15
|
+
for (const name of files) {
|
|
16
|
+
const stream = createReadStream(join(dir, name), { encoding: "utf8" });
|
|
17
|
+
const lines = createInterface({ input: stream, crlfDelay: Infinity });
|
|
18
|
+
try {
|
|
19
|
+
for await (const line of lines) {
|
|
20
|
+
if (!line.trim()) continue;
|
|
21
|
+
try {
|
|
22
|
+
yield JSON.parse(line);
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} finally {
|
|
27
|
+
lines.close();
|
|
28
|
+
stream.close();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
createReadableStream
|
|
34
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function logsDir(): string;
|
|
2
|
+
export type LogStoreOptions = {
|
|
3
|
+
port?: number;
|
|
4
|
+
dir?: string;
|
|
5
|
+
flushIntervalMs?: number;
|
|
6
|
+
maxBuffer?: number;
|
|
7
|
+
retentionDays?: number;
|
|
8
|
+
now?: () => Date;
|
|
9
|
+
};
|
|
10
|
+
export declare function createLogStore({ port, dir, flushIntervalMs, maxBuffer, retentionDays, now }?: LogStoreOptions): {
|
|
11
|
+
append(entry: unknown): void;
|
|
12
|
+
appendAnnotation(entry: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
13
|
+
flush: () => Promise<void>;
|
|
14
|
+
dispose(): Promise<void>;
|
|
15
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { mkdir, appendFile, readdir, rm } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
function logsDir() {
|
|
5
|
+
return join(homedir(), ".config", "dsh-decision-layer", "logs");
|
|
6
|
+
}
|
|
7
|
+
const dayStamp = (date) => date.toISOString().slice(0, 10);
|
|
8
|
+
function createLogStore({
|
|
9
|
+
port,
|
|
10
|
+
dir = logsDir(),
|
|
11
|
+
flushIntervalMs = 5e3,
|
|
12
|
+
maxBuffer = 200,
|
|
13
|
+
retentionDays = 30,
|
|
14
|
+
now = () => /* @__PURE__ */ new Date()
|
|
15
|
+
} = {}) {
|
|
16
|
+
const safePort = Number.isInteger(port) && port > 0 && port < 65536 ? port : 0;
|
|
17
|
+
const prefix = `decisions-${safePort}-`;
|
|
18
|
+
let buffer = [];
|
|
19
|
+
let timer;
|
|
20
|
+
let flushing = false;
|
|
21
|
+
const fileFor = (date) => join(dir, `${prefix}${dayStamp(date)}.jsonl`);
|
|
22
|
+
const prune = async () => {
|
|
23
|
+
let names;
|
|
24
|
+
try {
|
|
25
|
+
names = await readdir(dir);
|
|
26
|
+
} catch {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const cutoff = now().getTime() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
30
|
+
await Promise.all(names.map(async (name) => {
|
|
31
|
+
if (!name.startsWith(prefix) || !name.endsWith(".jsonl")) return;
|
|
32
|
+
const stamp = name.slice(prefix.length, -".jsonl".length);
|
|
33
|
+
const day = Date.parse(`${stamp}T00:00:00Z`);
|
|
34
|
+
if (Number.isFinite(day) && day < cutoff) await rm(join(dir, name), { force: true }).catch(() => {
|
|
35
|
+
});
|
|
36
|
+
}));
|
|
37
|
+
};
|
|
38
|
+
const flush = async () => {
|
|
39
|
+
if (flushing || buffer.length === 0) return;
|
|
40
|
+
flushing = true;
|
|
41
|
+
const pending = buffer;
|
|
42
|
+
buffer = [];
|
|
43
|
+
try {
|
|
44
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
45
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const entry of pending) {
|
|
47
|
+
const at = Number.isFinite(entry.at) ? entry.at : now().getTime();
|
|
48
|
+
const file = fileFor(new Date(at));
|
|
49
|
+
(byFile.get(file) ?? byFile.set(file, []).get(file)).push(`${JSON.stringify(entry)}
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
for (const [file, lines] of byFile) await appendFile(file, lines.join(""), { mode: 384 });
|
|
53
|
+
await prune();
|
|
54
|
+
} catch {
|
|
55
|
+
} finally {
|
|
56
|
+
flushing = false;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
append(entry) {
|
|
61
|
+
if (entry === null || typeof entry !== "object") return;
|
|
62
|
+
buffer.push(entry);
|
|
63
|
+
if (buffer.length >= maxBuffer) {
|
|
64
|
+
void flush();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!timer) timer = setTimeout(() => {
|
|
68
|
+
timer = void 0;
|
|
69
|
+
void flush();
|
|
70
|
+
}, flushIntervalMs);
|
|
71
|
+
},
|
|
72
|
+
// An annotation is an explicit user action, not telemetry: write it
|
|
73
|
+
// immediately (not batched) and surface failure to the caller instead of
|
|
74
|
+
// dropping it. It lands in the same JSONL directory as a `kind:"annotation"`
|
|
75
|
+
// event and is pruned with the decision logs on the next flush.
|
|
76
|
+
async appendAnnotation(entry) {
|
|
77
|
+
if (entry === null || typeof entry !== "object") throw new Error("Invalid annotation");
|
|
78
|
+
const at = Number.isFinite(entry.at) ? entry.at : now().getTime();
|
|
79
|
+
const record = { ...entry, at };
|
|
80
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
81
|
+
await appendFile(fileFor(new Date(at)), `${JSON.stringify(record)}
|
|
82
|
+
`, { mode: 384 });
|
|
83
|
+
return record;
|
|
84
|
+
},
|
|
85
|
+
flush,
|
|
86
|
+
dispose() {
|
|
87
|
+
if (timer) {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
timer = void 0;
|
|
90
|
+
}
|
|
91
|
+
return flush();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
createLogStore,
|
|
97
|
+
logsDir
|
|
98
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SessionsFace, ToolExec } from './types.js';
|
|
2
|
+
export declare function fingerprintCall(exec: ToolExec | undefined): string;
|
|
3
|
+
export declare function createLoopGuard({ threshold, maxSessions, sessions }?: {
|
|
4
|
+
threshold?: number;
|
|
5
|
+
maxSessions?: number;
|
|
6
|
+
sessions?: SessionsFace;
|
|
7
|
+
}): {
|
|
8
|
+
check(exec: ToolExec | undefined): string | undefined;
|
|
9
|
+
runLength(id: string): number;
|
|
10
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const MAX_FINGERPRINT_INPUT = 16 * 1024;
|
|
3
|
+
function fingerprintCall(exec) {
|
|
4
|
+
const name = typeof exec?.name === "string" ? exec.name : "";
|
|
5
|
+
let args = "";
|
|
6
|
+
try {
|
|
7
|
+
args = JSON.stringify(exec?.arguments ?? null);
|
|
8
|
+
} catch {
|
|
9
|
+
args = "";
|
|
10
|
+
}
|
|
11
|
+
return createHash("sha256").update(name).update("\0").update(args.slice(0, MAX_FINGERPRINT_INPUT)).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
function createLoopGuard({ threshold = 3, maxSessions = 1024, sessions } = {}) {
|
|
14
|
+
if (!Number.isSafeInteger(threshold) || threshold < 2) throw new Error("Invalid loop threshold");
|
|
15
|
+
const runs = /* @__PURE__ */ new Map();
|
|
16
|
+
const runFor = (id) => {
|
|
17
|
+
let run = runs.get(id);
|
|
18
|
+
if (!run) {
|
|
19
|
+
if (runs.size >= maxSessions) runs.delete(runs.keys().next().value);
|
|
20
|
+
run = { key: void 0, count: 0 };
|
|
21
|
+
runs.set(id, run);
|
|
22
|
+
}
|
|
23
|
+
return run;
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
check(exec) {
|
|
27
|
+
const id = exec?.agent?.session?.id;
|
|
28
|
+
if (typeof id !== "string") return void 0;
|
|
29
|
+
if (sessions) {
|
|
30
|
+
try {
|
|
31
|
+
if (sessions.snapshot(id).enabled === false) return void 0;
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const run = runFor(id);
|
|
36
|
+
const key = fingerprintCall(exec);
|
|
37
|
+
run.count = key === run.key ? run.count + 1 : 1;
|
|
38
|
+
run.key = key;
|
|
39
|
+
if (run.count >= threshold) {
|
|
40
|
+
return `Identical tool call repeated ${run.count} times in a row; blocked to break a loop`;
|
|
41
|
+
}
|
|
42
|
+
return void 0;
|
|
43
|
+
},
|
|
44
|
+
runLength(id) {
|
|
45
|
+
return runs.get(id)?.count ?? 0;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
createLoopGuard,
|
|
51
|
+
fingerprintCall
|
|
52
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Evaluator, ManagerRoute, SessionsFace } from './types.js';
|
|
2
|
+
export declare const API_PREFIX = "/plugins/dsh-decision-layer/api";
|
|
3
|
+
export type ManagerRouteDeps = {
|
|
4
|
+
path?: string;
|
|
5
|
+
sessions: SessionsFace;
|
|
6
|
+
backend: {
|
|
7
|
+
evaluate: Evaluator;
|
|
8
|
+
};
|
|
9
|
+
logStore?: {
|
|
10
|
+
appendAnnotation: (entry: Record<string, unknown>) => Promise<unknown>;
|
|
11
|
+
};
|
|
12
|
+
analyze?: (id: string) => Promise<unknown>;
|
|
13
|
+
listRows?: (id: string, options: {
|
|
14
|
+
page: number;
|
|
15
|
+
pageSize: number;
|
|
16
|
+
}) => Promise<unknown>;
|
|
17
|
+
env?: Record<string, string | undefined>;
|
|
18
|
+
bindHost?: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function createManagerRoutes({ path, sessions, backend, logStore, analyze, listRows, env, bindHost }: ManagerRouteDeps): ManagerRoute[];
|