dsh-plugin-coding-delegate 0.0.0-stage → 0.2.2

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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +920 -2
  3. package/cordis.patch.example.yml +48 -0
  4. package/cordis.patch.yml +13 -0
  5. package/dist/attestation.d.ts +25 -0
  6. package/dist/attestation.js +44 -0
  7. package/dist/containment.d.ts +33 -0
  8. package/dist/containment.js +63 -0
  9. package/dist/context-quality.d.ts +79 -0
  10. package/dist/context-quality.js +181 -0
  11. package/dist/context.d.ts +39 -0
  12. package/dist/context.js +136 -0
  13. package/dist/contracts.d.ts +174 -0
  14. package/dist/contracts.js +415 -0
  15. package/dist/declaration-egress-hook.d.ts +45 -0
  16. package/dist/declaration-egress-hook.js +125 -0
  17. package/dist/declaration-egress.d.ts +37 -0
  18. package/dist/declaration-egress.js +169 -0
  19. package/dist/delegation.d.ts +339 -0
  20. package/dist/delegation.js +933 -0
  21. package/dist/emission.d.ts +70 -0
  22. package/dist/emission.js +310 -0
  23. package/dist/guard.d.ts +162 -0
  24. package/dist/guard.js +873 -0
  25. package/dist/host-events.d.ts +15 -0
  26. package/dist/host-events.js +27 -0
  27. package/dist/index.d.ts +560 -0
  28. package/dist/index.js +1398 -0
  29. package/dist/local-classifier.d.ts +52 -0
  30. package/dist/local-classifier.js +159 -0
  31. package/dist/logging.d.ts +21 -0
  32. package/dist/logging.js +90 -0
  33. package/dist/paths.d.ts +10 -0
  34. package/dist/paths.js +83 -0
  35. package/dist/profiles.d.ts +35 -0
  36. package/dist/profiles.js +95 -0
  37. package/dist/retry-context.d.ts +52 -0
  38. package/dist/retry-context.js +97 -0
  39. package/dist/roles.d.ts +129 -0
  40. package/dist/roles.js +363 -0
  41. package/dist/routing.d.ts +35 -0
  42. package/dist/routing.js +88 -0
  43. package/dist/savings-tracker.d.ts +128 -0
  44. package/dist/savings-tracker.js +340 -0
  45. package/dist/session-events.d.ts +17 -0
  46. package/dist/session-events.js +31 -0
  47. package/dist/verification.d.ts +104 -0
  48. package/dist/verification.js +560 -0
  49. package/package.json +66 -4
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.classifyDestination = classifyDestination;
4
+ /**
5
+ * Where an outbound request will actually land, and whether that is allowed.
6
+ *
7
+ * Both halves of this decision used to be made by comparing provider IDs, and both fail in the
8
+ * documented cloud-only setup where `localProvider` and `cloudProvider` are the same id:
9
+ *
10
+ * 1. An entropy-only DLP hit forced a local reroute even when `dlpAction` was 'block', so the
11
+ * payload was pinned to the cloud it was meant to be kept away from -- and the log said it was
12
+ * pinned to the LOCAL worker.
13
+ * 2. `destination` was then inferred from `provider === localProvider`, which is true in that
14
+ * setup, so source egress classified the route as local and ALLOWED source to the cloud.
15
+ *
16
+ * The plugin cannot see the architect's endpoint, so where it cannot tell it refuses rather than
17
+ * guessing. `dlpAction` is an explicit input for the same reason: a caller that pre-computes
18
+ * "should I reroute" cannot express that 'block' was overridden, which is how half 1 stayed hidden.
19
+ *
20
+ * `dlpAction` undefined preserves the previous behaviour deliberately: entropy-only hits reroute,
21
+ * high-confidence hits do not. Only an explicit 'block' changes that, and only 'local' authorises a
22
+ * reroute for a high-confidence hit.
23
+ */
24
+ function classifyDestination(input) {
25
+ const norm = (s) => String(s || '').trim().toLowerCase();
26
+ const local = norm(input.localProvider);
27
+ const cloud = norm(input.cloudProvider);
28
+ const dest = norm(input.provider);
29
+ // No local provider, or one that IS the cloud provider, means there is nowhere local to go.
30
+ const isCloudOnly = !local || (Boolean(cloud) && local === cloud);
31
+ // 1. Is a reroute to local required, and was it authorised?
32
+ const wantsReroute = input.dlpTripped &&
33
+ (input.dlpAction === 'local' || (input.entropyOnly && input.dlpAction !== 'block'));
34
+ if (wantsReroute) {
35
+ if (isCloudOnly) {
36
+ return {
37
+ destination: 'cloud',
38
+ decision: 'block',
39
+ rerouteLocal: false,
40
+ reason: `Refusing local reroute: localProvider ('${input.localProvider || ''}') is ` +
41
+ `identical to cloudProvider or undefined. Cannot guarantee payload isolation.`,
42
+ };
43
+ }
44
+ return {
45
+ destination: 'local',
46
+ decision: 'proceed',
47
+ rerouteLocal: true,
48
+ reason: 'Rerouted to local provider due to DLP policy.',
49
+ };
50
+ }
51
+ // 2. A tripped DLP with an explicit block policy is refused outright.
52
+ if (input.dlpTripped && input.dlpAction === 'block') {
53
+ return {
54
+ destination: 'cloud',
55
+ decision: 'block',
56
+ rerouteLocal: false,
57
+ reason: 'DLP violation: dlpAction is configured to block.',
58
+ };
59
+ }
60
+ // 2b. A HIGH-CONFIDENCE hit that was not rerouted is refused, at every action setting.
61
+ //
62
+ // This clause is not optional and its absence shipped a credential leak in the first wiring attempt:
63
+ // without it, a high-confidence secret with a DISTINCT local provider and no policy fell through to
64
+ // step 3, matched `dest === local`, and returned proceed/local -- so the payload passed the gate and,
65
+ // because it was labelled local, source egress did not stop it either. `dlp-gate.test.cjs` caught it
66
+ // as "Missing expected rejection"; the reasoning that it was covered by egress did not hold.
67
+ //
68
+ // The rule is the one the original expression encoded: only an entropy-only hit, or an explicit
69
+ // 'local' action, may be rerouted. Everything else a tripped gate found is refused, because a
70
+ // medium-confidence heuristic is the only thing entitled to a soft landing.
71
+ if (input.dlpTripped && !input.entropyOnly) {
72
+ return {
73
+ destination: 'cloud',
74
+ decision: 'block',
75
+ rerouteLocal: false,
76
+ reason: 'DLP violation: a high-confidence match may only be rerouted when dlpAction is ' +
77
+ "'local'. It was not, so nothing was transmitted.",
78
+ };
79
+ }
80
+ // 3. No reroute: classify where the payload actually lands. In a cloud-only setup it is cloud
81
+ // regardless of whether the provider string happens to equal localProvider.
82
+ const effectiveDestination = !isCloudOnly && dest === local ? 'local' : 'cloud';
83
+ return {
84
+ destination: effectiveDestination,
85
+ decision: 'proceed',
86
+ rerouteLocal: false,
87
+ };
88
+ }
@@ -0,0 +1,128 @@
1
+ export interface PricingRates {
2
+ inputPerMillion: number;
3
+ inputCachedPerMillion: number;
4
+ outputPerMillion: number;
5
+ }
6
+ export type RouteType = 'ARCHITECT_CLOUD' | 'WORKER_LOCAL' | 'local' | 'cloud' | 'cloud-failover';
7
+ export interface StepUsage {
8
+ turn?: number;
9
+ route: RouteType;
10
+ model?: string;
11
+ reason?: string;
12
+ promptTokens: number;
13
+ completionTokens: number;
14
+ totalTokens: number;
15
+ cacheHitTokens?: number;
16
+ elapsedMs?: number;
17
+ /**
18
+ * What a unit decided, when this record is a delegation rather than a bare model call.
19
+ *
20
+ * Absent for anything that is not a unit verdict. Its presence is what makes a record a delegation,
21
+ * so the ledger counts delegations without parsing the routing reason.
22
+ */
23
+ outcome?: string;
24
+ /** Whether that verdict was a success. The signal that makes absorbed retries countable. */
25
+ succeeded?: boolean;
26
+ /** File bytes this unit produced -- content the architect never carried. */
27
+ bytesWritten?: number;
28
+ }
29
+ export interface TurnRecord {
30
+ turn: number;
31
+ timestamp: string;
32
+ route: RouteType;
33
+ provider: string;
34
+ model: string;
35
+ routingReason: string;
36
+ promptTokensEst: number;
37
+ completionTokensEst: number;
38
+ totalTokensEst: number;
39
+ cacheHitRateEst: number;
40
+ costUSD: number;
41
+ /**
42
+ * What these tokens would have cost at the metered tier. NOT a saving: the local GPU is a fixed cost
43
+ * this plugin neither pays for nor reduces. See the audit line in `recordUsage`.
44
+ */
45
+ cloudEquivUSD: number;
46
+ outcome?: string;
47
+ succeeded?: boolean;
48
+ bytesWritten?: number;
49
+ elapsedMs?: number;
50
+ tokensPerSecond?: number;
51
+ }
52
+ export interface LedgerSummary {
53
+ totalTurns: number;
54
+ totalTokens?: number;
55
+ localTurns: number;
56
+ cloudTurns: number;
57
+ architectTurns: number;
58
+ workerTurns: number;
59
+ failoverTurns: number;
60
+ totalLocalTokens: number;
61
+ totalCloudTokens: number;
62
+ totalSpendUSD?: number;
63
+ totalCostUSD: number;
64
+ /** Cumulative metered-tier equivalent of the delegated work. Not money saved. */
65
+ totalCloudEquivUSD: number;
66
+ /** Delegations recorded, and how many of them came back as failures. */
67
+ delegations?: number;
68
+ failedDelegations?: number;
69
+ /**
70
+ * File bytes produced by delegated work -- content that never entered the architect's context. This,
71
+ * not any dollar figure, is what the plugin claims: code and churn stay in the worker so the
72
+ * architect's window holds decisions. See `docs/experiment.md`.
73
+ */
74
+ bytesKeptOut?: number;
75
+ /**
76
+ * What this file covers. Stated in the file because four of its fields were once read as session-wide
77
+ * when the ledger only ever records delegated work.
78
+ */
79
+ scope?: string;
80
+ recentEvents?: TurnRecord[];
81
+ history: TurnRecord[];
82
+ }
83
+ export declare const PRICING: {
84
+ readonly INPUT_CACHE_HIT: number;
85
+ readonly INPUT_CACHE_MISS: number;
86
+ readonly OUTPUT_GENERATION: number;
87
+ readonly WORKER_LOCAL: 0;
88
+ };
89
+ export declare class SavingsTracker {
90
+ private ledgerPath;
91
+ private rates;
92
+ private activeTurns;
93
+ constructor(baseDir?: string, rates?: Partial<PricingRates>);
94
+ private estimateTokens;
95
+ recordUsage(usage: StepUsage): TurnRecord;
96
+ startTurn(turn: number, route: RouteType, provider: string, model: string, promptText: string, routingReason?: string): void;
97
+ updateTurnRoute(turn: number, route: RouteType, provider: string, model: string, routingReason: string): void;
98
+ accumulateChunk(turn: number, textChunk: string): void;
99
+ recordCompletionTokens(turn: number, count: number): void;
100
+ endTurn(turn: number): TurnRecord | null;
101
+ /**
102
+ * What this plugin kept out of the architect's context, and what that compounds to.
103
+ *
104
+ * The currency is context, not money. Content the worker produced never entered the architect's window,
105
+ * so it is absent from the call that produced it AND from every later call that carries that window
106
+ * forward. The second part is the whole claim: keeping it out stops it accumulating, so the figure is
107
+ * `tokensKeptOut x modelCalls` rather than `tokensKeptOut`.
108
+ *
109
+ * It is an UPPER BOUND, and it says so, because it assumes every byte produced so far rode every call
110
+ * counted. A byte produced late rode fewer. The plugin cannot do better without recording, per
111
+ * delegation, how many calls had already happened -- and an unlabelled bound is exactly the kind of
112
+ * number this project keeps having to retract.
113
+ */
114
+ contextHygiene(modelCalls: number): {
115
+ delegations: number;
116
+ failedDelegations: number;
117
+ bytesKeptOut: number;
118
+ tokensKeptOut: number;
119
+ modelCalls: number;
120
+ carriedTokens: number;
121
+ line: string;
122
+ };
123
+ /** The last ledger written or read in this process, so the summary does not re-read the file. */
124
+ private lastLedger;
125
+ /** Read the ledger from disk once, for a process that has not written one yet. */
126
+ private readLedger;
127
+ private persist;
128
+ }
@@ -0,0 +1,340 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SavingsTracker = exports.PRICING = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ /** Ledger location fallback: derive it, never hard-code a machine-specific path. */
41
+ function defaultLedgerDir() {
42
+ const dshHome = process.env.DSH_HOME;
43
+ if (dshHome && dshHome.trim())
44
+ return path.join(dshHome.trim(), 'local-router');
45
+ return path.join(os.homedir(), '.dsh', 'local-router');
46
+ }
47
+ exports.PRICING = {
48
+ INPUT_CACHE_HIT: 0.014 / 1_000_000,
49
+ INPUT_CACHE_MISS: 0.14 / 1_000_000,
50
+ OUTPUT_GENERATION: 0.28 / 1_000_000,
51
+ WORKER_LOCAL: 0,
52
+ };
53
+ class SavingsTracker {
54
+ ledgerPath;
55
+ rates;
56
+ activeTurns = new Map();
57
+ constructor(baseDir = defaultLedgerDir(), rates) {
58
+ this.ledgerPath = path.join(baseDir, 'savings-ledger.json');
59
+ this.rates = {
60
+ inputPerMillion: rates?.inputPerMillion ?? 0.14,
61
+ inputCachedPerMillion: rates?.inputCachedPerMillion ?? 0.014,
62
+ outputPerMillion: rates?.outputPerMillion ?? 0.28,
63
+ };
64
+ }
65
+ estimateTokens(text) {
66
+ if (!text)
67
+ return 0;
68
+ return Math.ceil(text.length / 3.8);
69
+ }
70
+ recordUsage(usage) {
71
+ const turn = usage.turn ?? 1;
72
+ const route = usage.route || 'ARCHITECT_CLOUD';
73
+ const model = usage.model || (route === 'WORKER_LOCAL' || route === 'local' ? 'qwen/qwen3.8-27b' : 'deepseek-chat');
74
+ const reason = usage.reason || 'STEP_COMPLETION';
75
+ const promptTokens = usage.promptTokens ?? 0;
76
+ const completionTokens = usage.completionTokens ?? 0;
77
+ const totalTokens = usage.totalTokens ?? (promptTokens + completionTokens);
78
+ const cacheHitTokens = Math.min(promptTokens, usage.cacheHitTokens ?? 0);
79
+ const cacheMissTokens = Math.max(0, promptTokens - cacheHitTokens);
80
+ let costUSD = 0;
81
+ let cloudEquivUSD = 0;
82
+ const isLocal = route === 'WORKER_LOCAL' || route === 'local';
83
+ if (isLocal) {
84
+ costUSD = 0;
85
+ cloudEquivUSD = parseFloat((promptTokens * exports.PRICING.INPUT_CACHE_MISS + completionTokens * exports.PRICING.OUTPUT_GENERATION).toFixed(6));
86
+ }
87
+ else {
88
+ cloudEquivUSD = 0;
89
+ costUSD = parseFloat((cacheHitTokens * exports.PRICING.INPUT_CACHE_HIT +
90
+ cacheMissTokens * exports.PRICING.INPUT_CACHE_MISS +
91
+ completionTokens * exports.PRICING.OUTPUT_GENERATION).toFixed(6));
92
+ }
93
+ const cacheHitRateEst = promptTokens > 0 ? parseFloat((cacheHitTokens / promptTokens).toFixed(2)) : 0;
94
+ const elapsedMs = usage.elapsedMs && usage.elapsedMs > 0 ? usage.elapsedMs : undefined;
95
+ const tokensPerSecond = elapsedMs !== undefined && completionTokens > 0
96
+ ? parseFloat((completionTokens / (elapsedMs / 1000)).toFixed(1))
97
+ : undefined;
98
+ const record = {
99
+ turn,
100
+ timestamp: new Date().toISOString(),
101
+ route,
102
+ provider: isLocal ? 'lm-studio' : 'deepseek-official',
103
+ model,
104
+ routingReason: reason,
105
+ promptTokensEst: promptTokens,
106
+ completionTokensEst: completionTokens,
107
+ totalTokensEst: totalTokens,
108
+ cacheHitRateEst,
109
+ costUSD,
110
+ cloudEquivUSD,
111
+ // The verdict, when there is one. Its presence is what marks this record a delegation rather than
112
+ // a bare model call, which is how the ledger counts delegations and absorbed failures without
113
+ // having to parse the routing reason.
114
+ ...(usage.outcome !== undefined ? { outcome: usage.outcome } : {}),
115
+ ...(usage.succeeded !== undefined ? { succeeded: usage.succeeded } : {}),
116
+ ...(usage.bytesWritten !== undefined ? { bytesWritten: usage.bytesWritten } : {}),
117
+ ...(elapsedMs !== undefined ? { elapsedMs } : {}),
118
+ ...(tokensPerSecond !== undefined ? { tokensPerSecond } : {}),
119
+ };
120
+ const ledger = this.persist(record);
121
+ this.lastLedger = ledger;
122
+ const rateText = tokensPerSecond !== undefined && elapsedMs !== undefined
123
+ ? ` | Rate: ${tokensPerSecond} tok/s e2e (${completionTokens} tok in ${(elapsedMs / 1000).toFixed(2)}s)`
124
+ : '';
125
+ // 'CloudEquiv', not 'Saved'. The local GPU is a fixed cost this plugin neither pays for nor
126
+ // reduces, so nothing here is money saved: a local card does not pay for itself against a
127
+ // metered plan. This figure is what those tokens would have cost at the metered tier, which
128
+ // measures the plan's exposure rather than a saving.
129
+ const auditMsg = `[LEDGER_AUDIT] Step ${turn} [${route.toUpperCase()}] -> Model: ${model} | Reason: ${reason} | Tokens: ${totalTokens} (Prompt: ${promptTokens}, Completion: ${completionTokens}, CacheHit: ${cacheHitTokens})${rateText} | Cost: $${costUSD.toFixed(6)} | CloudEquiv: $${cloudEquivUSD.toFixed(6)} | Metered: ${ledger.totalCloudTokens} tok | Local: ${ledger.totalLocalTokens} tok | Delegations: ${ledger.delegations ?? 0} (failed ${ledger.failedDelegations ?? 0}) | Kept out: ${ledger.bytesKeptOut ?? 0} B`;
130
+ console.log(auditMsg);
131
+ return record;
132
+ }
133
+ startTurn(turn, route, provider, model, promptText, routingReason = 'STANDARD_ROUTING') {
134
+ this.activeTurns.set(turn, {
135
+ route,
136
+ provider,
137
+ model,
138
+ routingReason,
139
+ promptText,
140
+ completionText: '',
141
+ });
142
+ }
143
+ updateTurnRoute(turn, route, provider, model, routingReason) {
144
+ const active = this.activeTurns.get(turn);
145
+ if (active) {
146
+ active.route = route;
147
+ active.provider = provider;
148
+ active.model = model;
149
+ active.routingReason = routingReason;
150
+ active.completionText = '';
151
+ delete active.exactCompletionTokens;
152
+ }
153
+ }
154
+ accumulateChunk(turn, textChunk) {
155
+ const active = this.activeTurns.get(turn);
156
+ if (active && textChunk) {
157
+ active.completionText += textChunk;
158
+ }
159
+ }
160
+ recordCompletionTokens(turn, count) {
161
+ const active = this.activeTurns.get(turn);
162
+ if (active && count > 0) {
163
+ active.exactCompletionTokens = count;
164
+ }
165
+ }
166
+ endTurn(turn) {
167
+ const active = this.activeTurns.get(turn);
168
+ if (!active)
169
+ return null;
170
+ this.activeTurns.delete(turn);
171
+ const promptTokens = this.estimateTokens(active.promptText);
172
+ const completionTokens = active.exactCompletionTokens ?? this.estimateTokens(active.completionText);
173
+ const totalTokens = promptTokens + completionTokens;
174
+ return this.recordUsage({
175
+ turn,
176
+ route: active.route,
177
+ model: active.model,
178
+ reason: active.routingReason || 'TURN_COMPLETION',
179
+ promptTokens,
180
+ completionTokens,
181
+ totalTokens,
182
+ });
183
+ }
184
+ /**
185
+ * What this plugin kept out of the architect's context, and what that compounds to.
186
+ *
187
+ * The currency is context, not money. Content the worker produced never entered the architect's window,
188
+ * so it is absent from the call that produced it AND from every later call that carries that window
189
+ * forward. The second part is the whole claim: keeping it out stops it accumulating, so the figure is
190
+ * `tokensKeptOut x modelCalls` rather than `tokensKeptOut`.
191
+ *
192
+ * It is an UPPER BOUND, and it says so, because it assumes every byte produced so far rode every call
193
+ * counted. A byte produced late rode fewer. The plugin cannot do better without recording, per
194
+ * delegation, how many calls had already happened -- and an unlabelled bound is exactly the kind of
195
+ * number this project keeps having to retract.
196
+ */
197
+ contextHygiene(modelCalls) {
198
+ // Read once if this process has not written a ledger yet, so the first turn of a session does not
199
+ // report zeros while the file already holds real totals. A figure that reads zero looks like a
200
+ // measurement and is not one.
201
+ if (!this.lastLedger)
202
+ this.lastLedger = this.readLedger();
203
+ const ledger = this.lastLedger;
204
+ const delegations = ledger?.delegations ?? 0;
205
+ const failedDelegations = ledger?.failedDelegations ?? 0;
206
+ const bytesKeptOut = ledger?.bytesKeptOut ?? 0;
207
+ // The same estimator this file already applies to text.
208
+ const tokensKeptOut = Math.ceil(bytesKeptOut / 3.8);
209
+ const calls = Number.isFinite(modelCalls) && modelCalls > 0 ? Math.floor(modelCalls) : 0;
210
+ const carriedTokens = tokensKeptOut * calls;
211
+ return {
212
+ delegations,
213
+ failedDelegations,
214
+ bytesKeptOut,
215
+ tokensKeptOut,
216
+ modelCalls: calls,
217
+ carriedTokens,
218
+ line: delegations +
219
+ ' delegation(s) [cumulative], ' +
220
+ failedDelegations +
221
+ ' failed attempt(s) absorbed, ' +
222
+ bytesKeptOut +
223
+ ' B kept out (~' +
224
+ tokensKeptOut +
225
+ ' tok); over ' +
226
+ calls +
227
+ ' model call(s) an upper bound of ' +
228
+ carriedTokens +
229
+ ' token(s) never carried',
230
+ };
231
+ }
232
+ /** The last ledger written or read in this process, so the summary does not re-read the file. */
233
+ lastLedger = null;
234
+ /** Read the ledger from disk once, for a process that has not written one yet. */
235
+ readLedger() {
236
+ try {
237
+ if (!fs.existsSync(this.ledgerPath))
238
+ return null;
239
+ return JSON.parse(fs.readFileSync(this.ledgerPath, 'utf8'));
240
+ }
241
+ catch {
242
+ return null;
243
+ }
244
+ }
245
+ persist(record) {
246
+ let ledger = {
247
+ totalTurns: 0,
248
+ totalTokens: 0,
249
+ localTurns: 0,
250
+ cloudTurns: 0,
251
+ architectTurns: 0,
252
+ workerTurns: 0,
253
+ failoverTurns: 0,
254
+ totalLocalTokens: 0,
255
+ totalCloudTokens: 0,
256
+ totalSpendUSD: 0,
257
+ totalCostUSD: 0,
258
+ totalCloudEquivUSD: 0,
259
+ delegations: 0,
260
+ failedDelegations: 0,
261
+ bytesKeptOut: 0,
262
+ scope: '',
263
+ recentEvents: [],
264
+ history: [],
265
+ };
266
+ try {
267
+ if (fs.existsSync(this.ledgerPath)) {
268
+ const raw = fs.readFileSync(this.ledgerPath, 'utf8');
269
+ ledger = JSON.parse(raw);
270
+ }
271
+ }
272
+ catch { }
273
+ // Carried forward from the name this used to have. The name was wrong, not the number: the local GPU
274
+ // is a fixed cost, so nothing here is money saved. Losing the history to a rename would be its own
275
+ // small dishonesty.
276
+ if (ledger.totalCloudEquivUSD === undefined && ledger.totalSavedUSD !== undefined) {
277
+ ledger.totalCloudEquivUSD = ledger.totalSavedUSD;
278
+ }
279
+ delete ledger.totalSavedUSD;
280
+ ledger.totalTurns += 1;
281
+ ledger.totalTokens = (ledger.totalTokens || 0) + record.totalTokensEst;
282
+ if (record.route === 'WORKER_LOCAL' || record.route === 'local') {
283
+ ledger.localTurns += 1;
284
+ if (record.route === 'WORKER_LOCAL')
285
+ ledger.workerTurns = (ledger.workerTurns || 0) + 1;
286
+ ledger.totalLocalTokens += record.totalTokensEst;
287
+ ledger.totalCloudEquivUSD = parseFloat(((ledger.totalCloudEquivUSD || 0) + record.cloudEquivUSD).toFixed(6));
288
+ }
289
+ else if (record.route === 'cloud-failover') {
290
+ ledger.failoverTurns = (ledger.failoverTurns || 0) + 1;
291
+ ledger.totalCloudTokens += record.totalTokensEst;
292
+ ledger.totalCostUSD = parseFloat(((ledger.totalCostUSD || 0) + record.costUSD).toFixed(6));
293
+ ledger.totalSpendUSD = ledger.totalCostUSD;
294
+ }
295
+ else {
296
+ ledger.cloudTurns += 1;
297
+ if (record.route === 'ARCHITECT_CLOUD')
298
+ ledger.architectTurns = (ledger.architectTurns || 0) + 1;
299
+ ledger.totalCloudTokens += record.totalTokensEst;
300
+ ledger.totalCostUSD = parseFloat(((ledger.totalCostUSD || 0) + record.costUSD).toFixed(6));
301
+ ledger.totalSpendUSD = ledger.totalCostUSD;
302
+ }
303
+ // Context hygiene, not money. These count what the architect never had to carry: content the worker
304
+ // produced, and attempts that came back as failures. The compounding is the point -- what is kept out
305
+ // of the window is kept out of every later call, not only the one that produced it.
306
+ if (record.outcome !== undefined) {
307
+ ledger.delegations = (ledger.delegations || 0) + 1;
308
+ // Only a genuine failure. `succeeded === false` also covers an UNVERIFIED unit, which this plugin
309
+ // distinguishes everywhere else -- and counting those would make the "churn the architect never saw"
310
+ // figure meaningless, because an unverified edit is the common case rather than the exception.
311
+ if (record.outcome === 'UNIT_FAILED') {
312
+ ledger.failedDelegations = (ledger.failedDelegations || 0) + 1;
313
+ }
314
+ }
315
+ if (record.bytesWritten) {
316
+ ledger.bytesKeptOut = (ledger.bytesKeptOut || 0) + record.bytesWritten;
317
+ }
318
+ // Stated in the file, because four of these fields were once read as session-wide when the ledger has
319
+ // only ever recorded delegated work. A measurement whose scope is implicit will be misread.
320
+ ledger.scope =
321
+ 'cumulative across sessions; records delegated worker calls only. Architect usage is NOT recorded here ' +
322
+ '-- cloudTurns, architectTurns, totalCloudTokens and totalSpendUSD are always zero because the host ' +
323
+ 'events that would feed them do not exist or carry no usage. The architect side is measured as ' +
324
+ 'context by context-quality.ts off session/event, not as money.';
325
+ ledger.history.push(record);
326
+ if (ledger.history.length > 500) {
327
+ ledger.history = ledger.history.slice(-500);
328
+ }
329
+ ledger.recentEvents = ledger.history.slice(-50);
330
+ try {
331
+ fs.mkdirSync(path.dirname(this.ledgerPath), { recursive: true });
332
+ fs.writeFileSync(this.ledgerPath, JSON.stringify(ledger, null, 2), 'utf8');
333
+ }
334
+ catch (err) {
335
+ console.error('[SAVINGS_TRACKER] Failed to write ledger:', err);
336
+ }
337
+ return ledger;
338
+ }
339
+ }
340
+ exports.SavingsTracker = SavingsTracker;
@@ -0,0 +1,17 @@
1
+ import type { SessionEventMap } from '@deepseek-ai/dsh-session';
2
+ /**
3
+ * Events `foldContextQuality` reads. Every one of these has a `case` in that switch.
4
+ */
5
+ declare const OBSERVED_EVENT_TYPES: readonly ["turn/start", "step/start", "compaction/summary", "compaction/prune", "compaction/end", "assistant/message", "request/context"];
6
+ /** One of the observed events, carrying the HOST's payload type for it rather than `any`. */
7
+ export type ObservedSessionEvent = {
8
+ [K in (typeof OBSERVED_EVENT_TYPES)[number]]: {
9
+ type: K;
10
+ data: SessionEventMap[K];
11
+ };
12
+ }[(typeof OBSERVED_EVENT_TYPES)[number]];
13
+ /** Runtime membership for the fold's vocabulary. `ReadonlySet<string>` so a plain `string` may be tested. */
14
+ export declare const OBSERVED_EVENTS: ReadonlySet<string>;
15
+ /** Runtime membership for the traced subset, tested against the same plain `string` a firehose delivers. */
16
+ export declare const TRACED_EVENTS: ReadonlySet<string>;
17
+ export {};
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TRACED_EVENTS = exports.OBSERVED_EVENTS = void 0;
4
+ /**
5
+ * Events `foldContextQuality` reads. Every one of these has a `case` in that switch.
6
+ */
7
+ const OBSERVED_EVENT_TYPES = [
8
+ 'turn/start',
9
+ 'step/start',
10
+ 'compaction/summary',
11
+ 'compaction/prune',
12
+ 'compaction/end',
13
+ 'assistant/message',
14
+ 'request/context',
15
+ ];
16
+ /**
17
+ * The subset worth a trace line. `step/start` is folded but deliberately not traced: it fires once per
18
+ * model call, and this plugin reads its own live evidence out of that log.
19
+ */
20
+ const TRACED_EVENT_TYPES = [
21
+ 'turn/start',
22
+ 'assistant/message',
23
+ 'request/context',
24
+ 'compaction/summary',
25
+ 'compaction/prune',
26
+ 'compaction/end',
27
+ ];
28
+ /** Runtime membership for the fold's vocabulary. `ReadonlySet<string>` so a plain `string` may be tested. */
29
+ exports.OBSERVED_EVENTS = new Set(OBSERVED_EVENT_TYPES);
30
+ /** Runtime membership for the traced subset, tested against the same plain `string` a firehose delivers. */
31
+ exports.TRACED_EVENTS = new Set(TRACED_EVENT_TYPES);