@liwu1995/afu-context 0.1.6 → 0.1.11
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 +80 -51
- package/README.zh-CN.md +80 -51
- package/bin/afu.mjs +54 -7
- package/bin/host-autoload.py +89 -25
- package/bin/install-guide.mjs +25 -3
- package/bin/platform.mjs +4 -4
- package/bin/runtime.mjs +5 -1
- package/package.json +1 -1
- package/python/{afu_context_local-0.1.6-py3-none-any.whl → afu_context_local-0.1.11-py3-none-any.whl} +0 -0
- package/release.json +3 -3
- package/scripts/dsh-overlay.mjs +8 -3
- package/src/afu_host_integrations/dsh/dist/index.js +29 -4
- package/src/afu_host_integrations/pi/dist/index.js +214 -10
- package/src/afu_host_integrations/shared/runtime-client/dist/host-worker.js +4 -2
- package/src/afu_host_integrations/shared/runtime-client/dist/index.js +1 -1
- package/src/afu_host_integrations/shared/runtime-client/dist/telemetry.js +25 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { sessionEntryToContextMessages, convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { journal, sessionHash, summaryPrefix, observationId, CompactWorkerClient, HostCompactPool, bindingKey, findAppliedArtifact, normalizeContent, } from "../../shared/runtime-client/dist/index.js";
|
|
1
|
+
import { sessionEntryToContextMessages, convertToLlm, estimateTokens, getAgentDir, SettingsManager, DEFAULT_COMPACTION_SETTINGS } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { journal, sessionHash, afuTrigger, summaryPrefix, observationId, CompactWorkerClient, HostCompactPool, bindingKey, findAppliedArtifact, normalizeContent, } from "../../shared/runtime-client/dist/index.js";
|
|
3
3
|
export function createCompactPiExtension(options = {}) {
|
|
4
4
|
return function compactPi(pi) {
|
|
5
5
|
const injected = options.worker instanceof CompactWorkerClient ? options.worker : undefined;
|
|
@@ -12,9 +12,46 @@ export function createCompactPiExtension(options = {}) {
|
|
|
12
12
|
let warned = false;
|
|
13
13
|
const log = journal("pi");
|
|
14
14
|
const pending = new Map();
|
|
15
|
+
let epoch = 0;
|
|
16
|
+
let timer;
|
|
17
|
+
let automatic;
|
|
18
|
+
const fingerprint = (ctx) => bindingKey([
|
|
19
|
+
ctx.sessionManager.getSessionId(), ctx.model,
|
|
20
|
+
ctx.sessionManager.buildContextEntries(),
|
|
21
|
+
]);
|
|
22
|
+
const invalidate = () => {
|
|
23
|
+
epoch++;
|
|
24
|
+
if (timer)
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
timer = undefined;
|
|
27
|
+
};
|
|
28
|
+
pi.on("agent_start", invalidate);
|
|
29
|
+
pi.on("session_before_switch", invalidate);
|
|
30
|
+
pi.on("session_before_fork", invalidate);
|
|
31
|
+
pi.on("session_before_tree", invalidate);
|
|
32
|
+
pi.on("model_select", invalidate);
|
|
33
|
+
// Ready summaries per Pi session. They replace the summarized prefix of the
|
|
34
|
+
// very next model request (as the Codex/Claude Code gateway and DSH do),
|
|
35
|
+
// whether or not Pi's own compaction later accepts them.
|
|
36
|
+
const ready = new Map();
|
|
37
|
+
pi.on("context", async (event, ctx) => {
|
|
38
|
+
const sid = ctx.sessionManager.getSessionId();
|
|
39
|
+
const summary = ready.get(sid);
|
|
40
|
+
if (!summary)
|
|
41
|
+
return undefined;
|
|
42
|
+
const messages = applyPreparedSummary(event.messages, summary);
|
|
43
|
+
if (!messages)
|
|
44
|
+
return undefined;
|
|
45
|
+
log("summary_applied", { session: sessionHash(sid), artifact_id: summary.artifactId, phase: "host_request", removed_tokens_estimate: summary.removedTokens });
|
|
46
|
+
return { messages: messages };
|
|
47
|
+
});
|
|
15
48
|
const report = (ctx, error) => {
|
|
16
49
|
if (ctx.signal?.aborted)
|
|
17
50
|
return;
|
|
51
|
+
// Pi's own compaction declining (its keep-recent window covers the whole
|
|
52
|
+
// session) is not a model problem; the summary is still used per request.
|
|
53
|
+
if (error instanceof Error && /Nothing to compact|Already compacted/.test(error.message))
|
|
54
|
+
return;
|
|
18
55
|
log("adapter_error", { session: sessionHash(ctx.sessionManager.getSessionId()), reason: "host_model_unavailable_or_changed" });
|
|
19
56
|
if (warned || options.notifyErrors === false)
|
|
20
57
|
return;
|
|
@@ -23,7 +60,21 @@ export function createCompactPiExtension(options = {}) {
|
|
|
23
60
|
ctx.ui.notify(`compact unavailable; Pi compaction remains active: ${message}`, "warning");
|
|
24
61
|
};
|
|
25
62
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
26
|
-
|
|
63
|
+
invalidate();
|
|
64
|
+
const generation = epoch;
|
|
65
|
+
// Context size as Pi itself measures it, for the dashboard's progress to the AFU trigger.
|
|
66
|
+
let usage;
|
|
67
|
+
try {
|
|
68
|
+
usage = ctx.getContextUsage();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
usage = undefined;
|
|
72
|
+
}
|
|
73
|
+
log("request_observed", {
|
|
74
|
+
session: sessionHash(ctx.sessionManager.getSessionId()), phase: "host_turn_settled",
|
|
75
|
+
...(usage?.tokens == null ? {} : { estimated_input_tokens: usage.tokens }),
|
|
76
|
+
...afuTrigger(usage?.contextWindow),
|
|
77
|
+
});
|
|
27
78
|
const entries = ctx.sessionManager.buildContextEntries();
|
|
28
79
|
const visible = modelVisibleEntries(entries);
|
|
29
80
|
const latest = visible.at(-1);
|
|
@@ -33,7 +84,54 @@ export function createCompactPiExtension(options = {}) {
|
|
|
33
84
|
if (request === undefined)
|
|
34
85
|
return;
|
|
35
86
|
try {
|
|
36
|
-
|
|
87
|
+
const identity = fingerprint(ctx);
|
|
88
|
+
const binding = injected ? undefined : await piHostBinding(ctx, pi);
|
|
89
|
+
const prepare = () => injected ? injected.compact(request, ctx.signal) : host.compact(request, binding, ctx.signal);
|
|
90
|
+
const deadline = Date.now() + 630_000;
|
|
91
|
+
const valid = () => generation === epoch && identity === fingerprint(ctx) && !ctx.signal?.aborted;
|
|
92
|
+
const inspect = async (result) => {
|
|
93
|
+
if (!valid())
|
|
94
|
+
return;
|
|
95
|
+
if (piCompactionFromResult(visible, result, usage?.tokens ?? 0)) {
|
|
96
|
+
const prepared = preparedSummary(visible, result);
|
|
97
|
+
if (prepared)
|
|
98
|
+
ready.set(ctx.sessionManager.getSessionId(), prepared);
|
|
99
|
+
// Pi refuses to compact ("Nothing to compact") while its keep-recent
|
|
100
|
+
// window covers the whole branch, and shows that as an error. The
|
|
101
|
+
// summary is already used per request, so only commit when Pi can.
|
|
102
|
+
if (!piCanCompact(ctx, visible, options.keepRecentTokens))
|
|
103
|
+
return;
|
|
104
|
+
// The native compact API aborts an active turn. Invoke only while idle,
|
|
105
|
+
// with no queued prompt, then revalidate again in the native hook.
|
|
106
|
+
if (!ctx.isIdle?.() || ctx.hasPendingMessages?.() || automatic)
|
|
107
|
+
return;
|
|
108
|
+
if (binding && !await binding.current())
|
|
109
|
+
return;
|
|
110
|
+
if (!valid() || !ctx.isIdle() || ctx.hasPendingMessages())
|
|
111
|
+
return;
|
|
112
|
+
automatic = { result, fingerprint: identity, ...(binding ? { binding } : {}) };
|
|
113
|
+
const clear = () => { automatic = undefined; };
|
|
114
|
+
try {
|
|
115
|
+
ctx.compact({ onComplete: clear, onError: error => { clear(); report(ctx, error); } });
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
clear();
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!["scheduled", "running"].includes(result.async_job.status) || Date.now() >= deadline)
|
|
124
|
+
return;
|
|
125
|
+
timer = setTimeout(() => {
|
|
126
|
+
timer = undefined;
|
|
127
|
+
if (!valid())
|
|
128
|
+
return;
|
|
129
|
+
void prepare().then(inspect).catch(error => { if (valid())
|
|
130
|
+
report(ctx, error); });
|
|
131
|
+
}, 500);
|
|
132
|
+
timer.unref?.();
|
|
133
|
+
};
|
|
134
|
+
await inspect(await prepare());
|
|
37
135
|
warned = false;
|
|
38
136
|
}
|
|
39
137
|
catch (error) {
|
|
@@ -41,6 +139,7 @@ export function createCompactPiExtension(options = {}) {
|
|
|
41
139
|
}
|
|
42
140
|
});
|
|
43
141
|
pi.on("session_before_compact", async (event, ctx) => {
|
|
142
|
+
invalidate();
|
|
44
143
|
const started = performance.now();
|
|
45
144
|
const entries = modelVisibleEntries(ctx.sessionManager.buildContextEntries());
|
|
46
145
|
const latest = entries.at(-1);
|
|
@@ -51,7 +150,13 @@ export function createCompactPiExtension(options = {}) {
|
|
|
51
150
|
if (request === undefined)
|
|
52
151
|
return;
|
|
53
152
|
try {
|
|
54
|
-
const
|
|
153
|
+
const prepared = automatic;
|
|
154
|
+
// COSEC: an automatically requested commit must never use a stale model,
|
|
155
|
+
// session or history, nor fall back to an unrelated native summary.
|
|
156
|
+
if (prepared && ((prepared.binding && !await prepared.binding.current()) ||
|
|
157
|
+
prepared.fingerprint !== fingerprint(ctx) || event.signal.aborted))
|
|
158
|
+
return { cancel: true };
|
|
159
|
+
const result = prepared?.result ?? await compact(request, ctx, event.signal);
|
|
55
160
|
warned = false;
|
|
56
161
|
const compaction = piCompactionFromResult(entries, result, event.preparation.tokensBefore);
|
|
57
162
|
if (compaction) {
|
|
@@ -63,18 +168,21 @@ export function createCompactPiExtension(options = {}) {
|
|
|
63
168
|
pending.set(sid, { signature: bindingKey([compaction.summary, compaction.firstKeptEntryId]), artifactId });
|
|
64
169
|
log("summary_handoff", { session: sid, artifact_id: artifactId });
|
|
65
170
|
}
|
|
66
|
-
return compaction === undefined ? undefined : { compaction };
|
|
171
|
+
return compaction === undefined ? (prepared ? { cancel: true } : undefined) : { compaction };
|
|
67
172
|
}
|
|
68
173
|
catch (error) {
|
|
69
174
|
if (!event.signal.aborted)
|
|
70
175
|
report(ctx, error);
|
|
71
|
-
return undefined;
|
|
176
|
+
return automatic ? { cancel: true } : undefined;
|
|
72
177
|
}
|
|
73
178
|
finally {
|
|
74
179
|
log("host_compaction_receipt", { session: sessionHash(ctx.sessionManager.getSessionId()), host_compaction_ms: performance.now() - started });
|
|
75
180
|
}
|
|
76
181
|
});
|
|
77
182
|
pi.on("session_compact", async (event, ctx) => {
|
|
183
|
+
// Pi's history now starts from its own compaction entry; the prepared
|
|
184
|
+
// prefix can no longer match, so stop offering it.
|
|
185
|
+
ready.delete(ctx.sessionManager.getSessionId());
|
|
78
186
|
const sid = sessionHash(ctx.sessionManager.getSessionId());
|
|
79
187
|
const handoff = pending.get(sid);
|
|
80
188
|
if (event.fromExtension && handoff?.signature === bindingKey([event.compactionEntry.summary, event.compactionEntry.firstKeptEntryId]))
|
|
@@ -92,6 +200,9 @@ export function createCompactPiExtension(options = {}) {
|
|
|
92
200
|
}
|
|
93
201
|
});
|
|
94
202
|
pi.on("session_shutdown", async () => {
|
|
203
|
+
invalidate();
|
|
204
|
+
automatic = undefined;
|
|
205
|
+
ready.clear();
|
|
95
206
|
log("runtime_stopped");
|
|
96
207
|
await host.close();
|
|
97
208
|
await injected?.close();
|
|
@@ -134,10 +245,17 @@ export async function piHostBinding(ctx, pi) {
|
|
|
134
245
|
const response = await ctx.modelRegistry.complete(model, {
|
|
135
246
|
...(ids?.length ? { systemPrompt: ctx.getSystemPrompt(), ...(tools ? { tools } : {}) } : {}),
|
|
136
247
|
messages: [...selected.messages, { role: "user", content: selected.prompt, timestamp: Date.now() }],
|
|
137
|
-
}, { signal, maxTokens:
|
|
248
|
+
}, { signal, maxTokens: piSummaryMaxTokens(ctx, model), sessionId: ctx.sessionManager.getSessionId() });
|
|
138
249
|
usage = piSummaryUsage(response.usage);
|
|
139
|
-
|
|
140
|
-
|
|
250
|
+
const failure = (reason) => Object.assign(new Error("Pi summary incomplete"), { afuReason: reason });
|
|
251
|
+
if (response.stopReason === "length")
|
|
252
|
+
throw failure("summary_output_limit");
|
|
253
|
+
if (response.stopReason === "error")
|
|
254
|
+
throw failure("summary_provider_error");
|
|
255
|
+
if (response.stopReason !== "stop")
|
|
256
|
+
throw failure("summary_incomplete");
|
|
257
|
+
if (response.content.some(b => b.type === "toolCall"))
|
|
258
|
+
throw failure("summary_called_tool");
|
|
141
259
|
return response.content.filter(b => b.type === "text").map(b => b.text).join("\n");
|
|
142
260
|
},
|
|
143
261
|
};
|
|
@@ -182,6 +300,8 @@ export function piEntriesToRequest(entries, ctx, requiredIds) {
|
|
|
182
300
|
mode: "active",
|
|
183
301
|
metadata: {
|
|
184
302
|
adapter: "pi",
|
|
303
|
+
// Pi's own measurement of the whole request; the worker follows it for the trigger.
|
|
304
|
+
...(Number.isSafeInteger(usage.tokens) && usage.tokens > 0 ? { host_context_tokens: usage.tokens } : {}),
|
|
185
305
|
source_profile: {
|
|
186
306
|
representation: "text",
|
|
187
307
|
sequence_semantics: "append_only",
|
|
@@ -219,6 +339,90 @@ export function piCompactionFromResult(entries, result, tokensBefore) {
|
|
|
219
339
|
},
|
|
220
340
|
};
|
|
221
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Output budget for one summary, the way Pi sizes its own: 80% of
|
|
344
|
+
* compaction.reserveTokens (16,384 by default, so 13,107), capped by the
|
|
345
|
+
* model's maxTokens. Reasoning models spend part of it thinking; a fixed
|
|
346
|
+
* 4,096 made long summaries stop at the limit and every retry fail.
|
|
347
|
+
*/
|
|
348
|
+
export function piSummaryMaxTokens(ctx, model) {
|
|
349
|
+
let reserve = DEFAULT_COMPACTION_SETTINGS.reserveTokens;
|
|
350
|
+
try {
|
|
351
|
+
const value = SettingsManager.create(ctx.cwd, getAgentDir()).getCompactionReserveTokens();
|
|
352
|
+
if (Number.isFinite(value) && value > 0)
|
|
353
|
+
reserve = value;
|
|
354
|
+
}
|
|
355
|
+
catch { /* Pi's default */ }
|
|
356
|
+
const budget = Math.max(4096, Math.floor(0.8 * reserve));
|
|
357
|
+
return typeof model.maxTokens === "number" && model.maxTokens > 0 ? Math.min(budget, model.maxTokens) : budget;
|
|
358
|
+
}
|
|
359
|
+
/** Pi's compaction.keepRecentTokens for this project (Pi's own default when unset). */
|
|
360
|
+
function piKeepRecentTokens(ctx) {
|
|
361
|
+
try {
|
|
362
|
+
const value = SettingsManager.create(ctx.cwd, getAgentDir()).getCompactionKeepRecentTokens();
|
|
363
|
+
if (Number.isFinite(value) && value >= 0)
|
|
364
|
+
return value;
|
|
365
|
+
}
|
|
366
|
+
catch { /* fall back to Pi's default */ }
|
|
367
|
+
return DEFAULT_COMPACTION_SETTINGS.keepRecentTokens;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Whether Pi's own compaction would accept a commit now: the messages after
|
|
371
|
+
* the latest compaction must exceed keepRecentTokens (Pi's estimate), or Pi
|
|
372
|
+
* reports "Nothing to compact (session too small)".
|
|
373
|
+
*/
|
|
374
|
+
export function piCanCompact(ctx, visible, keepRecent = piKeepRecentTokens(ctx)) {
|
|
375
|
+
let total = 0;
|
|
376
|
+
for (const entry of visible) {
|
|
377
|
+
if (entry.type === "compaction") {
|
|
378
|
+
total = 0;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
for (const message of sessionEntryToContextMessages(entry))
|
|
382
|
+
total += estimateTokens(message);
|
|
383
|
+
}
|
|
384
|
+
return total > keepRecent;
|
|
385
|
+
}
|
|
386
|
+
/** A ready result that replaces a leading run of the given entries. */
|
|
387
|
+
export function preparedSummary(visible, result) {
|
|
388
|
+
const artifact = findAppliedArtifact(result);
|
|
389
|
+
if (artifact === undefined || typeof artifact.item.content !== "string")
|
|
390
|
+
return undefined;
|
|
391
|
+
const ids = visible.map((entry) => entry.id);
|
|
392
|
+
if (artifact.replacedItemIds.length === 0 || artifact.replacedItemIds.length >= ids.length)
|
|
393
|
+
return undefined;
|
|
394
|
+
if (artifact.replacedItemIds.some((id, index) => ids[index] !== id))
|
|
395
|
+
return undefined;
|
|
396
|
+
const replaced = visible.slice(0, artifact.replacedItemIds.length).flatMap((entry) => sessionEntryToContextMessages(entry));
|
|
397
|
+
const replacedChars = JSON.stringify(replaced).length;
|
|
398
|
+
// Same id the summary_started/summary_ready events carry, so the dashboard links them.
|
|
399
|
+
const artifactId = observationId(result) ?? artifact.artifactId;
|
|
400
|
+
return {
|
|
401
|
+
replaced,
|
|
402
|
+
summary: artifact.item.content,
|
|
403
|
+
...(artifactId === undefined ? {} : { artifactId }),
|
|
404
|
+
removedTokens: Math.max(0, Math.round((replacedChars - artifact.item.content.length) / 4)),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Replace the summarized prefix of a model request with Pi's own compaction
|
|
409
|
+
* summary message. Only an exact prefix match is rewritten; anything else
|
|
410
|
+
* (a later Pi compaction, a branch switch, edited history) is sent unchanged.
|
|
411
|
+
*/
|
|
412
|
+
export function applyPreparedSummary(messages, ready) {
|
|
413
|
+
const count = ready.replaced.length;
|
|
414
|
+
if (messages.length <= count)
|
|
415
|
+
return undefined;
|
|
416
|
+
for (let index = 0; index < count; index++) {
|
|
417
|
+
if (JSON.stringify(messages[index]) !== JSON.stringify(ready.replaced[index]))
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
const first = messages[0];
|
|
421
|
+
return [
|
|
422
|
+
{ role: "compactionSummary", summary: ready.summary, tokensBefore: ready.removedTokens, timestamp: first?.timestamp ?? Date.now() },
|
|
423
|
+
...messages.slice(count),
|
|
424
|
+
];
|
|
425
|
+
}
|
|
222
426
|
function modelVisibleEntries(entries) {
|
|
223
427
|
return entries.filter((entry) => sessionEntryToContextMessages(entry).length > 0);
|
|
224
428
|
}
|
|
@@ -130,10 +130,12 @@ export class HostCompactPool {
|
|
|
130
130
|
res.setHeader("Content-Type", "application/json");
|
|
131
131
|
res.end(JSON.stringify({ choices: [{ message: { content: text }, finish_reason: "stop" }] }));
|
|
132
132
|
}
|
|
133
|
-
catch {
|
|
133
|
+
catch (error) {
|
|
134
134
|
const diag = entry.binding.diagnostics;
|
|
135
|
+
// A short, fixed reason code from the binding (never provider text).
|
|
136
|
+
const code = typeof error?.afuReason === "string" ? error.afuReason : "host_summary_unavailable";
|
|
135
137
|
if (diag)
|
|
136
|
-
journal(diag.host)("summary_failed", { session: diag.session, artifact_id: artifactId, duration_ms: started === undefined ? undefined : performance.now() - started, reason: controller.signal.aborted ? "summary_cancelled" :
|
|
138
|
+
journal(diag.host)("summary_failed", { session: diag.session, artifact_id: artifactId, duration_ms: started === undefined ? undefined : performance.now() - started, reason: controller.signal.aborted ? "summary_cancelled" : code });
|
|
137
139
|
if (!res.destroyed)
|
|
138
140
|
res.writeHead(502).end('{"error":"host_summary_unavailable"}');
|
|
139
141
|
}
|
|
@@ -237,6 +237,6 @@ function isJsonValue(value) {
|
|
|
237
237
|
return isRecord(value) && Object.values(value).every(isJsonValue);
|
|
238
238
|
}
|
|
239
239
|
export { HostCompactPool, bindingKey } from "./host-worker.js";
|
|
240
|
-
export { journal, sessionHash } from "./telemetry.js";
|
|
240
|
+
export { journal, sessionHash, afuTrigger } from "./telemetry.js";
|
|
241
241
|
export { summaryPrefix } from "./prefix.js";
|
|
242
242
|
//# sourceMappingURL=index.js.map
|
|
@@ -3,15 +3,35 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { randomUUID, createHash } from 'node:crypto';
|
|
5
5
|
export const sessionHash = (value) => createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
6
|
+
/**
|
|
7
|
+
* Context window and the token count at which AFU starts a background summary,
|
|
8
|
+
* matching the worker (afu_context.local_worker): an explicit window override,
|
|
9
|
+
* then AFU_TRIGGER_RATIO, else 0.5 for windows of 1M tokens or more and 0.6 below.
|
|
10
|
+
*/
|
|
11
|
+
export function afuTrigger(hostWindow, env = process.env, _platform = process.platform) {
|
|
12
|
+
const override = env.AFU_HOST_CONTEXT_WINDOW_OVERRIDE;
|
|
13
|
+
const window = override && override !== 'auto' ? Number(override) : hostWindow;
|
|
14
|
+
if (!Number.isSafeInteger(window) || !window || window < 1)
|
|
15
|
+
return {};
|
|
16
|
+
const configured = env.AFU_TRIGGER_RATIO && env.AFU_TRIGGER_RATIO !== 'auto' ? Number(env.AFU_TRIGGER_RATIO) : NaN;
|
|
17
|
+
const fallback = window >= 1_000_000 ? 0.5 : 0.6;
|
|
18
|
+
const ratio = configured > 0 && configured < 1 ? configured : fallback;
|
|
19
|
+
return { context_window: window, threshold: Math.floor(window * ratio) };
|
|
20
|
+
}
|
|
6
21
|
const journals = new Map();
|
|
7
22
|
export function journal(host) {
|
|
8
23
|
let log = journals.get(host);
|
|
9
24
|
if (!log) {
|
|
10
|
-
|
|
11
|
-
journals.set(host,
|
|
12
|
-
|
|
25
|
+
const created = createJournal(host);
|
|
26
|
+
journals.set(host, created);
|
|
27
|
+
created('runtime_started');
|
|
28
|
+
// One runtime_stopped per process, however many times the host recycles
|
|
29
|
+
// the adapter (DSH disposes an engine per agent scope).
|
|
30
|
+
process.once('exit', () => created('runtime_stopped'));
|
|
31
|
+
log = created;
|
|
13
32
|
}
|
|
14
|
-
return
|
|
33
|
+
return (event, fields) => { if (event !== 'runtime_stopped')
|
|
34
|
+
log(event, fields); };
|
|
15
35
|
}
|
|
16
36
|
function createJournal(host) {
|
|
17
37
|
const root = join(process.env.AFU_STATE_DIR ?? join(homedir(), '.local/share/afu-context'), 'logs');
|
|
@@ -33,7 +53,7 @@ function createJournal(host) {
|
|
|
33
53
|
for (const [k, v] of Object.entries(fields)) {
|
|
34
54
|
if (['session', 'artifact_id', 'phase', 'reason'].includes(k) && typeof v === 'string' && /^[A-Za-z0-9_.:+-]{1,100}$/.test(v))
|
|
35
55
|
row[k] = v;
|
|
36
|
-
if (['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens', 'context_window', 'threshold', 'duration_ms', 'host_compaction_ms'].includes(k) && typeof v === 'number' && Number.isFinite(v) && v >= 0)
|
|
56
|
+
if (['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens', 'context_window', 'threshold', 'estimated_input_tokens', 'removed_tokens_estimate', 'duration_ms', 'host_compaction_ms'].includes(k) && typeof v === 'number' && Number.isFinite(v) && v >= 0)
|
|
37
57
|
row[k] = v;
|
|
38
58
|
if (['cache_usage_known', 'applied', 'upstream_success'].includes(k) && typeof v === 'boolean')
|
|
39
59
|
row[k] = v;
|