context-mode 1.0.151 → 1.0.152
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/mcp.json +5 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +16 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +89 -3
- package/build/adapters/claude-code/hooks.js +2 -2
- package/build/adapters/claude-code/index.js +14 -13
- package/build/adapters/client-map.js +3 -0
- package/build/adapters/detect.js +13 -1
- package/build/adapters/gemini-cli/hooks.d.ts +10 -0
- package/build/adapters/gemini-cli/hooks.js +12 -2
- package/build/adapters/gemini-cli/index.d.ts +21 -1
- package/build/adapters/gemini-cli/index.js +37 -1
- package/build/adapters/kimi/config.d.ts +8 -0
- package/build/adapters/kimi/config.js +8 -0
- package/build/adapters/kimi/hooks.d.ts +28 -0
- package/build/adapters/kimi/hooks.js +34 -0
- package/build/adapters/kimi/index.d.ts +66 -0
- package/build/adapters/kimi/index.js +537 -0
- package/build/adapters/kimi/paths.d.ts +1 -0
- package/build/adapters/kimi/paths.js +12 -0
- package/build/adapters/kiro/hooks.js +2 -2
- package/build/adapters/openclaw/plugin.d.ts +14 -13
- package/build/adapters/openclaw/plugin.js +140 -40
- package/build/adapters/opencode/plugin.js +4 -3
- package/build/adapters/opencode/zod3tov4.js +8 -8
- package/build/adapters/pi/extension.js +9 -24
- package/build/adapters/pi/mcp-bridge.js +37 -0
- package/build/adapters/qwen-code/index.js +7 -7
- package/build/adapters/types.d.ts +39 -2
- package/build/adapters/types.js +55 -2
- package/build/cli.js +433 -25
- package/build/executor.js +6 -3
- package/build/runtime.d.ts +81 -1
- package/build/runtime.js +195 -9
- package/build/search/ctx-search-schema.d.ts +90 -0
- package/build/search/ctx-search-schema.js +135 -0
- package/build/search/unified.d.ts +12 -0
- package/build/search/unified.js +17 -2
- package/build/server.d.ts +2 -1
- package/build/server.js +378 -97
- package/build/session/analytics.d.ts +36 -13
- package/build/session/analytics.js +123 -26
- package/build/session/db.d.ts +24 -0
- package/build/session/db.js +41 -0
- package/build/session/extract.js +30 -0
- package/build/session/snapshot.js +24 -0
- package/build/store.d.ts +12 -1
- package/build/store.js +72 -20
- package/build/types.d.ts +7 -0
- package/build/util/project-dir.d.ts +19 -16
- package/build/util/project-dir.js +80 -45
- package/cli.bundle.mjs +371 -320
- package/configs/kimi/hooks.json +54 -0
- package/configs/pi/AGENTS.md +3 -85
- package/hooks/cache-heal-utils.mjs +148 -0
- package/hooks/core/formatters.mjs +26 -0
- package/hooks/core/routing.mjs +9 -1
- package/hooks/core/stdin.mjs +74 -3
- package/hooks/core/tool-naming.mjs +1 -0
- package/hooks/heal-partial-install.mjs +712 -0
- package/hooks/kimi/platform.mjs +1 -0
- package/hooks/kimi/posttooluse.mjs +72 -0
- package/hooks/kimi/precompact.mjs +80 -0
- package/hooks/kimi/pretooluse.mjs +42 -0
- package/hooks/kimi/sessionend.mjs +61 -0
- package/hooks/kimi/sessionstart.mjs +113 -0
- package/hooks/kimi/stop.mjs +61 -0
- package/hooks/kimi/userpromptsubmit.mjs +90 -0
- package/hooks/normalize-hooks.mjs +66 -12
- package/hooks/routing-block.mjs +8 -2
- package/hooks/security.bundle.mjs +1 -1
- package/hooks/session-db.bundle.mjs +6 -4
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +93 -3
- package/hooks/session-snapshot.bundle.mjs +20 -19
- package/hooks/sessionstart.mjs +64 -0
- package/insight/server.mjs +15 -3
- package/openclaw.plugin.json +16 -1
- package/package.json +1 -1
- package/scripts/heal-installed-plugins.mjs +31 -10
- package/scripts/postinstall.mjs +10 -0
- package/server.bundle.mjs +206 -157
- package/skills/ctx-index/SKILL.md +46 -0
- package/skills/ctx-search/SKILL.md +35 -0
- package/start.mjs +84 -11
- package/build/cache-heal.d.ts +0 -48
- package/build/cache-heal.js +0 -150
- package/build/concurrency/runPool.d.ts +0 -36
- package/build/concurrency/runPool.js +0 -51
- package/build/openclaw/mcp-tools.d.ts +0 -54
- package/build/openclaw/mcp-tools.js +0 -198
- package/build/openclaw/workspace-router.d.ts +0 -29
- package/build/openclaw/workspace-router.js +0 -64
- package/build/openclaw-plugin.d.ts +0 -130
- package/build/openclaw-plugin.js +0 -626
- package/build/opencode-plugin.d.ts +0 -122
- package/build/opencode-plugin.js +0 -375
- package/build/pi-extension.d.ts +0 -14
- package/build/pi-extension.js +0 -451
- package/build/routing-block.d.ts +0 -8
- package/build/routing-block.js +0 -86
- package/build/tool-naming.d.ts +0 -4
- package/build/tool-naming.js +0 -24
|
@@ -109,8 +109,19 @@ export interface RuntimeStats {
|
|
|
109
109
|
calls: Record<string, number>;
|
|
110
110
|
sessionStart: number;
|
|
111
111
|
cacheHits: number;
|
|
112
|
+
cacheMisses?: number;
|
|
112
113
|
cacheBytesSaved: number;
|
|
113
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Index observability snapshot — point-in-time view of the persistent
|
|
117
|
+
* content store. Optional input to `formatReport` so callers that don't
|
|
118
|
+
* have store access (or don't want the extra DB hit) can omit it.
|
|
119
|
+
*/
|
|
120
|
+
export interface IndexState {
|
|
121
|
+
totalChunks: number;
|
|
122
|
+
totalSources: number;
|
|
123
|
+
lastIndexedAt?: string;
|
|
124
|
+
}
|
|
114
125
|
/** Unified report combining runtime stats, DB analytics, and continuity data. */
|
|
115
126
|
export interface FullReport {
|
|
116
127
|
/** Runtime context savings (passed in, not from DB) */
|
|
@@ -133,6 +144,8 @@ export interface FullReport {
|
|
|
133
144
|
};
|
|
134
145
|
cache?: {
|
|
135
146
|
hits: number;
|
|
147
|
+
misses: number;
|
|
148
|
+
hit_rate: number;
|
|
136
149
|
bytes_saved: number;
|
|
137
150
|
ttl_hours_left: number;
|
|
138
151
|
total_with_cache: number;
|
|
@@ -595,7 +608,7 @@ export declare function detectLocaleAndTz(): {
|
|
|
595
608
|
* the section disappears cleanly on a fresh install.
|
|
596
609
|
*
|
|
597
610
|
* Math constants:
|
|
598
|
-
* Opus 4 = $15.00 per 1M input tokens (
|
|
611
|
+
* Opus 4 = $15.00 per 1M input tokens (fallback when PI_CONTEXT_MODE_PRICE_OUTPUT_PER_TOKEN not set)
|
|
599
612
|
* Sonnet 4 = $3.00 per 1M input tokens
|
|
600
613
|
* GPT-4o = $2.50 per 1M input tokens
|
|
601
614
|
* Gemini 2 = $1.25 per 1M input tokens
|
|
@@ -654,20 +667,24 @@ export declare function renderHorizontalTimeline(days: TimelineDay[], locale: st
|
|
|
654
667
|
* line ("started …") without an extra timestamp-validity check.
|
|
655
668
|
*/
|
|
656
669
|
export declare function formatLocalDateTime(ms: number, locale: string, tz: string): string;
|
|
657
|
-
/** Opus 4 input price: $15 per 1M tokens. */
|
|
658
|
-
export declare const OPUS_INPUT_PRICE_PER_TOKEN: number;
|
|
659
|
-
/** Convert a token count to a USD string at the Opus input rate. */
|
|
660
|
-
export declare function tokensToUsd(tokens: number): string;
|
|
661
670
|
/**
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
* -
|
|
669
|
-
*
|
|
671
|
+
* Per-token USD rate — resolves on every call.
|
|
672
|
+
* Dynamic when PI_CONTEXT_MODE_PRICE_OUTPUT_PER_TOKEN is set, Opus 4 input
|
|
673
|
+
* ($15 per 1M tokens) otherwise.
|
|
674
|
+
*/
|
|
675
|
+
export declare function pricePerToken(): number;
|
|
676
|
+
/**
|
|
677
|
+
* Back-compat alias for the original Opus-rate const (PR #401 architect
|
|
678
|
+
* P1.1 — single source of truth). Kept as a literal so any third-party
|
|
679
|
+
* consumer importing the named constant still resolves to the same
|
|
680
|
+
* fallback rate. New code should call pricePerToken() to pick up the
|
|
681
|
+
* dynamic Pi env override.
|
|
682
|
+
*
|
|
683
|
+
* @deprecated Use pricePerToken() to honor PI_CONTEXT_MODE_PRICE_OUTPUT_PER_TOKEN.
|
|
670
684
|
*/
|
|
685
|
+
export declare const OPUS_INPUT_PRICE_PER_TOKEN: number;
|
|
686
|
+
/** Convert a token count to a USD string at the current per-token rate. */
|
|
687
|
+
export declare function tokensToUsd(tokens: number): string;
|
|
671
688
|
export declare function formatReport(report: FullReport, version?: string, latestVersion?: string | null, opts?: {
|
|
672
689
|
lifetime?: LifetimeStats;
|
|
673
690
|
mcpUsage?: McpToolUsageRow[];
|
|
@@ -700,6 +717,12 @@ export declare function formatReport(report: FullReport, version?: string, lates
|
|
|
700
717
|
* single-adapter renderer output unchanged.
|
|
701
718
|
*/
|
|
702
719
|
multiAdapter?: MultiAdapterLifetimeStats;
|
|
720
|
+
/**
|
|
721
|
+
* Point-in-time snapshot of the persistent content store. Optional —
|
|
722
|
+
* callers that don't have store access can omit it and the renderer
|
|
723
|
+
* skips the observability section gracefully.
|
|
724
|
+
*/
|
|
725
|
+
indexState?: IndexState;
|
|
703
726
|
/**
|
|
704
727
|
* 5-section narrative renderer overrides. Defaults to ambient
|
|
705
728
|
* `process.cwd()` + `Date.now()` + `detectLocaleAndTz()` for production
|
|
@@ -45,7 +45,8 @@ export const categoryLabels = {
|
|
|
45
45
|
// Configuration & intent
|
|
46
46
|
rule: "Project rules (CLAUDE.md)",
|
|
47
47
|
prompt: "Your requests saved",
|
|
48
|
-
intent: "Session
|
|
48
|
+
intent: "Session intent",
|
|
49
|
+
goal: "Session goal",
|
|
49
50
|
role: "Behavior rules",
|
|
50
51
|
constraint: "Constraints you set",
|
|
51
52
|
// Tools & delegation
|
|
@@ -198,7 +199,8 @@ export class AnalyticsEngine {
|
|
|
198
199
|
let median = null;
|
|
199
200
|
let max = null;
|
|
200
201
|
if (b.concurrencies.length > 0) {
|
|
201
|
-
|
|
202
|
+
b.concurrencies.sort((a, c) => a - c);
|
|
203
|
+
const sorted = b.concurrencies;
|
|
202
204
|
const mid = Math.floor(sorted.length / 2);
|
|
203
205
|
median = sorted.length % 2 === 0
|
|
204
206
|
? (sorted[mid - 1] + sorted[mid]) / 2
|
|
@@ -252,12 +254,21 @@ export class AnalyticsEngine {
|
|
|
252
254
|
const uptimeMin = (uptimeMs / 60_000).toFixed(1);
|
|
253
255
|
// ── Cache ──
|
|
254
256
|
let cache;
|
|
255
|
-
|
|
257
|
+
const cacheMisses = runtimeStats.cacheMisses ?? 0;
|
|
258
|
+
if (runtimeStats.cacheHits > 0 || runtimeStats.cacheBytesSaved > 0 || cacheMisses > 0) {
|
|
256
259
|
const totalWithCache = totalProcessed + runtimeStats.cacheBytesSaved;
|
|
257
260
|
const totalSavingsRatio = totalWithCache / Math.max(totalBytesReturned, 1);
|
|
258
261
|
const ttlHoursLeft = Math.max(0, 24 - Math.floor((Date.now() - runtimeStats.sessionStart) / (60 * 60 * 1000)));
|
|
262
|
+
// hit_rate is the nominal cache effectiveness — the metric ctx_stats
|
|
263
|
+
// historically inferred-only by diffing tokens_saved snapshots. When
|
|
264
|
+
// there is no activity we report 0 instead of NaN/undefined so the
|
|
265
|
+
// renderer stays JSON-safe.
|
|
266
|
+
const totalLookups = runtimeStats.cacheHits + cacheMisses;
|
|
267
|
+
const hitRate = totalLookups > 0 ? runtimeStats.cacheHits / totalLookups : 0;
|
|
259
268
|
cache = {
|
|
260
269
|
hits: runtimeStats.cacheHits,
|
|
270
|
+
misses: cacheMisses,
|
|
271
|
+
hit_rate: hitRate,
|
|
261
272
|
bytes_saved: runtimeStats.cacheBytesSaved,
|
|
262
273
|
ttl_hours_left: ttlHoursLeft,
|
|
263
274
|
total_with_cache: totalWithCache,
|
|
@@ -1382,7 +1393,7 @@ function shortPath(abs) {
|
|
|
1382
1393
|
* the section disappears cleanly on a fresh install.
|
|
1383
1394
|
*
|
|
1384
1395
|
* Math constants:
|
|
1385
|
-
* Opus 4 = $15.00 per 1M input tokens (
|
|
1396
|
+
* Opus 4 = $15.00 per 1M input tokens (fallback when PI_CONTEXT_MODE_PRICE_OUTPUT_PER_TOKEN not set)
|
|
1386
1397
|
* Sonnet 4 = $3.00 per 1M input tokens
|
|
1387
1398
|
* GPT-4o = $2.50 per 1M input tokens
|
|
1388
1399
|
* Gemini 2 = $1.25 per 1M input tokens
|
|
@@ -1395,38 +1406,53 @@ function shortPath(abs) {
|
|
|
1395
1406
|
export function renderCostExample(lifetimeBytes, lifetimeTokens, lifetimeDays) {
|
|
1396
1407
|
if (!Number.isFinite(lifetimeTokens) || lifetimeTokens <= 0)
|
|
1397
1408
|
return [];
|
|
1398
|
-
const
|
|
1409
|
+
const lifetimeUsd = lifetimeTokens * pricePerToken();
|
|
1399
1410
|
const usdStr = (n, dp = 2) => n.toFixed(dp);
|
|
1400
1411
|
// Comparison units — kept locally so they're easy to tune without touching
|
|
1401
1412
|
// the renderer logic. Cursor Pro & Claude Max are public list prices; the
|
|
1402
1413
|
// weekend constant is an intentional approximation calibrated to make
|
|
1403
1414
|
// $1399.73 → "19 weekends" line up with the demo target.
|
|
1404
|
-
const cursorMonths = Math.round(
|
|
1405
|
-
const claudeMaxMonths = (
|
|
1406
|
-
const weekendCount = Math.round(
|
|
1407
|
-
const teamUsd = Math.round(
|
|
1415
|
+
const cursorMonths = Math.round(lifetimeUsd / 20);
|
|
1416
|
+
const claudeMaxMonths = (lifetimeUsd / 200).toFixed(1);
|
|
1417
|
+
const weekendCount = Math.round(lifetimeUsd / 73.67);
|
|
1418
|
+
const teamUsd = Math.round(lifetimeUsd * 10);
|
|
1408
1419
|
const teamYearUsd = lifetimeDays > 0
|
|
1409
|
-
? Math.round((
|
|
1420
|
+
? Math.round((lifetimeUsd * 10) / lifetimeDays * 365)
|
|
1410
1421
|
: 0;
|
|
1411
1422
|
// Alternate-model scale row — same token count, different per-1M rates.
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1423
|
+
// (Kept for internal reference but unreachable per Mert directive.)
|
|
1424
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1425
|
+
const _sonnetUsd = ((lifetimeTokens * 3.0) / 1_000_000).toFixed(2);
|
|
1426
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1427
|
+
const _gpt4oUsd = ((lifetimeTokens * 2.5) / 1_000_000).toFixed(2);
|
|
1428
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1429
|
+
const _geminiUsd = ((lifetimeTokens * 1.25) / 1_000_000).toFixed(2);
|
|
1430
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1431
|
+
const _haikuUsd = ((lifetimeTokens * 0.8) / 1_000_000).toFixed(2);
|
|
1432
|
+
const usingDynamicPrice = process.env.PI_CONTEXT_MODE_PRICE_OUTPUT_PER_TOKEN !== undefined;
|
|
1433
|
+
const modelId = process.env.PI_CONTEXT_MODE_MODEL_ID;
|
|
1416
1434
|
// Mert: "daha marketing ve business value e vermeli, math hesaplamalari ile
|
|
1417
|
-
// kalabalik yapma" — collapse the old 4-block render
|
|
1418
|
-
//
|
|
1419
|
-
// headline number, ONE relatable comparison, ONE team-scale callout. Drop
|
|
1420
|
-
// the alternate-model scaling row (engineer-curiosity, not value framing).
|
|
1435
|
+
// kalabalik yapma" — collapse the old 4-block render into ONE headline
|
|
1436
|
+
// number, ONE relatable comparison, ONE team-scale callout.
|
|
1421
1437
|
const out = [];
|
|
1422
|
-
|
|
1438
|
+
if (usingDynamicPrice && modelId) {
|
|
1439
|
+
out.push(` $${usdStr(lifetimeUsd)} of ${modelId} tokens your team didn't burn.`);
|
|
1440
|
+
}
|
|
1441
|
+
else if (usingDynamicPrice) {
|
|
1442
|
+
out.push(` $${usdStr(lifetimeUsd)} of tokens your team didn't burn.`);
|
|
1443
|
+
}
|
|
1444
|
+
else {
|
|
1445
|
+
out.push(` $${usdStr(lifetimeUsd)} of Opus 4 tokens your team didn't burn.`);
|
|
1446
|
+
}
|
|
1423
1447
|
out.push(` context-mode kept ${kb(lifetimeBytes)} out of context — that's ${cursorMonths} months of Cursor Pro paid for itself.`);
|
|
1424
1448
|
if (teamUsd > 0 && teamYearUsd > 0) {
|
|
1425
1449
|
out.push("");
|
|
1426
1450
|
out.push(` Scale across a 10-dev team and that's ~$${teamYearUsd.toLocaleString("en-US")}/year saved.`);
|
|
1427
1451
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1452
|
+
if (!usingDynamicPrice) {
|
|
1453
|
+
out.push("");
|
|
1454
|
+
out.push(` (Opus rates shown for context. On cheaper models the dollar number drops; the savings ratio holds.)`);
|
|
1455
|
+
}
|
|
1430
1456
|
return out;
|
|
1431
1457
|
}
|
|
1432
1458
|
/**
|
|
@@ -1829,12 +1855,46 @@ function fmtNum(n) {
|
|
|
1829
1855
|
// ─────────────────────────────────────────────────────────
|
|
1830
1856
|
// Pricing (Bug #6) — Anthropic Opus input rate
|
|
1831
1857
|
// ─────────────────────────────────────────────────────────
|
|
1832
|
-
|
|
1858
|
+
// ── Pricing (Bug #6) — per-token USD rate ─────────────────
|
|
1859
|
+
// Reads PI_CONTEXT_MODE_PRICE_OUTPUT_PER_TOKEN when set by a Pi host;
|
|
1860
|
+
// falls back to the Opus 4 input rate ($15/1M) for all other adapters.
|
|
1861
|
+
//
|
|
1862
|
+
// IMPORTANT: this is a FUNCTION, not a const. Pi sets the env var
|
|
1863
|
+
// AFTER the MCP server has been imported (the bridge spawns the server
|
|
1864
|
+
// child, then the child reads its own env on every render). A
|
|
1865
|
+
// module-load-time const would freeze to the fallback because
|
|
1866
|
+
// process.env.PI_CONTEXT_MODE_PRICE_OUTPUT_PER_TOKEN is unset at
|
|
1867
|
+
// import time. Resolving on every call keeps the dynamic-pricing
|
|
1868
|
+
// contract honest — the env var works without an MCP restart.
|
|
1869
|
+
// (Reverted module-load const semantics, PR #741 follow-up.)
|
|
1870
|
+
/**
|
|
1871
|
+
* Per-token USD rate — resolves on every call.
|
|
1872
|
+
* Dynamic when PI_CONTEXT_MODE_PRICE_OUTPUT_PER_TOKEN is set, Opus 4 input
|
|
1873
|
+
* ($15 per 1M tokens) otherwise.
|
|
1874
|
+
*/
|
|
1875
|
+
export function pricePerToken() {
|
|
1876
|
+
const env = process.env.PI_CONTEXT_MODE_PRICE_OUTPUT_PER_TOKEN;
|
|
1877
|
+
if (env !== undefined && env !== "") {
|
|
1878
|
+
const parsed = Number(env);
|
|
1879
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
1880
|
+
return parsed;
|
|
1881
|
+
}
|
|
1882
|
+
return 15 / 1_000_000; // Opus 4 input fallback
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Back-compat alias for the original Opus-rate const (PR #401 architect
|
|
1886
|
+
* P1.1 — single source of truth). Kept as a literal so any third-party
|
|
1887
|
+
* consumer importing the named constant still resolves to the same
|
|
1888
|
+
* fallback rate. New code should call pricePerToken() to pick up the
|
|
1889
|
+
* dynamic Pi env override.
|
|
1890
|
+
*
|
|
1891
|
+
* @deprecated Use pricePerToken() to honor PI_CONTEXT_MODE_PRICE_OUTPUT_PER_TOKEN.
|
|
1892
|
+
*/
|
|
1833
1893
|
export const OPUS_INPUT_PRICE_PER_TOKEN = 15 / 1_000_000;
|
|
1834
|
-
/** Convert a token count to a USD string at the
|
|
1894
|
+
/** Convert a token count to a USD string at the current per-token rate. */
|
|
1835
1895
|
export function tokensToUsd(tokens) {
|
|
1836
1896
|
const safe = Number.isFinite(tokens) && tokens > 0 ? tokens : 0;
|
|
1837
|
-
return `$${(safe *
|
|
1897
|
+
return `$${(safe * pricePerToken()).toFixed(2)}`;
|
|
1838
1898
|
}
|
|
1839
1899
|
/**
|
|
1840
1900
|
* Build a proportional bar using █ chars, scaled to a fixed width.
|
|
@@ -2043,8 +2103,10 @@ function renderConversation(c, conversationUsd, contribPct) {
|
|
|
2043
2103
|
function renderMultiAdapter(multiAdapter) {
|
|
2044
2104
|
if (!multiAdapter)
|
|
2045
2105
|
return [];
|
|
2046
|
-
const real =
|
|
2047
|
-
const skipped =
|
|
2106
|
+
const real = [];
|
|
2107
|
+
const skipped = [];
|
|
2108
|
+
for (const a of multiAdapter.perAdapter)
|
|
2109
|
+
(a.isReal ? real : skipped).push(a);
|
|
2048
2110
|
if (real.length === 0 && skipped.length === 0)
|
|
2049
2111
|
return [];
|
|
2050
2112
|
const out = [];
|
|
@@ -2090,6 +2152,34 @@ function renderMultiAdapter(multiAdapter) {
|
|
|
2090
2152
|
* - Project memory: category bars showing persistent data across sessions
|
|
2091
2153
|
* - No: Pct column, category tables, tips, jargon
|
|
2092
2154
|
*/
|
|
2155
|
+
/**
|
|
2156
|
+
* Render the machine-readable Observability block (cache + index state).
|
|
2157
|
+
*
|
|
2158
|
+
* Returns an empty array when no observability data is available, so callers
|
|
2159
|
+
* can `lines.push(...renderObservabilityBlock(...))` unconditionally and the
|
|
2160
|
+
* section is omitted when the kit has nothing to report.
|
|
2161
|
+
*
|
|
2162
|
+
* Shared between the narrative path (early-returns when conversation.events > 0)
|
|
2163
|
+
* and the legacy path so the block surfaces in both, matching the contract
|
|
2164
|
+
* that the handler always passes `indexState` regardless of code path.
|
|
2165
|
+
*/
|
|
2166
|
+
function renderObservabilityBlock(report, indexState) {
|
|
2167
|
+
const obs = [];
|
|
2168
|
+
if (report.cache) {
|
|
2169
|
+
const hitRatePct = (report.cache.hit_rate * 100).toFixed(1);
|
|
2170
|
+
obs.push(`cache.hit_rate: ${hitRatePct}% (${report.cache.hits} hits / ${report.cache.misses} misses)`);
|
|
2171
|
+
}
|
|
2172
|
+
if (indexState) {
|
|
2173
|
+
obs.push(`index.total_chunks: ${indexState.totalChunks}`);
|
|
2174
|
+
obs.push(`index.total_sources: ${indexState.totalSources}`);
|
|
2175
|
+
if (indexState.lastIndexedAt) {
|
|
2176
|
+
obs.push(`index.last_indexed_at: ${indexState.lastIndexedAt}`);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
if (obs.length === 0)
|
|
2180
|
+
return [];
|
|
2181
|
+
return ["", "## Observability", ...obs];
|
|
2182
|
+
}
|
|
2093
2183
|
export function formatReport(report, version, latestVersion, opts) {
|
|
2094
2184
|
const lines = [];
|
|
2095
2185
|
const duration = formatDuration(report.session.uptime_min);
|
|
@@ -2157,6 +2247,9 @@ export function formatReport(report, version, latestVersion, opts) {
|
|
|
2157
2247
|
conversation, lifetime, multiAdapter, realBytes,
|
|
2158
2248
|
cwd, locale, tz, now, version, latestVersion,
|
|
2159
2249
|
}));
|
|
2250
|
+
// Append Observability so cache.hit_rate / index.* surface in the
|
|
2251
|
+
// narrative path too (handler passes indexState regardless of path).
|
|
2252
|
+
lines.push(...renderObservabilityBlock(report, opts?.indexState));
|
|
2160
2253
|
return lines.join("\n");
|
|
2161
2254
|
}
|
|
2162
2255
|
// ── Compute real savings ──
|
|
@@ -2261,6 +2354,10 @@ export function formatReport(report, version, latestVersion, opts) {
|
|
|
2261
2354
|
lines.push(...renderAutoMemory(lifetime));
|
|
2262
2355
|
// ── Bottom line — business value framing (Bug #8) ──
|
|
2263
2356
|
lines.push(...renderBottomLine(tokensSaved, lifetime));
|
|
2357
|
+
// ── Observability — machine-readable cache + index state ──
|
|
2358
|
+
// Rendered via shared helper so the narrative path (above, early-return
|
|
2359
|
+
// when conversation.events > 0) emits the same block.
|
|
2360
|
+
lines.push(...renderObservabilityBlock(report, opts?.indexState));
|
|
2264
2361
|
// ── Footer ──
|
|
2265
2362
|
lines.push("");
|
|
2266
2363
|
const versionStr = version ? `v${version}` : "context-mode";
|
package/build/session/db.d.ts
CHANGED
|
@@ -319,6 +319,18 @@ export declare class SessionDB extends SQLiteBase {
|
|
|
319
319
|
data: string;
|
|
320
320
|
created_at: string;
|
|
321
321
|
}>;
|
|
322
|
+
/**
|
|
323
|
+
* Return the distinct list of session ids whose events were attributed
|
|
324
|
+
* to a given `project_dir`. Powers the ctx_search `project:` filter
|
|
325
|
+
* (#737) via the 2-step IN-clause strategy — ATTACH DATABASE is avoided
|
|
326
|
+
* because SQLite's WAL + ATTACH combination has known correctness
|
|
327
|
+
* trade-offs flagged in the upstream docs.
|
|
328
|
+
*
|
|
329
|
+
* Backed by the `idx_session_events_project(session_id, project_dir)`
|
|
330
|
+
* composite index, so 1000-session lookups complete in single-digit
|
|
331
|
+
* milliseconds. Best-effort: returns `[]` on any error.
|
|
332
|
+
*/
|
|
333
|
+
getSessionIdsForProject(projectDir: string): string[];
|
|
322
334
|
/**
|
|
323
335
|
* Ensure a session metadata entry exists. Idempotent (INSERT OR IGNORE).
|
|
324
336
|
* `projectDir` is the session origin directory, not per-event attribution.
|
|
@@ -393,5 +405,17 @@ export declare class SessionDB extends SQLiteBase {
|
|
|
393
405
|
* Remove sessions older than maxAgeDays. Returns the count of deleted sessions.
|
|
394
406
|
*/
|
|
395
407
|
cleanupOldSessions(maxAgeDays?: number): number;
|
|
408
|
+
/**
|
|
409
|
+
* Delete event rows whose session_id has no matching session_meta row.
|
|
410
|
+
*
|
|
411
|
+
* Orphaned events accumulate when meta rows were aged out by an older
|
|
412
|
+
* version of `cleanupOldSessions` but the matching events were left
|
|
413
|
+
* behind (or when callers wrote events without a meta upsert). The Kimi
|
|
414
|
+
* Code sessionstart hook calls this on every startup as a self-healing
|
|
415
|
+
* step; surfacing it as a SessionDB method keeps the SQL definition in
|
|
416
|
+
* one place instead of letting hook scripts reach through to
|
|
417
|
+
* `db.db.exec(...)` and re-encode schema knowledge in mjs files.
|
|
418
|
+
*/
|
|
419
|
+
pruneOrphanedEvents(): number;
|
|
396
420
|
}
|
|
397
421
|
export {};
|
package/build/session/db.js
CHANGED
|
@@ -976,6 +976,30 @@ export class SessionDB extends SQLiteBase {
|
|
|
976
976
|
return [];
|
|
977
977
|
}
|
|
978
978
|
}
|
|
979
|
+
/**
|
|
980
|
+
* Return the distinct list of session ids whose events were attributed
|
|
981
|
+
* to a given `project_dir`. Powers the ctx_search `project:` filter
|
|
982
|
+
* (#737) via the 2-step IN-clause strategy — ATTACH DATABASE is avoided
|
|
983
|
+
* because SQLite's WAL + ATTACH combination has known correctness
|
|
984
|
+
* trade-offs flagged in the upstream docs.
|
|
985
|
+
*
|
|
986
|
+
* Backed by the `idx_session_events_project(session_id, project_dir)`
|
|
987
|
+
* composite index, so 1000-session lookups complete in single-digit
|
|
988
|
+
* milliseconds. Best-effort: returns `[]` on any error.
|
|
989
|
+
*/
|
|
990
|
+
getSessionIdsForProject(projectDir) {
|
|
991
|
+
try {
|
|
992
|
+
const rows = this.db
|
|
993
|
+
.prepare(`SELECT DISTINCT session_id
|
|
994
|
+
FROM session_events
|
|
995
|
+
WHERE project_dir = ?`)
|
|
996
|
+
.all(projectDir);
|
|
997
|
+
return rows.map((r) => r.session_id);
|
|
998
|
+
}
|
|
999
|
+
catch {
|
|
1000
|
+
return [];
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
979
1003
|
// ═══════════════════════════════════════════
|
|
980
1004
|
// Meta
|
|
981
1005
|
// ═══════════════════════════════════════════
|
|
@@ -1127,4 +1151,21 @@ export class SessionDB extends SQLiteBase {
|
|
|
1127
1151
|
}
|
|
1128
1152
|
return oldSessions.length;
|
|
1129
1153
|
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Delete event rows whose session_id has no matching session_meta row.
|
|
1156
|
+
*
|
|
1157
|
+
* Orphaned events accumulate when meta rows were aged out by an older
|
|
1158
|
+
* version of `cleanupOldSessions` but the matching events were left
|
|
1159
|
+
* behind (or when callers wrote events without a meta upsert). The Kimi
|
|
1160
|
+
* Code sessionstart hook calls this on every startup as a self-healing
|
|
1161
|
+
* step; surfacing it as a SessionDB method keeps the SQL definition in
|
|
1162
|
+
* one place instead of letting hook scripts reach through to
|
|
1163
|
+
* `db.db.exec(...)` and re-encode schema knowledge in mjs files.
|
|
1164
|
+
*/
|
|
1165
|
+
pruneOrphanedEvents() {
|
|
1166
|
+
const result = this.db
|
|
1167
|
+
.prepare(`DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)`)
|
|
1168
|
+
.run();
|
|
1169
|
+
return Number(result.changes ?? 0);
|
|
1170
|
+
}
|
|
1130
1171
|
}
|
package/build/session/extract.js
CHANGED
|
@@ -932,6 +932,35 @@ function extractIntent(message) {
|
|
|
932
932
|
priority: 4,
|
|
933
933
|
}];
|
|
934
934
|
}
|
|
935
|
+
/**
|
|
936
|
+
* Category: session goal (objective).
|
|
937
|
+
*
|
|
938
|
+
* Captures the user's stated objective so it survives compaction and resume —
|
|
939
|
+
* unlike `intent`, which stores only the coarse mode (investigate/implement)
|
|
940
|
+
* and discards the goal text. Triggered by the `/goal <text>` command or an
|
|
941
|
+
* explicit `goal:` / `objective:` marker, so the FULL goal text is preserved
|
|
942
|
+
* (priority 4 = critical in the DB eviction contract) and restored at the top
|
|
943
|
+
* of the resume snapshot.
|
|
944
|
+
* Without this, a `/goal` directive is lost across compaction/resume.
|
|
945
|
+
*/
|
|
946
|
+
const GOAL_DIRECTIVE_PATTERN = /^(?:\/goal\s+|(?:goal|objective)\s*:\s*)(.+)$/is;
|
|
947
|
+
function extractGoal(message) {
|
|
948
|
+
const trimmed = message.trim();
|
|
949
|
+
if (!trimmed)
|
|
950
|
+
return [];
|
|
951
|
+
const match = trimmed.match(GOAL_DIRECTIVE_PATTERN);
|
|
952
|
+
if (!match)
|
|
953
|
+
return [];
|
|
954
|
+
const goalText = match[1].trim();
|
|
955
|
+
if (!goalText)
|
|
956
|
+
return [];
|
|
957
|
+
return [{
|
|
958
|
+
type: "goal",
|
|
959
|
+
category: "goal",
|
|
960
|
+
data: safeString(goalText),
|
|
961
|
+
priority: 4,
|
|
962
|
+
}];
|
|
963
|
+
}
|
|
935
964
|
/**
|
|
936
965
|
* Category 25: blocked-on
|
|
937
966
|
* Detect when work is blocked on something, or when a blocker is resolved.
|
|
@@ -1184,6 +1213,7 @@ export function extractUserEvents(message) {
|
|
|
1184
1213
|
events.push(...extractUserDecision(message));
|
|
1185
1214
|
events.push(...extractRole(message));
|
|
1186
1215
|
events.push(...extractIntent(message));
|
|
1216
|
+
events.push(...extractGoal(message));
|
|
1187
1217
|
events.push(...extractBlocker(message));
|
|
1188
1218
|
events.push(...extractData(message));
|
|
1189
1219
|
return events;
|
|
@@ -342,6 +342,22 @@ function buildIntentSection(intentEvents) {
|
|
|
342
342
|
const lastIntent = intentEvents[intentEvents.length - 1];
|
|
343
343
|
return ` <intent mode="${escapeXML(lastIntent.data)}"/>`;
|
|
344
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Restore the most recent stated session goal verbatim. Placed at the top of
|
|
347
|
+
* the snapshot (right after how_to_search) so the resuming LLM reads the
|
|
348
|
+
* active objective before anything else and keeps working toward it.
|
|
349
|
+
*/
|
|
350
|
+
function buildGoalSection(goalEvents) {
|
|
351
|
+
if (goalEvents.length === 0)
|
|
352
|
+
return "";
|
|
353
|
+
const lastGoal = goalEvents[goalEvents.length - 1];
|
|
354
|
+
return [
|
|
355
|
+
` <session_goal>`,
|
|
356
|
+
` The active objective for this session. Keep working toward it until it is met; do not ask the user to restate it.`,
|
|
357
|
+
` ${escapeXML(lastGoal.data)}`,
|
|
358
|
+
` </session_goal>`,
|
|
359
|
+
].join("\n");
|
|
360
|
+
}
|
|
345
361
|
/**
|
|
346
362
|
* Raw-prompt safety net (issue #535):
|
|
347
363
|
* Always surface the most recent user prompts verbatim so the next LLM
|
|
@@ -404,6 +420,7 @@ export function buildResumeSnapshot(events, opts) {
|
|
|
404
420
|
const gitEvents = [];
|
|
405
421
|
const subagentEvents = [];
|
|
406
422
|
const intentEvents = [];
|
|
423
|
+
const goalEvents = [];
|
|
407
424
|
const skillEvents = [];
|
|
408
425
|
const roleEvents = [];
|
|
409
426
|
const userPromptEvents = [];
|
|
@@ -439,6 +456,9 @@ export function buildResumeSnapshot(events, opts) {
|
|
|
439
456
|
case "intent":
|
|
440
457
|
intentEvents.push(ev);
|
|
441
458
|
break;
|
|
459
|
+
case "goal":
|
|
460
|
+
goalEvents.push(ev);
|
|
461
|
+
break;
|
|
442
462
|
case "skill":
|
|
443
463
|
skillEvents.push(ev);
|
|
444
464
|
break;
|
|
@@ -459,6 +479,10 @@ export function buildResumeSnapshot(events, opts) {
|
|
|
459
479
|
Do NOT ask the user to re-explain prior work. Search first.
|
|
460
480
|
Do NOT invent your own queries — use the ones provided.
|
|
461
481
|
</how_to_search>`);
|
|
482
|
+
// Session goal first — the objective the LLM must keep working toward.
|
|
483
|
+
const goal = buildGoalSection(goalEvents);
|
|
484
|
+
if (goal)
|
|
485
|
+
sections.push(goal);
|
|
462
486
|
const files = buildFilesSection(fileEvents, searchTool);
|
|
463
487
|
if (files)
|
|
464
488
|
sections.push(files);
|
package/build/store.d.ts
CHANGED
|
@@ -103,7 +103,7 @@ export declare class ContentStore {
|
|
|
103
103
|
search(query: string, limit?: number, source?: string, mode?: "AND" | "OR", contentType?: "code" | "prose", sourceMatchMode?: SourceMatchMode): SearchResult[];
|
|
104
104
|
searchTrigram(query: string, limit?: number, source?: string, mode?: "AND" | "OR", contentType?: "code" | "prose", sourceMatchMode?: SourceMatchMode): SearchResult[];
|
|
105
105
|
fuzzyCorrect(query: string): string | null;
|
|
106
|
-
searchWithFallback(query: string, limit?: number, source?: string, contentType?: "code" | "prose", sourceMatchMode?: SourceMatchMode): SearchResult[];
|
|
106
|
+
searchWithFallback(query: string, limit?: number, source?: string, contentType?: "code" | "prose", sourceMatchMode?: SourceMatchMode, sessionIdAllowSet?: Set<string>): SearchResult[];
|
|
107
107
|
/** Number of sources auto-refreshed in the last searchWithFallback call. */
|
|
108
108
|
lastRefreshCount: number;
|
|
109
109
|
getSourceMeta(label: string): {
|
|
@@ -118,6 +118,17 @@ export declare class ContentStore {
|
|
|
118
118
|
label: string;
|
|
119
119
|
chunkCount: number;
|
|
120
120
|
}>;
|
|
121
|
+
/**
|
|
122
|
+
* Aggregate snapshot of the persistent content store. Returns total
|
|
123
|
+
* chunk count, source count, and the most recent indexed_at timestamp.
|
|
124
|
+
* Used by ctx_stats so callers can see observability state in the same
|
|
125
|
+
* round trip instead of inferring it from snapshot diffs.
|
|
126
|
+
*/
|
|
127
|
+
getIndexState(): {
|
|
128
|
+
totalChunks: number;
|
|
129
|
+
totalSources: number;
|
|
130
|
+
lastIndexedAt?: string;
|
|
131
|
+
};
|
|
121
132
|
/**
|
|
122
133
|
* Get all chunks for a given source by ID — bypasses FTS5 MATCH entirely.
|
|
123
134
|
* Use this for inventory/listing where you need all sections, not search.
|