@martian-engineering/lossless-claw 0.8.0 → 0.8.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 +8 -0
- package/dist/index.js +19240 -0
- package/docs/configuration.md +15 -5
- package/openclaw.plugin.json +27 -3
- package/package.json +7 -6
- package/skills/lossless-claw/references/config.md +37 -0
- package/index.ts +0 -2
- package/src/assembler.ts +0 -1196
- package/src/compaction.ts +0 -1753
- package/src/db/config.ts +0 -345
- package/src/db/connection.ts +0 -151
- package/src/db/features.ts +0 -61
- package/src/db/migration.ts +0 -868
- package/src/engine.ts +0 -4486
- package/src/estimate-tokens.ts +0 -80
- package/src/expansion-auth.ts +0 -365
- package/src/expansion-policy.ts +0 -303
- package/src/expansion.ts +0 -383
- package/src/integrity.ts +0 -600
- package/src/large-files.ts +0 -546
- package/src/lcm-log.ts +0 -37
- package/src/openclaw-bridge.ts +0 -22
- package/src/plugin/index.ts +0 -2037
- package/src/plugin/lcm-command.ts +0 -1040
- package/src/plugin/lcm-doctor-apply.ts +0 -540
- package/src/plugin/lcm-doctor-cleaners.ts +0 -655
- package/src/plugin/lcm-doctor-shared.ts +0 -210
- package/src/plugin/shared-init.ts +0 -59
- package/src/prune.ts +0 -391
- package/src/retrieval.ts +0 -360
- package/src/session-patterns.ts +0 -23
- package/src/startup-banner-log.ts +0 -49
- package/src/store/compaction-telemetry-store.ts +0 -156
- package/src/store/conversation-store.ts +0 -929
- package/src/store/fts5-sanitize.ts +0 -50
- package/src/store/full-text-fallback.ts +0 -83
- package/src/store/full-text-sort.ts +0 -21
- package/src/store/index.ts +0 -39
- package/src/store/parse-utc-timestamp.ts +0 -25
- package/src/store/summary-store.ts +0 -1519
- package/src/summarize.ts +0 -1508
- package/src/tools/common.ts +0 -53
- package/src/tools/lcm-conversation-scope.ts +0 -127
- package/src/tools/lcm-describe-tool.ts +0 -245
- package/src/tools/lcm-expand-query-tool.ts +0 -1235
- package/src/tools/lcm-expand-tool.delegation.ts +0 -580
- package/src/tools/lcm-expand-tool.ts +0 -453
- package/src/tools/lcm-expansion-recursion-guard.ts +0 -373
- package/src/tools/lcm-grep-tool.ts +0 -228
- package/src/transaction-mutex.ts +0 -136
- package/src/transcript-repair.ts +0 -301
- package/src/types.ts +0 -165
package/src/estimate-tokens.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared token estimation utility.
|
|
3
|
-
*
|
|
4
|
-
* Uses code-point-aware weighting instead of `text.length / 4`:
|
|
5
|
-
* - CJK (Chinese/Japanese/Korean) characters: ~1.5 tokens/char
|
|
6
|
-
* - Emoji / Supplementary Plane: ~2 tokens/char
|
|
7
|
-
* - ASCII / Latin: ~0.25 tokens/char (≈ 4 chars/token)
|
|
8
|
-
*
|
|
9
|
-
* Why not `text.length / 4`?
|
|
10
|
-
* JavaScript `String.length` counts UTF-16 code units, not Unicode code points.
|
|
11
|
-
* CJK characters are 1 UTF-16 unit but ~1.5 tokens; emoji are 2 UTF-16 units
|
|
12
|
-
* (surrogate pairs) but ~2-4 tokens. The naive formula underestimates CJK by
|
|
13
|
-
* ~6× and emoji by ~2-4×, causing compaction to trigger far too late for
|
|
14
|
-
* non-English conversations.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/** Detect CJK code points across all relevant Unicode ranges. */
|
|
18
|
-
function isCjkCodePoint(cp: number): boolean {
|
|
19
|
-
return (
|
|
20
|
-
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK Unified Ideographs
|
|
21
|
-
(cp >= 0x3400 && cp <= 0x4dbf) || // CJK Extension A
|
|
22
|
-
(cp >= 0x20000 && cp <= 0x2a6df) || // CJK Extension B
|
|
23
|
-
(cp >= 0x2a700 && cp <= 0x2b73f) || // CJK Extension C
|
|
24
|
-
(cp >= 0x2b740 && cp <= 0x2b81f) || // CJK Extension D
|
|
25
|
-
(cp >= 0x2b820 && cp <= 0x2ceaf) || // CJK Extension E
|
|
26
|
-
(cp >= 0x2ceb0 && cp <= 0x2ebef) || // CJK Extension F
|
|
27
|
-
(cp >= 0x3000 && cp <= 0x303f) || // CJK Symbols and Punctuation
|
|
28
|
-
(cp >= 0x3040 && cp <= 0x30ff) || // Hiragana + Katakana
|
|
29
|
-
(cp >= 0xac00 && cp <= 0xd7af) || // Hangul Syllables
|
|
30
|
-
(cp >= 0xff00 && cp <= 0xffef) // Fullwidth Forms
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Estimate token cost for a single Unicode code point. */
|
|
35
|
-
function estimateCodePointTokens(cp: number): number {
|
|
36
|
-
if (isCjkCodePoint(cp)) {
|
|
37
|
-
return 1.5;
|
|
38
|
-
}
|
|
39
|
-
if (cp > 0xffff) {
|
|
40
|
-
return 2;
|
|
41
|
-
}
|
|
42
|
-
return 0.25;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Estimate text tokens using Unicode-aware character weighting. */
|
|
46
|
-
export function estimateTokens(text: string): number {
|
|
47
|
-
let tokens = 0;
|
|
48
|
-
for (const char of text) {
|
|
49
|
-
const cp = char.codePointAt(0) ?? 0;
|
|
50
|
-
tokens += estimateCodePointTokens(cp);
|
|
51
|
-
}
|
|
52
|
-
return Math.ceil(tokens);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Truncate text so the estimated token count stays within `maxTokens`.
|
|
57
|
-
*
|
|
58
|
-
* Iterates by Unicode code point to avoid splitting surrogate pairs while
|
|
59
|
-
* preserving the same weighting model as `estimateTokens()`.
|
|
60
|
-
*/
|
|
61
|
-
export function truncateTextToEstimatedTokens(text: string, maxTokens: number): string {
|
|
62
|
-
if (maxTokens <= 0 || !text) {
|
|
63
|
-
return "";
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
let tokens = 0;
|
|
67
|
-
let end = 0;
|
|
68
|
-
|
|
69
|
-
for (const char of text) {
|
|
70
|
-
const cp = char.codePointAt(0) ?? 0;
|
|
71
|
-
const nextTokens = tokens + estimateCodePointTokens(cp);
|
|
72
|
-
if (Math.ceil(nextTokens) > maxTokens) {
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
tokens = nextTokens;
|
|
76
|
-
end += char.length;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return text.slice(0, end);
|
|
80
|
-
}
|
package/src/expansion-auth.ts
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
import type { ExpansionOrchestrator, ExpansionRequest, ExpansionResult } from "./expansion.js";
|
|
2
|
-
|
|
3
|
-
// ── Types ────────────────────────────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
export type ExpansionGrant = {
|
|
6
|
-
/** Unique grant ID */
|
|
7
|
-
grantId: string;
|
|
8
|
-
/** Session ID that issued the grant */
|
|
9
|
-
issuerSessionId: string;
|
|
10
|
-
/** Conversation IDs the grantee is allowed to traverse */
|
|
11
|
-
allowedConversationIds: number[];
|
|
12
|
-
/** Specific summary IDs the grantee is allowed to expand (if empty, all within conversation are allowed) */
|
|
13
|
-
allowedSummaryIds: string[];
|
|
14
|
-
/** Maximum traversal depth */
|
|
15
|
-
maxDepth: number;
|
|
16
|
-
/** Maximum tokens the grantee can retrieve */
|
|
17
|
-
tokenCap: number;
|
|
18
|
-
/** When the grant expires */
|
|
19
|
-
expiresAt: Date;
|
|
20
|
-
/** Whether this grant has been revoked */
|
|
21
|
-
revoked: boolean;
|
|
22
|
-
/** Creation timestamp */
|
|
23
|
-
createdAt: Date;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export type CreateGrantInput = {
|
|
27
|
-
issuerSessionId: string;
|
|
28
|
-
allowedConversationIds: number[];
|
|
29
|
-
allowedSummaryIds?: string[];
|
|
30
|
-
maxDepth?: number;
|
|
31
|
-
tokenCap?: number;
|
|
32
|
-
/** TTL in milliseconds (default: 5 minutes) */
|
|
33
|
-
ttlMs?: number;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export type CreateDelegatedExpansionGrantInput = CreateGrantInput & {
|
|
37
|
-
delegatedSessionKey: string;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export type ValidationResult = {
|
|
41
|
-
valid: boolean;
|
|
42
|
-
reason?: string;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export type AuthorizedExpansionOrchestrator = {
|
|
46
|
-
expand(grantId: string, request: ExpansionRequest): Promise<ExpansionResult>;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
// ── Defaults ─────────────────────────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
const DEFAULT_MAX_DEPTH = 3;
|
|
52
|
-
const DEFAULT_TOKEN_CAP = 4000;
|
|
53
|
-
const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
54
|
-
|
|
55
|
-
// ── ExpansionAuthManager ─────────────────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
export class ExpansionAuthManager {
|
|
58
|
-
private grants: Map<string, ExpansionGrant> = new Map();
|
|
59
|
-
private consumedTokensByGrantId: Map<string, number> = new Map();
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Create a new expansion grant with the given parameters.
|
|
63
|
-
* Generates a unique grant ID and applies defaults for optional fields.
|
|
64
|
-
*/
|
|
65
|
-
createGrant(input: CreateGrantInput): ExpansionGrant {
|
|
66
|
-
const grantId = "grant_" + crypto.randomUUID().slice(0, 12);
|
|
67
|
-
const now = new Date();
|
|
68
|
-
const ttlMs = input.ttlMs ?? DEFAULT_TTL_MS;
|
|
69
|
-
|
|
70
|
-
const grant: ExpansionGrant = {
|
|
71
|
-
grantId,
|
|
72
|
-
issuerSessionId: input.issuerSessionId,
|
|
73
|
-
allowedConversationIds: input.allowedConversationIds,
|
|
74
|
-
allowedSummaryIds: input.allowedSummaryIds ?? [],
|
|
75
|
-
maxDepth: input.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
76
|
-
tokenCap: input.tokenCap ?? DEFAULT_TOKEN_CAP,
|
|
77
|
-
expiresAt: new Date(now.getTime() + ttlMs),
|
|
78
|
-
revoked: false,
|
|
79
|
-
createdAt: now,
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
this.grants.set(grantId, grant);
|
|
83
|
-
this.consumedTokensByGrantId.set(grantId, 0);
|
|
84
|
-
return grant;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Retrieve a grant by ID. Returns null if the grant does not exist,
|
|
89
|
-
* has been revoked, or has expired.
|
|
90
|
-
*/
|
|
91
|
-
getGrant(grantId: string): ExpansionGrant | null {
|
|
92
|
-
const grant = this.grants.get(grantId);
|
|
93
|
-
if (!grant) {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
if (grant.revoked) {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
if (grant.expiresAt.getTime() <= Date.now()) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
return grant;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Revoke a grant, preventing any further use.
|
|
107
|
-
* Returns true if the grant was found and revoked, false if not found.
|
|
108
|
-
*/
|
|
109
|
-
revokeGrant(grantId: string): boolean {
|
|
110
|
-
const grant = this.grants.get(grantId);
|
|
111
|
-
if (!grant) {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
grant.revoked = true;
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Resolve remaining token budget for an active grant.
|
|
120
|
-
*/
|
|
121
|
-
getRemainingTokenBudget(grantId: string): number | null {
|
|
122
|
-
const grant = this.getGrant(grantId);
|
|
123
|
-
if (!grant) {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
const consumed = Math.max(0, this.consumedTokensByGrantId.get(grantId) ?? 0);
|
|
127
|
-
return Math.max(0, Math.floor(grant.tokenCap) - consumed);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Consume token budget for a grant, clamped to the grant token cap.
|
|
132
|
-
*/
|
|
133
|
-
consumeTokenBudget(grantId: string, consumedTokens: number): number | null {
|
|
134
|
-
const grant = this.getGrant(grantId);
|
|
135
|
-
if (!grant) {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
const safeConsumed =
|
|
139
|
-
typeof consumedTokens === "number" && Number.isFinite(consumedTokens)
|
|
140
|
-
? Math.max(0, Math.floor(consumedTokens))
|
|
141
|
-
: 0;
|
|
142
|
-
const previous = Math.max(0, this.consumedTokensByGrantId.get(grantId) ?? 0);
|
|
143
|
-
const next = Math.min(Math.max(1, Math.floor(grant.tokenCap)), previous + safeConsumed);
|
|
144
|
-
this.consumedTokensByGrantId.set(grantId, next);
|
|
145
|
-
return Math.max(0, Math.floor(grant.tokenCap) - next);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Validate an expansion request against a grant.
|
|
150
|
-
* Checks existence, expiry, revocation, conversation scope, and summary scope.
|
|
151
|
-
*/
|
|
152
|
-
validateExpansion(
|
|
153
|
-
grantId: string,
|
|
154
|
-
request: {
|
|
155
|
-
conversationId: number;
|
|
156
|
-
summaryIds: string[];
|
|
157
|
-
depth: number;
|
|
158
|
-
tokenCap: number;
|
|
159
|
-
},
|
|
160
|
-
): ValidationResult {
|
|
161
|
-
const grant = this.grants.get(grantId);
|
|
162
|
-
|
|
163
|
-
// 1. Grant must exist
|
|
164
|
-
if (!grant) {
|
|
165
|
-
return { valid: false, reason: "Grant not found" };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// 2. Grant must not be revoked
|
|
169
|
-
if (grant.revoked) {
|
|
170
|
-
return { valid: false, reason: "Grant has been revoked" };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 3. Grant must not be expired
|
|
174
|
-
if (grant.expiresAt.getTime() <= Date.now()) {
|
|
175
|
-
return { valid: false, reason: "Grant has expired" };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// 4. Conversation ID must be in the allowed set
|
|
179
|
-
if (!grant.allowedConversationIds.includes(request.conversationId)) {
|
|
180
|
-
return {
|
|
181
|
-
valid: false,
|
|
182
|
-
reason: `Conversation ${request.conversationId} is not in the allowed set`,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// 5. If allowedSummaryIds is non-empty, all requested summaryIds must be allowed
|
|
187
|
-
if (grant.allowedSummaryIds.length > 0) {
|
|
188
|
-
const allowedSet = new Set(grant.allowedSummaryIds);
|
|
189
|
-
const unauthorized = request.summaryIds.filter((id) => !allowedSet.has(id));
|
|
190
|
-
if (unauthorized.length > 0) {
|
|
191
|
-
return {
|
|
192
|
-
valid: false,
|
|
193
|
-
reason: `Summary IDs not authorized: ${unauthorized.join(", ")}`,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// 6. Depth and tokenCap are enforced via clamping in wrapWithAuth, not
|
|
199
|
-
// rejected here. This allows callers to request more than the grant
|
|
200
|
-
// permits — the values will be clamped to the grant limits at execution time.
|
|
201
|
-
|
|
202
|
-
return { valid: true };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Remove all expired and revoked grants from the store.
|
|
207
|
-
* Returns the number of grants removed.
|
|
208
|
-
*/
|
|
209
|
-
cleanup(): number {
|
|
210
|
-
const now = Date.now();
|
|
211
|
-
let removed = 0;
|
|
212
|
-
|
|
213
|
-
for (const [grantId, grant] of this.grants) {
|
|
214
|
-
if (grant.revoked || grant.expiresAt.getTime() <= now) {
|
|
215
|
-
this.grants.delete(grantId);
|
|
216
|
-
this.consumedTokensByGrantId.delete(grantId);
|
|
217
|
-
removed++;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return removed;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const runtimeExpansionAuthManager = new ExpansionAuthManager();
|
|
226
|
-
const delegatedSessionGrantIds = new Map<string, string>();
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Return the singleton auth manager used by runtime delegated expansion flows.
|
|
230
|
-
*/
|
|
231
|
-
export function getRuntimeExpansionAuthManager(): ExpansionAuthManager {
|
|
232
|
-
return runtimeExpansionAuthManager;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Create a delegated expansion grant and bind it to the child session key.
|
|
237
|
-
*/
|
|
238
|
-
export function createDelegatedExpansionGrant(
|
|
239
|
-
input: CreateDelegatedExpansionGrantInput,
|
|
240
|
-
): ExpansionGrant {
|
|
241
|
-
const delegatedSessionKey = input.delegatedSessionKey.trim();
|
|
242
|
-
if (!delegatedSessionKey) {
|
|
243
|
-
throw new Error("delegatedSessionKey is required for delegated expansion grants");
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const grant = runtimeExpansionAuthManager.createGrant({
|
|
247
|
-
issuerSessionId: input.issuerSessionId,
|
|
248
|
-
allowedConversationIds: input.allowedConversationIds,
|
|
249
|
-
allowedSummaryIds: input.allowedSummaryIds,
|
|
250
|
-
maxDepth: input.maxDepth,
|
|
251
|
-
tokenCap: input.tokenCap,
|
|
252
|
-
ttlMs: input.ttlMs,
|
|
253
|
-
});
|
|
254
|
-
delegatedSessionGrantIds.set(delegatedSessionKey, grant.grantId);
|
|
255
|
-
return grant;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Resolve the delegated expansion grant id bound to a session key.
|
|
260
|
-
*/
|
|
261
|
-
export function resolveDelegatedExpansionGrantId(sessionKey: string): string | null {
|
|
262
|
-
const key = sessionKey.trim();
|
|
263
|
-
if (!key) {
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
return delegatedSessionGrantIds.get(key) ?? null;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Revoke the delegated grant bound to a session key.
|
|
271
|
-
* Optionally remove the binding after revocation.
|
|
272
|
-
*/
|
|
273
|
-
export function revokeDelegatedExpansionGrantForSession(
|
|
274
|
-
sessionKey: string,
|
|
275
|
-
opts?: { removeBinding?: boolean },
|
|
276
|
-
): boolean {
|
|
277
|
-
const key = sessionKey.trim();
|
|
278
|
-
if (!key) {
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
const grantId = delegatedSessionGrantIds.get(key);
|
|
282
|
-
if (!grantId) {
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
const didRevoke = runtimeExpansionAuthManager.revokeGrant(grantId);
|
|
286
|
-
if (opts?.removeBinding) {
|
|
287
|
-
delegatedSessionGrantIds.delete(key);
|
|
288
|
-
}
|
|
289
|
-
return didRevoke;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Remove delegated grant binding for a session key without revoking.
|
|
294
|
-
*/
|
|
295
|
-
export function removeDelegatedExpansionGrantForSession(sessionKey: string): boolean {
|
|
296
|
-
const key = sessionKey.trim();
|
|
297
|
-
if (!key) {
|
|
298
|
-
return false;
|
|
299
|
-
}
|
|
300
|
-
return delegatedSessionGrantIds.delete(key);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Test-only reset helper for delegated runtime grants.
|
|
305
|
-
*/
|
|
306
|
-
export function resetDelegatedExpansionGrantsForTests(): void {
|
|
307
|
-
delegatedSessionGrantIds.clear();
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// ── Authorized wrapper ───────────────────────────────────────────────────────
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Create a thin authorization wrapper around an ExpansionOrchestrator.
|
|
314
|
-
* The wrapper validates the grant before delegating to the underlying
|
|
315
|
-
* orchestrator.
|
|
316
|
-
*/
|
|
317
|
-
export function wrapWithAuth(
|
|
318
|
-
orchestrator: ExpansionOrchestrator,
|
|
319
|
-
authManager: ExpansionAuthManager,
|
|
320
|
-
): AuthorizedExpansionOrchestrator {
|
|
321
|
-
return {
|
|
322
|
-
async expand(grantId: string, request: ExpansionRequest): Promise<ExpansionResult> {
|
|
323
|
-
const validation = authManager.validateExpansion(grantId, {
|
|
324
|
-
conversationId: request.conversationId,
|
|
325
|
-
summaryIds: request.summaryIds,
|
|
326
|
-
depth: request.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
327
|
-
tokenCap: request.tokenCap ?? DEFAULT_TOKEN_CAP,
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
if (!validation.valid) {
|
|
331
|
-
throw new Error(`Expansion authorization failed: ${validation.reason}`);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const remainingBudget = authManager.getRemainingTokenBudget(grantId);
|
|
335
|
-
if (remainingBudget == null) {
|
|
336
|
-
throw new Error("Expansion authorization failed: Grant not found");
|
|
337
|
-
}
|
|
338
|
-
if (remainingBudget <= 0) {
|
|
339
|
-
throw new Error("Expansion authorization failed: Grant token budget exhausted");
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Clamp depth to grant maxDepth
|
|
343
|
-
const grant = authManager.getGrant(grantId);
|
|
344
|
-
const grantMaxDepth = grant?.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
345
|
-
const requestedDepth =
|
|
346
|
-
typeof request.maxDepth === "number" && Number.isFinite(request.maxDepth)
|
|
347
|
-
? Math.max(1, Math.trunc(request.maxDepth))
|
|
348
|
-
: grantMaxDepth;
|
|
349
|
-
const effectiveDepth = Math.min(requestedDepth, grantMaxDepth);
|
|
350
|
-
|
|
351
|
-
const requestedTokenCap =
|
|
352
|
-
typeof request.tokenCap === "number" && Number.isFinite(request.tokenCap)
|
|
353
|
-
? Math.max(1, Math.trunc(request.tokenCap))
|
|
354
|
-
: remainingBudget;
|
|
355
|
-
const effectiveTokenCap = Math.max(1, Math.min(requestedTokenCap, remainingBudget));
|
|
356
|
-
const result = await orchestrator.expand({
|
|
357
|
-
...request,
|
|
358
|
-
maxDepth: effectiveDepth,
|
|
359
|
-
tokenCap: effectiveTokenCap,
|
|
360
|
-
});
|
|
361
|
-
authManager.consumeTokenBudget(grantId, result.totalTokens);
|
|
362
|
-
return result;
|
|
363
|
-
},
|
|
364
|
-
};
|
|
365
|
-
}
|