@tekmidian/pai 0.66.1 → 0.66.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.
- package/dist/{aibroker-client-B8c42Lh8.mjs → aibroker-client-CHEEvJZW.mjs} +2 -2
- package/dist/{aibroker-client-C5Fw7DNz.mjs → aibroker-client-Dfv7j1Od.mjs} +3 -3
- package/dist/{aibroker-client-C5Fw7DNz.mjs.map → aibroker-client-Dfv7j1Od.mjs.map} +1 -1
- package/dist/{auto-route-o0BOsXn7.mjs → auto-route-BnizyALK.mjs} +65 -4
- package/dist/auto-route-BnizyALK.mjs.map +1 -0
- package/dist/auto-route-D6fW1Q8z.mjs +3 -0
- package/dist/{chain-CZfKIEq9.mjs → chain-D5876UQX.mjs} +3 -4
- package/dist/{chain-CZfKIEq9.mjs.map → chain-D5876UQX.mjs.map} +1 -1
- package/dist/cli/index.mjs +18 -23
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.mjs +17 -22
- package/dist/{config-BbLFD7Uf.mjs → config-C-LiGlop.mjs} +2 -2
- package/dist/{config-BbLFD7Uf.mjs.map → config-C-LiGlop.mjs.map} +1 -1
- package/dist/{config-YinjgXEJ.mjs → config-DKIA7wgF.mjs} +1 -1
- package/dist/{context-handover-cache-pkHzmL3c.mjs → context-handover-cache-9PGvXRIw.mjs} +984 -10
- package/dist/context-handover-cache-9PGvXRIw.mjs.map +1 -0
- package/dist/daemon/index.mjs +17 -19
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/daemon-BUgNvMUh.mjs +5029 -0
- package/dist/daemon-BUgNvMUh.mjs.map +1 -0
- package/dist/daemon-DL0eglMJ.mjs +18 -0
- package/dist/daemon-mcp/index.mjs +12 -9
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{embeddings-CcscYWwk.mjs → embeddings-CEBGrzwu.mjs} +1 -1
- package/dist/{embeddings-Bx3q0QOY.mjs → embeddings-DOLZnT1X.mjs} +1 -1
- package/dist/{embeddings-Bx3q0QOY.mjs.map → embeddings-DOLZnT1X.mjs.map} +1 -1
- package/dist/{env-JNEIrQWg.mjs → env-DiolswKQ.mjs} +1 -1
- package/dist/{env-JNEIrQWg.mjs.map → env-DiolswKQ.mjs.map} +1 -1
- package/dist/factory-DrBg24UT.mjs +8 -0
- package/dist/factory-Ypf8r0fK.mjs +2892 -0
- package/dist/factory-Ypf8r0fK.mjs.map +1 -0
- package/dist/{fallback-CIiq2Raw.mjs → fallback-D51R4shY.mjs} +23 -11
- package/dist/fallback-D51R4shY.mjs.map +1 -0
- package/dist/{chunker-BH4i-F2b.mjs → helpers-CZsi_49C.mjs} +221 -2
- package/dist/helpers-CZsi_49C.mjs.map +1 -0
- package/dist/hooks/worker-status-line.mjs.map +2 -2
- package/dist/index.mjs +6 -7
- package/dist/{ipc-client-D16Xw6Uo.mjs → ipc-client-BLYX51nG.mjs} +2 -2
- package/dist/{ipc-client-D16Xw6Uo.mjs.map → ipc-client-BLYX51nG.mjs.map} +1 -1
- package/dist/main-resolver-BYgP86C1.mjs +7 -0
- package/dist/{main-resolver-DPPwHNtn.mjs → main-resolver-kVkMJZUD.mjs} +6 -6
- package/dist/{main-resolver-DPPwHNtn.mjs.map → main-resolver-kVkMJZUD.mjs.map} +1 -1
- package/dist/{migrate-BD7D8EEh.mjs → migrate-Bq0esMOa.mjs} +2 -2
- package/dist/{migrate-BD7D8EEh.mjs.map → migrate-Bq0esMOa.mjs.map} +1 -1
- package/dist/{pai-marker-D1MMswkz.mjs → pai-marker-DXVpFsYz.mjs} +1 -1
- package/dist/{pai-marker-D1MMswkz.mjs.map → pai-marker-DXVpFsYz.mjs.map} +1 -1
- package/dist/{planner-Dm5Acyo1.mjs → planner-D5yt0EfA.mjs} +10 -7
- package/dist/{planner-Dm5Acyo1.mjs.map → planner-D5yt0EfA.mjs.map} +1 -1
- package/dist/postgres-BmLr0MUm.mjs +5 -0
- package/dist/{postgres-Ceqsa64C.mjs → postgres-D3xc2RB4.mjs} +364 -9
- package/dist/postgres-D3xc2RB4.mjs.map +1 -0
- package/dist/{program-JJ0TwSHI.mjs → program-DIXRtAFy.mjs} +77 -70
- package/dist/program-DIXRtAFy.mjs.map +1 -0
- package/dist/{query-feedback-BIaZTTFO.mjs → query-feedback-B7FYE4JR.mjs} +1 -1
- package/dist/{query-feedback-BIaZTTFO.mjs.map → query-feedback-B7FYE4JR.mjs.map} +1 -1
- package/dist/{reranker-DKv80KO5.mjs → reranker-3lnggwgq.mjs} +1 -1
- package/dist/{reranker-DKv80KO5.mjs.map → reranker-3lnggwgq.mjs.map} +1 -1
- package/dist/{reranker-CFiEzHQu.mjs → reranker-CZ2mP4cf.mjs} +1 -1
- package/dist/router-BK-hFeQ6.mjs +3 -0
- package/dist/{router-a5G7q8_3.mjs → router-BaTbc9VX.mjs} +2 -2
- package/dist/{router-a5G7q8_3.mjs.map → router-BaTbc9VX.mjs.map} +1 -1
- package/dist/{run-BCpbxqA5.mjs → run-CLYTOm0x.mjs} +599 -22
- package/dist/run-CLYTOm0x.mjs.map +1 -0
- package/dist/{run-env-DRcp7A8K.mjs → run-env-Bnmf6bRI.mjs} +2 -2
- package/dist/{run-env-DRcp7A8K.mjs.map → run-env-Bnmf6bRI.mjs.map} +1 -1
- package/dist/{runtime-paths-CHTg3ywb.mjs → runtime-paths-D3FKAm69.mjs} +1 -1
- package/dist/{runtime-paths-CHTg3ywb.mjs.map → runtime-paths-D3FKAm69.mjs.map} +1 -1
- package/dist/{search-BOSphCJ1.mjs → search-CNAGTiJP.mjs} +2 -2
- package/dist/{search-BOSphCJ1.mjs.map → search-CNAGTiJP.mjs.map} +1 -1
- package/dist/{sources-CijVso3n.mjs → sources-CM2g-CLT.mjs} +2 -2
- package/dist/{sources-CijVso3n.mjs.map → sources-CM2g-CLT.mjs.map} +1 -1
- package/dist/{stop-words-BdQuaE9K.mjs → stop-words-DtxaTWU_.mjs} +1 -1
- package/dist/{stop-words-BdQuaE9K.mjs.map → stop-words-DtxaTWU_.mjs.map} +1 -1
- package/dist/{utils-C9HsYDpO.mjs → utils-DddRwMsG.mjs} +2 -2
- package/dist/{utils-C9HsYDpO.mjs.map → utils-DddRwMsG.mjs.map} +1 -1
- package/dist/{zettelkasten-CtGHQWPU.mjs → zettelkasten-BhZMvmoK.mjs} +149 -7
- package/dist/zettelkasten-BhZMvmoK.mjs.map +1 -0
- package/dist/zettelkasten-BwV1pluL.mjs +5 -0
- package/docs/commands/worker.md +2 -0
- package/package.json +1 -1
- package/dist/async-C2Bm_Lal.mjs +0 -300
- package/dist/async-C2Bm_Lal.mjs.map +0 -1
- package/dist/auto-route-o0BOsXn7.mjs.map +0 -1
- package/dist/chunker-BH4i-F2b.mjs.map +0 -1
- package/dist/clusters-BCtD3fbe.mjs +0 -169
- package/dist/clusters-BCtD3fbe.mjs.map +0 -1
- package/dist/context-handover-cache-pkHzmL3c.mjs.map +0 -1
- package/dist/daemon-DXlaVCkw.mjs +0 -1741
- package/dist/daemon-DXlaVCkw.mjs.map +0 -1
- package/dist/daemon-DyORmxZu.mjs +0 -19
- package/dist/detector-BEPFsINR.mjs +0 -3
- package/dist/detector-BRTtAWrM.mjs +0 -65
- package/dist/detector-BRTtAWrM.mjs.map +0 -1
- package/dist/factory-CNpPQQ-5.mjs +0 -249
- package/dist/factory-CNpPQQ-5.mjs.map +0 -1
- package/dist/factory-DY7x73mE.mjs +0 -3
- package/dist/fallback-CIiq2Raw.mjs.map +0 -1
- package/dist/federation-db-BTyoufBh.mjs +0 -139
- package/dist/federation-db-BTyoufBh.mjs.map +0 -1
- package/dist/federation-db-HfIFc7FG.mjs +0 -3
- package/dist/helpers-BRCJg0G3.mjs +0 -223
- package/dist/helpers-BRCJg0G3.mjs.map +0 -1
- package/dist/indexer-backend-DFF2FrYx.mjs +0 -5
- package/dist/indexer-backend-isSLg6yE.mjs +0 -1
- package/dist/kg-entity-DCOcsVFD.mjs +0 -29
- package/dist/kg-entity-DCOcsVFD.mjs.map +0 -1
- package/dist/latent-ideas-wwAXquGe.mjs +0 -191
- package/dist/latent-ideas-wwAXquGe.mjs.map +0 -1
- package/dist/link-boost-CnI7UVMJ.mjs +0 -34
- package/dist/link-boost-CnI7UVMJ.mjs.map +0 -1
- package/dist/main-resolver-BKz_OuEh.mjs +0 -7
- package/dist/merge-2gqRPFu2.mjs +0 -3
- package/dist/merge-DgU9OgZy.mjs +0 -6
- package/dist/merge-DgU9OgZy.mjs.map +0 -1
- package/dist/module-paths-DdRzbkUI.mjs +0 -44
- package/dist/module-paths-DdRzbkUI.mjs.map +0 -1
- package/dist/neighborhood-2FsmzoxG.mjs +0 -114
- package/dist/neighborhood-2FsmzoxG.mjs.map +0 -1
- package/dist/note-context-De63k8na.mjs +0 -106
- package/dist/note-context-De63k8na.mjs.map +0 -1
- package/dist/postgres-Ceqsa64C.mjs.map +0 -1
- package/dist/program-JJ0TwSHI.mjs.map +0 -1
- package/dist/query-feedback-B_iigYj-.mjs +0 -3
- package/dist/registry-db-C7voqML9.mjs +0 -213
- package/dist/registry-db-C7voqML9.mjs.map +0 -1
- package/dist/registry-db-JHPhA8vF.mjs +0 -3
- package/dist/registry-postgres-nOfyBSIW.mjs +0 -795
- package/dist/registry-postgres-nOfyBSIW.mjs.map +0 -1
- package/dist/registry-sqlite-DrCW1aRK.mjs +0 -593
- package/dist/registry-sqlite-DrCW1aRK.mjs.map +0 -1
- package/dist/router-CXUGsv85.mjs +0 -3
- package/dist/run-BCpbxqA5.mjs.map +0 -1
- package/dist/search-Bf3Kub3F.mjs +0 -3
- package/dist/server-DgmAHyFK.mjs +0 -403
- package/dist/server-DgmAHyFK.mjs.map +0 -1
- package/dist/session-keepalive-DxjTHGcK.mjs +0 -582
- package/dist/session-keepalive-DxjTHGcK.mjs.map +0 -1
- package/dist/sqlite-CqsTy6xo.mjs +0 -936
- package/dist/sqlite-CqsTy6xo.mjs.map +0 -1
- package/dist/state-DW8zdweW.mjs +0 -3
- package/dist/state-HyjqTihC.mjs +0 -76
- package/dist/state-HyjqTihC.mjs.map +0 -1
- package/dist/themes-CTaOj3e1.mjs +0 -148
- package/dist/themes-CTaOj3e1.mjs.map +0 -1
- package/dist/tools-BcOKPBtg.mjs +0 -1489
- package/dist/tools-BcOKPBtg.mjs.map +0 -1
- package/dist/tools-C-_l0o8Z.mjs +0 -6
- package/dist/trace-CtV0RO6n.mjs +0 -137
- package/dist/trace-CtV0RO6n.mjs.map +0 -1
- package/dist/utils-BrX9FLeK.mjs +0 -3
- package/dist/vault-indexer-BG1sEVyM.mjs +0 -537
- package/dist/vault-indexer-BG1sEVyM.mjs.map +0 -1
- package/dist/wakeup-D8n33hYV.mjs +0 -335
- package/dist/wakeup-D8n33hYV.mjs.map +0 -1
- package/dist/work-queue-worker-4tLa5gpQ.mjs +0 -567
- package/dist/work-queue-worker-4tLa5gpQ.mjs.map +0 -1
- package/dist/work-queue-worker-rFZvTO3p.mjs +0 -11
- package/dist/zettelkasten-CtGHQWPU.mjs.map +0 -1
|
@@ -1,19 +1,111 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as slugify, i as parseSessionFilename, n as decodeEncodedDir, t as buildEncodedDirMap } from "./migrate-
|
|
3
|
-
import { n as ensurePaiMarker, t as discoverPaiMarkers } from "./pai-marker-
|
|
4
|
-
import { i as getRegistryBackend } from "./factory-
|
|
1
|
+
import { c as ok, g as warn, i as err, n as dim, r as encodeDir, s as now, t as bold } from "./utils-DddRwMsG.mjs";
|
|
2
|
+
import { a as slugify, i as parseSessionFilename, n as decodeEncodedDir, t as buildEncodedDirMap } from "./migrate-Bq0esMOa.mjs";
|
|
3
|
+
import { n as ensurePaiMarker, t as discoverPaiMarkers } from "./pai-marker-DXVpFsYz.mjs";
|
|
4
|
+
import { i as getRegistryBackend } from "./factory-Ypf8r0fK.mjs";
|
|
5
|
+
import { y as writeJsonAtomic } from "./config-C-LiGlop.mjs";
|
|
5
6
|
import { a as paiHomePath, o as resolvePaiFile, r as migratePaiFile } from "./pai-home-Cm9rcJgX.mjs";
|
|
6
|
-
import { D as readWorkersSection, k as resolveModelCapability, t as buildRunEnv, v as defaultWorkersConfig } from "./run-env-
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { D as readWorkersSection, k as resolveModelCapability, t as buildRunEnv, v as defaultWorkersConfig } from "./run-env-Bnmf6bRI.mjs";
|
|
8
|
+
import { C as itermUuid, F as appendLedger, P as worktreesDir, T as sessionMapPath, ct as resolveTarget, lt as ensureNoMcpConfig, nt as DEFAULT_PROXY_PORT, pt as workersLogDir, rt as ensureProxyRunning } from "./run-CLYTOm0x.mjs";
|
|
9
|
+
import { i as sendToSession, n as fetchLiveSessions } from "./aibroker-client-Dfv7j1Od.mjs";
|
|
10
|
+
import { createReadStream, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
10
11
|
import { homedir, tmpdir } from "node:os";
|
|
11
12
|
import { basename, dirname, join, resolve } from "node:path";
|
|
12
13
|
import { randomUUID } from "node:crypto";
|
|
13
14
|
import { copyFileSync as copyFileSync$1, existsSync as existsSync$1, linkSync as linkSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, renameSync as renameSync$1, writeFileSync as writeFileSync$1 } from "fs";
|
|
14
15
|
import { basename as basename$1, join as join$1, resolve as resolve$1 } from "path";
|
|
15
16
|
import { homedir as homedir$1 } from "os";
|
|
17
|
+
import { createInterface } from "node:readline";
|
|
16
18
|
|
|
19
|
+
//#region src/cli/commands/detect.ts
|
|
20
|
+
/**
|
|
21
|
+
* Project detection logic for PAI.
|
|
22
|
+
*
|
|
23
|
+
* detectProject(cwd) — given a filesystem path, returns the best matching
|
|
24
|
+
* project from the registry:
|
|
25
|
+
* 1. Exact path match
|
|
26
|
+
* 2. Longest parent match (project whose root_path is an ancestor of cwd)
|
|
27
|
+
*
|
|
28
|
+
* Exported for use by the CLI `pai project detect` command and the MCP
|
|
29
|
+
* `project_detect` tool.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Detect which registered project a filesystem path belongs to.
|
|
33
|
+
*
|
|
34
|
+
* @param cwd Absolute path to detect (defaults to process.cwd())
|
|
35
|
+
* @returns The best matching project, or null if no match
|
|
36
|
+
*/
|
|
37
|
+
async function detectProject(cwd) {
|
|
38
|
+
const target = resolve(cwd ?? process.cwd());
|
|
39
|
+
const backend = await getRegistryBackend();
|
|
40
|
+
const projects = await backend.listProjectsByPathLengthDesc({ excludeArchived: true });
|
|
41
|
+
let matched = null;
|
|
42
|
+
let matchType = "exact";
|
|
43
|
+
for (const p of projects) {
|
|
44
|
+
const root = resolve(p.root_path);
|
|
45
|
+
if (target === root) {
|
|
46
|
+
matched = p;
|
|
47
|
+
matchType = "exact";
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
if (!matched && target.startsWith(root + "/")) {
|
|
51
|
+
matched = p;
|
|
52
|
+
matchType = "parent";
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!matched) return null;
|
|
57
|
+
const sessionCount = await backend.countSessionsForProject(matched.id);
|
|
58
|
+
const lastDate = await backend.getMostRecentSessionDate(matched.id);
|
|
59
|
+
const relative = matchType === "parent" ? target.slice(resolve(matched.root_path).length + 1) : null;
|
|
60
|
+
return {
|
|
61
|
+
id: matched.id,
|
|
62
|
+
slug: matched.slug,
|
|
63
|
+
display_name: matched.display_name,
|
|
64
|
+
root_path: matched.root_path,
|
|
65
|
+
encoded_dir: matched.encoded_dir,
|
|
66
|
+
type: matched.type,
|
|
67
|
+
status: matched.status,
|
|
68
|
+
session_count: sessionCount,
|
|
69
|
+
last_session_date: lastDate,
|
|
70
|
+
match_type: matchType,
|
|
71
|
+
relative_path: relative
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Format a DetectedProject for human-readable CLI output.
|
|
76
|
+
*/
|
|
77
|
+
function formatDetection(d) {
|
|
78
|
+
const lines = [
|
|
79
|
+
`slug: ${d.slug}`,
|
|
80
|
+
`display_name: ${d.display_name}`,
|
|
81
|
+
`root_path: ${d.root_path}`,
|
|
82
|
+
`type: ${d.type}`,
|
|
83
|
+
`status: ${d.status}`,
|
|
84
|
+
`match: ${d.match_type}${d.relative_path ? ` (+${d.relative_path})` : ""}`,
|
|
85
|
+
`sessions: ${d.session_count}`
|
|
86
|
+
];
|
|
87
|
+
if (d.last_session_date) lines.push(`last_session: ${d.last_session_date}`);
|
|
88
|
+
return lines.join("\n");
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Format a DetectedProject as JSON for machine consumption.
|
|
92
|
+
*/
|
|
93
|
+
function formatDetectionJson(d) {
|
|
94
|
+
return JSON.stringify({
|
|
95
|
+
slug: d.slug,
|
|
96
|
+
display_name: d.display_name,
|
|
97
|
+
root_path: d.root_path,
|
|
98
|
+
encoded_dir: d.encoded_dir,
|
|
99
|
+
type: d.type,
|
|
100
|
+
status: d.status,
|
|
101
|
+
match_type: d.match_type,
|
|
102
|
+
relative_path: d.relative_path,
|
|
103
|
+
session_count: d.session_count,
|
|
104
|
+
last_session_date: d.last_session_date
|
|
105
|
+
}, null, 2);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
//#endregion
|
|
17
109
|
//#region src/registry/moved.ts
|
|
18
110
|
/**
|
|
19
111
|
* moved.ts — reconnect a project to transcripts that moved out from under it.
|
|
@@ -553,6 +645,243 @@ async function cmdScan(opts = {}) {
|
|
|
553
645
|
} else process.stderr.write(`[registry-scan] ${result.projectsScanned} projects, ${result.sessionsScanned} sessions (${result.projectsNew}+${result.sessionsNew} new).\n`);
|
|
554
646
|
}
|
|
555
647
|
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region src/memory/wakeup.ts
|
|
650
|
+
/**
|
|
651
|
+
* Wake-up context system — progressive context loading inspired by mempalace.
|
|
652
|
+
*
|
|
653
|
+
* Layers:
|
|
654
|
+
* L0 Identity (~100 tokens) — user identity from ~/.pai/identity.txt. Always loaded.
|
|
655
|
+
* L1 Essential Story (~500-800t) — top session notes for the project, key lines extracted.
|
|
656
|
+
* L2 On-Demand — triggered by topic queries (handled by memory_search).
|
|
657
|
+
* L3 Deep Search — unlimited federated memory search (memory_search tool).
|
|
658
|
+
*/
|
|
659
|
+
/** Maximum tokens for the L1 essential story block. Approx 4 chars/token.
|
|
660
|
+
* Measured 2026-09-20: 800 tokens was mostly stale bullets. Reduced to 300.
|
|
661
|
+
*/
|
|
662
|
+
const L1_TOKEN_BUDGET = 300;
|
|
663
|
+
L1_TOKEN_BUDGET * 4;
|
|
664
|
+
/** Maximum session notes to scan when building L1. Reduced from 10 to 3 (2026-09-20). */
|
|
665
|
+
const L1_MAX_NOTES = 3;
|
|
666
|
+
/** Sections to extract from session notes (in priority order). */
|
|
667
|
+
const EXTRACT_SECTIONS = [
|
|
668
|
+
"Work Done",
|
|
669
|
+
"Key Decisions",
|
|
670
|
+
"Next Steps",
|
|
671
|
+
"Checkpoint"
|
|
672
|
+
];
|
|
673
|
+
/** Identity file location. */
|
|
674
|
+
const IDENTITY_FILE = join(homedir(), ".pai", "identity.txt");
|
|
675
|
+
/**
|
|
676
|
+
* Load L0 identity. Prefers the PAI_HOME location (identity.txt migrated by
|
|
677
|
+
* `pai config migrate`), falling back to the legacy ~/.pai/identity.txt so a
|
|
678
|
+
* not-yet-migrated file keeps working. Returns "" if neither exists. Never
|
|
679
|
+
* throws.
|
|
680
|
+
*/
|
|
681
|
+
function loadL0Identity(opts = {}) {
|
|
682
|
+
const newPath = opts.newPath ?? paiHomePath("identity.txt");
|
|
683
|
+
const legacyPath = opts.legacyPath ?? IDENTITY_FILE;
|
|
684
|
+
const path = existsSync(newPath) ? newPath : legacyPath;
|
|
685
|
+
if (!existsSync(path)) return "";
|
|
686
|
+
try {
|
|
687
|
+
return readFileSync(path, "utf-8").trim();
|
|
688
|
+
} catch {
|
|
689
|
+
return "";
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Move ~/.pai/identity.txt into PAI_HOME, following the same copy /
|
|
694
|
+
* verify-byte-identical / rename-aside semantics as every other per-user
|
|
695
|
+
* file (see migratePaiFile). `from`/`to` are overridable for tests.
|
|
696
|
+
*/
|
|
697
|
+
function migrateIdentityFile(opts = {}) {
|
|
698
|
+
return migratePaiFile(opts.to ?? paiHomePath("identity.txt"), [opts.from ?? IDENTITY_FILE], { dryRun: opts.dryRun });
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Find the Notes directory for a project given its root_path from the registry.
|
|
702
|
+
* Checks local Notes/ first, then central ~/.claude/projects/... path.
|
|
703
|
+
*/
|
|
704
|
+
function findNotesDirForProject(rootPath) {
|
|
705
|
+
const localCandidates = [
|
|
706
|
+
join(rootPath, "Notes"),
|
|
707
|
+
join(rootPath, "notes"),
|
|
708
|
+
join(rootPath, ".claude", "Notes")
|
|
709
|
+
];
|
|
710
|
+
for (const p of localCandidates) if (existsSync(p)) return p;
|
|
711
|
+
const encoded = rootPath.replace(/\//g, "-").replace(/\./g, "-").replace(/ /g, "-");
|
|
712
|
+
const centralNotes = join(homedir(), ".claude", "projects", encoded, "Notes");
|
|
713
|
+
if (existsSync(centralNotes)) return centralNotes;
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Recursively find all .md session note files in a Notes directory.
|
|
718
|
+
* Handles both flat layout (Notes/*.md) and month-subdirectory layout
|
|
719
|
+
* (Notes/YYYY/MM/*.md). Returns files sorted newest-first by filename
|
|
720
|
+
* (note numbers are monotonically increasing, so lexicographic = newest-last,
|
|
721
|
+
* so we reverse).
|
|
722
|
+
*/
|
|
723
|
+
function findSessionNotes(notesDir) {
|
|
724
|
+
const result = [];
|
|
725
|
+
const scanDir = (dir) => {
|
|
726
|
+
if (!existsSync(dir)) return;
|
|
727
|
+
let entries;
|
|
728
|
+
try {
|
|
729
|
+
entries = readdirSync(dir, { withFileTypes: true }).map((e) => ({
|
|
730
|
+
name: e.name,
|
|
731
|
+
isDir: e.isDirectory()
|
|
732
|
+
}));
|
|
733
|
+
} catch {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
for (const entry of entries) {
|
|
737
|
+
const fullPath = join(dir, entry.name);
|
|
738
|
+
if (entry.isDir) scanDir(fullPath);
|
|
739
|
+
else if (entry.name.match(/^\d{3,4}[\s_-].*\.md$/)) result.push(fullPath);
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
scanDir(notesDir);
|
|
743
|
+
const dateOf = (p) => basename(p).match(/(\d{4}-\d{2}-\d{2})/)?.[1] ?? "";
|
|
744
|
+
const numberOf = (p) => parseInt(basename(p).match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
745
|
+
const mtimeOf = (p) => {
|
|
746
|
+
try {
|
|
747
|
+
return statSync(p).mtimeMs;
|
|
748
|
+
} catch {
|
|
749
|
+
return 0;
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
result.sort((a, b) => {
|
|
753
|
+
const dateA = dateOf(a);
|
|
754
|
+
const dateB = dateOf(b);
|
|
755
|
+
if (dateA !== dateB) {
|
|
756
|
+
if (!dateA) return 1;
|
|
757
|
+
if (!dateB) return -1;
|
|
758
|
+
return dateB.localeCompare(dateA);
|
|
759
|
+
}
|
|
760
|
+
const byNumber = numberOf(b) - numberOf(a);
|
|
761
|
+
if (byNumber !== 0) return byNumber;
|
|
762
|
+
return mtimeOf(b) - mtimeOf(a);
|
|
763
|
+
});
|
|
764
|
+
return result;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Extract the most important lines from a session note.
|
|
768
|
+
* Prioritises: Work Done items, Key Decisions, Next Steps, Checkpoint headings.
|
|
769
|
+
* Returns a condensed string under maxChars.
|
|
770
|
+
*/
|
|
771
|
+
function normalizeLine(line) {
|
|
772
|
+
return line.replace(/^(- \[[ x]\] |- |\* |\d+\.\s)/, "").trim().replace(/\s+/g, " ");
|
|
773
|
+
}
|
|
774
|
+
function extractKeyLines(content, maxChars, seen = /* @__PURE__ */ new Set(), seenLabels = /* @__PURE__ */ new Set()) {
|
|
775
|
+
const lines = content.split("\n");
|
|
776
|
+
const selected = [];
|
|
777
|
+
let inTargetSection = false;
|
|
778
|
+
let currentSection = "";
|
|
779
|
+
let charCount = 0;
|
|
780
|
+
let pendingLabel = null;
|
|
781
|
+
for (const line of lines) {
|
|
782
|
+
const h2Match = line.match(/^## (.+)$/);
|
|
783
|
+
const h3Match = line.match(/^### (.+)$/);
|
|
784
|
+
if (h2Match) {
|
|
785
|
+
currentSection = h2Match[1];
|
|
786
|
+
inTargetSection = EXTRACT_SECTIONS.some((s) => currentSection.toLowerCase().includes(s.toLowerCase()));
|
|
787
|
+
pendingLabel = null;
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (h3Match) {
|
|
791
|
+
if (inTargetSection) pendingLabel = h3Match[1];
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
if (!inTargetSection) continue;
|
|
795
|
+
const trimmed = line.trim();
|
|
796
|
+
if (!trimmed || trimmed.startsWith("<!--") || trimmed === "---") continue;
|
|
797
|
+
if (trimmed.startsWith("- ") || trimmed.startsWith("* ") || trimmed.match(/^\d+\./) || trimmed.startsWith("**")) {
|
|
798
|
+
const normalized = normalizeLine(trimmed);
|
|
799
|
+
if (seen.has(normalized)) continue;
|
|
800
|
+
let labelText = "";
|
|
801
|
+
let labelKey = "";
|
|
802
|
+
if (pendingLabel !== null) {
|
|
803
|
+
labelKey = pendingLabel.toLowerCase().trim();
|
|
804
|
+
if (!seenLabels.has(labelKey)) labelText = `[${pendingLabel}]`;
|
|
805
|
+
}
|
|
806
|
+
if (charCount + labelText.length + trimmed.length + 2 > maxChars) break;
|
|
807
|
+
if (labelText) {
|
|
808
|
+
selected.push(labelText);
|
|
809
|
+
seenLabels.add(labelKey);
|
|
810
|
+
charCount += labelText.length + 1;
|
|
811
|
+
}
|
|
812
|
+
pendingLabel = null;
|
|
813
|
+
seen.add(normalized);
|
|
814
|
+
selected.push(trimmed);
|
|
815
|
+
charCount += trimmed.length + 1;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return selected.join("\n");
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Build the L1 essential story block.
|
|
822
|
+
*
|
|
823
|
+
* Reads the most recent session notes for the project and extracts the key
|
|
824
|
+
* lines (Work Done, Key Decisions, Next Steps) within the token budget.
|
|
825
|
+
*
|
|
826
|
+
* @param rootPath The project root path (from the registry).
|
|
827
|
+
* @param tokenBudget Max tokens to consume. Default 800 (~3200 chars).
|
|
828
|
+
* @returns Formatted L1 block, or empty string if no notes found.
|
|
829
|
+
*/
|
|
830
|
+
function buildL1EssentialStory(rootPath, tokenBudget = L1_TOKEN_BUDGET) {
|
|
831
|
+
const charBudget = tokenBudget * 4;
|
|
832
|
+
const notesDir = findNotesDirForProject(rootPath);
|
|
833
|
+
if (!notesDir) return "";
|
|
834
|
+
const noteFiles = findSessionNotes(notesDir).slice(0, L1_MAX_NOTES);
|
|
835
|
+
if (noteFiles.length === 0) return "";
|
|
836
|
+
const sections = [];
|
|
837
|
+
const seen = /* @__PURE__ */ new Set();
|
|
838
|
+
const seenLabels = /* @__PURE__ */ new Set();
|
|
839
|
+
let remaining = charBudget;
|
|
840
|
+
for (const noteFile of noteFiles) {
|
|
841
|
+
if (remaining <= 50) break;
|
|
842
|
+
let content;
|
|
843
|
+
try {
|
|
844
|
+
content = readFileSync(noteFile, "utf-8");
|
|
845
|
+
} catch {
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
const name = basename(noteFile);
|
|
849
|
+
const titleMatch = name.match(/^\d+ - (\d{4}-\d{2}-\d{2}) - (.+)\.md$/);
|
|
850
|
+
const dateLabel = titleMatch ? titleMatch[1] : "";
|
|
851
|
+
const titleLabel = titleMatch ? titleMatch[2] : name.replace(/^\d+ - /, "").replace(/\.md$/, "");
|
|
852
|
+
const perNoteChars = Math.min(remaining, Math.floor(charBudget / noteFiles.length) + 200);
|
|
853
|
+
const extracted = extractKeyLines(content, perNoteChars, seen, seenLabels);
|
|
854
|
+
const hasContentLine = extracted.split("\n").some((l) => !/^\[.*\]$/.test(l));
|
|
855
|
+
if (!extracted || !hasContentLine) continue;
|
|
856
|
+
const noteBlock = `[${dateLabel} - ${titleLabel}]\n${extracted}`;
|
|
857
|
+
sections.push(noteBlock);
|
|
858
|
+
remaining -= noteBlock.length + 1;
|
|
859
|
+
}
|
|
860
|
+
if (sections.length === 0) return "";
|
|
861
|
+
return sections.join("\n\n");
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Build the combined wake-up context block (L0 + L1).
|
|
865
|
+
*
|
|
866
|
+
* Returns a formatted string suitable for injection as a system-reminder,
|
|
867
|
+
* or an empty string if both layers are empty.
|
|
868
|
+
*
|
|
869
|
+
* @param rootPath Project root path for L1 note lookup. Optional.
|
|
870
|
+
* @param tokenBudget L1 token budget. Default 800.
|
|
871
|
+
* @param opts.skipStory Skip the L1 essential story. Set when a handover
|
|
872
|
+
* checkpoint was already injected — the story re-summarises the same note
|
|
873
|
+
* the handover came from, at lower quality.
|
|
874
|
+
*/
|
|
875
|
+
function buildWakeupContext(rootPath, tokenBudget = L1_TOKEN_BUDGET, opts = {}) {
|
|
876
|
+
const identity = loadL0Identity();
|
|
877
|
+
const essentialStory = rootPath && !opts.skipStory ? buildL1EssentialStory(rootPath, tokenBudget) : "";
|
|
878
|
+
if (!identity && !essentialStory) return "";
|
|
879
|
+
const parts = [];
|
|
880
|
+
if (identity) parts.push(`## L0 Identity\n\n${identity}`);
|
|
881
|
+
if (essentialStory) parts.push(`## L1 Essential Story\n\n${essentialStory}`);
|
|
882
|
+
return parts.join("\n\n");
|
|
883
|
+
}
|
|
884
|
+
|
|
556
885
|
//#endregion
|
|
557
886
|
//#region src/hooks/ts/lib/pai-paths.ts
|
|
558
887
|
/**
|
|
@@ -2228,6 +2557,82 @@ async function extractAndStoreTriples$1(backend, params) {
|
|
|
2228
2557
|
return stats;
|
|
2229
2558
|
}
|
|
2230
2559
|
|
|
2560
|
+
//#endregion
|
|
2561
|
+
//#region src/daemon/daemon/state.ts
|
|
2562
|
+
/** RegistryBackend-based access (projects, sessions, tags, aliases, links, compaction_log). */
|
|
2563
|
+
let registryBackend;
|
|
2564
|
+
let storageBackend;
|
|
2565
|
+
let daemonConfig;
|
|
2566
|
+
let startTime = Date.now();
|
|
2567
|
+
/** True while a project index pass is running. */
|
|
2568
|
+
let indexInProgress = false;
|
|
2569
|
+
let lastIndexTime = 0;
|
|
2570
|
+
let indexSchedulerTimer = null;
|
|
2571
|
+
/** True while an embedding pass is running. */
|
|
2572
|
+
let embedInProgress = false;
|
|
2573
|
+
let lastEmbedTime = 0;
|
|
2574
|
+
let embedSchedulerTimer = null;
|
|
2575
|
+
/** True while a vault index pass is running. */
|
|
2576
|
+
let vaultIndexInProgress = false;
|
|
2577
|
+
let lastVaultIndexTime = 0;
|
|
2578
|
+
/** Interval timer of the worker prompt-cache keepalive (null = not armed). */
|
|
2579
|
+
let cacheKeepaliveTimer = null;
|
|
2580
|
+
/** Mutable notification config — loaded from disk at startup, patchable at runtime. */
|
|
2581
|
+
let notificationConfig;
|
|
2582
|
+
/**
|
|
2583
|
+
* Set to true when a SIGTERM/SIGINT is received so that long-running loops
|
|
2584
|
+
* (embed, index) can detect the signal and exit their inner loops before the
|
|
2585
|
+
* pool/backend is closed.
|
|
2586
|
+
*/
|
|
2587
|
+
let shutdownRequested = false;
|
|
2588
|
+
function setRegistryBackend(b) {
|
|
2589
|
+
registryBackend = b;
|
|
2590
|
+
}
|
|
2591
|
+
function setStorageBackend(b) {
|
|
2592
|
+
storageBackend = b;
|
|
2593
|
+
}
|
|
2594
|
+
function setDaemonConfig(c) {
|
|
2595
|
+
daemonConfig = c;
|
|
2596
|
+
}
|
|
2597
|
+
function setStartTime(t) {
|
|
2598
|
+
startTime = t;
|
|
2599
|
+
}
|
|
2600
|
+
function setNotificationConfig(c) {
|
|
2601
|
+
notificationConfig = c;
|
|
2602
|
+
}
|
|
2603
|
+
function setShutdownRequested(v) {
|
|
2604
|
+
shutdownRequested = v;
|
|
2605
|
+
}
|
|
2606
|
+
function setIndexInProgress(v) {
|
|
2607
|
+
indexInProgress = v;
|
|
2608
|
+
}
|
|
2609
|
+
function setLastIndexTime(v) {
|
|
2610
|
+
lastIndexTime = v;
|
|
2611
|
+
}
|
|
2612
|
+
function setIndexSchedulerTimer(v) {
|
|
2613
|
+
indexSchedulerTimer = v;
|
|
2614
|
+
}
|
|
2615
|
+
function setEmbedInProgress(v) {
|
|
2616
|
+
embedInProgress = v;
|
|
2617
|
+
}
|
|
2618
|
+
function setLastEmbedTime(v) {
|
|
2619
|
+
lastEmbedTime = v;
|
|
2620
|
+
}
|
|
2621
|
+
function setEmbedSchedulerTimer(v) {
|
|
2622
|
+
embedSchedulerTimer = v;
|
|
2623
|
+
}
|
|
2624
|
+
function setVaultIndexInProgress(v) {
|
|
2625
|
+
vaultIndexInProgress = v;
|
|
2626
|
+
}
|
|
2627
|
+
function setLastVaultIndexTime(v) {
|
|
2628
|
+
lastVaultIndexTime = v;
|
|
2629
|
+
}
|
|
2630
|
+
function setCacheKeepaliveTimer(v) {
|
|
2631
|
+
cacheKeepaliveTimer = v;
|
|
2632
|
+
}
|
|
2633
|
+
/** Failing passes by name ("index" | "embed" | "vault"); absent = healthy. */
|
|
2634
|
+
const passFailures = /* @__PURE__ */ new Map();
|
|
2635
|
+
|
|
2231
2636
|
//#endregion
|
|
2232
2637
|
//#region src/daemon/session-summary-worker.ts
|
|
2233
2638
|
/**
|
|
@@ -3171,6 +3576,575 @@ function cleanup() {
|
|
|
3171
3576
|
saveIfDirty();
|
|
3172
3577
|
}
|
|
3173
3578
|
|
|
3579
|
+
//#endregion
|
|
3580
|
+
//#region src/audit/session-usage.ts
|
|
3581
|
+
/**
|
|
3582
|
+
* session-usage.ts — parse the usage numbers out of a Claude Code session
|
|
3583
|
+
* (or subagent, or pai-worker event-mirror) JSONL transcript.
|
|
3584
|
+
*
|
|
3585
|
+
* All three log shapes share the same assistant-message envelope
|
|
3586
|
+
* ({ type: "assistant", message: { id, model, usage } }), so one parser
|
|
3587
|
+
* covers `pai audit tokens session` and the per-log readings feeding
|
|
3588
|
+
* `pai audit tokens spawn`.
|
|
3589
|
+
*
|
|
3590
|
+
* Streaming writes one JSONL line per content block of the same logical
|
|
3591
|
+
* turn, repeating message.id and usage each time — summing every line would
|
|
3592
|
+
* multiply usage by the block count. Keeping only the first line seen per
|
|
3593
|
+
* message.id is what the reference script (and this parser) count instead.
|
|
3594
|
+
*/
|
|
3595
|
+
const USAGE_KEYS = [
|
|
3596
|
+
"cache_read_input_tokens",
|
|
3597
|
+
"cache_creation_input_tokens",
|
|
3598
|
+
"input_tokens",
|
|
3599
|
+
"output_tokens"
|
|
3600
|
+
];
|
|
3601
|
+
function emptyTotals() {
|
|
3602
|
+
return {
|
|
3603
|
+
cache_read_input_tokens: 0,
|
|
3604
|
+
cache_creation_input_tokens: 0,
|
|
3605
|
+
input_tokens: 0,
|
|
3606
|
+
output_tokens: 0
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3609
|
+
/**
|
|
3610
|
+
* `type:"system"` `subtype:"compact_boundary"` lines mark a compaction.
|
|
3611
|
+
* turnIndex is the count of assistant turns already folded when it fired,
|
|
3612
|
+
* so it lines up with the turn numbering the text/JSON report prints.
|
|
3613
|
+
*/
|
|
3614
|
+
function isCompactBoundary(line) {
|
|
3615
|
+
return line.type === "system" && line.subtype === "compact_boundary";
|
|
3616
|
+
}
|
|
3617
|
+
function parseCompactionEvent(line, turnIndex) {
|
|
3618
|
+
const meta = line.compactMetadata;
|
|
3619
|
+
if (!meta || typeof meta.preTokens !== "number") return null;
|
|
3620
|
+
return {
|
|
3621
|
+
trigger: meta.trigger ?? "unknown",
|
|
3622
|
+
preTokens: meta.preTokens,
|
|
3623
|
+
turnIndex
|
|
3624
|
+
};
|
|
3625
|
+
}
|
|
3626
|
+
/**
|
|
3627
|
+
* `type:"system"` `subtype:"model_refusal_fallback"` lines mark an
|
|
3628
|
+
* automatic model switch fired by an API safety refusal (e.g. "cyber"
|
|
3629
|
+
* category), not a user or config choice — worth flagging separately since
|
|
3630
|
+
* it can rebuild the whole prompt cache mid-session.
|
|
3631
|
+
*/
|
|
3632
|
+
function isModelFallback(line) {
|
|
3633
|
+
return line.type === "system" && line.subtype === "model_refusal_fallback";
|
|
3634
|
+
}
|
|
3635
|
+
function parseFallbackEvent(line, turnIndex) {
|
|
3636
|
+
return {
|
|
3637
|
+
turnIndex,
|
|
3638
|
+
from: line.originalModel ?? "unknown",
|
|
3639
|
+
to: line.fallbackModel ?? "unknown",
|
|
3640
|
+
category: line.apiRefusalCategory ?? "unknown",
|
|
3641
|
+
scope: line.scope ?? "unknown"
|
|
3642
|
+
};
|
|
3643
|
+
}
|
|
3644
|
+
/**
|
|
3645
|
+
* Fold one already-parsed JSONL line into a report being accumulated.
|
|
3646
|
+
* Exported separately so both the streaming file reader below and tests
|
|
3647
|
+
* (which build fixtures as arrays of objects, not files) share one path.
|
|
3648
|
+
* Returns the turn's context value when a new turn was counted, else null
|
|
3649
|
+
* (non-assistant line, no usage, or a duplicate message.id already seen).
|
|
3650
|
+
*/
|
|
3651
|
+
function foldAssistantLine(report, seenIds, line, threshold) {
|
|
3652
|
+
if (line.type !== "assistant") return null;
|
|
3653
|
+
const message = line.message;
|
|
3654
|
+
const usage = message?.usage;
|
|
3655
|
+
if (!usage) return null;
|
|
3656
|
+
const id = message?.id ?? line.uuid;
|
|
3657
|
+
if (id) {
|
|
3658
|
+
if (seenIds.has(id)) return null;
|
|
3659
|
+
seenIds.add(id);
|
|
3660
|
+
}
|
|
3661
|
+
report.turns++;
|
|
3662
|
+
for (const key of USAGE_KEYS) {
|
|
3663
|
+
const v = usage[key];
|
|
3664
|
+
if (typeof v === "number") report.totals[key] += v;
|
|
3665
|
+
}
|
|
3666
|
+
const context = (Number(usage.cache_read_input_tokens) || 0) + (Number(usage.cache_creation_input_tokens) || 0) + (Number(usage.input_tokens) || 0);
|
|
3667
|
+
if (report.firstTurnContext === null) {
|
|
3668
|
+
report.firstTurnContext = context;
|
|
3669
|
+
report.firstTurnAt = line.timestamp ?? null;
|
|
3670
|
+
}
|
|
3671
|
+
const atMs = line.timestamp ? Date.parse(line.timestamp) : NaN;
|
|
3672
|
+
if (!Number.isNaN(atMs)) {
|
|
3673
|
+
if (report.lastTurnAtMs !== null && atMs - report.lastTurnAtMs > 3600 * 1e3) report.idleGapsOver60min++;
|
|
3674
|
+
report.lastTurnAtMs = atMs;
|
|
3675
|
+
}
|
|
3676
|
+
report.lastTurnContext = context;
|
|
3677
|
+
report.maxContext = report.maxContext === null ? context : Math.max(report.maxContext, context);
|
|
3678
|
+
if (context > threshold) report.turnsAboveThreshold++;
|
|
3679
|
+
if ((Number(usage.cache_creation_input_tokens) || 0) > 2e4) report.cacheRebuildTurns++;
|
|
3680
|
+
const model = message?.model ?? "unknown";
|
|
3681
|
+
report.models[model] = (report.models[model] ?? 0) + 1;
|
|
3682
|
+
if (report.lastModel !== null && report.lastModel !== model) report.modelSwitches.push({
|
|
3683
|
+
turnIndex: report.turns,
|
|
3684
|
+
from: report.lastModel,
|
|
3685
|
+
to: model,
|
|
3686
|
+
cacheRead: Number(usage.cache_read_input_tokens) || 0,
|
|
3687
|
+
cacheCreation: Number(usage.cache_creation_input_tokens) || 0
|
|
3688
|
+
});
|
|
3689
|
+
report.lastModel = model;
|
|
3690
|
+
const split = usage.cache_creation;
|
|
3691
|
+
if (split) {
|
|
3692
|
+
report.cacheCreationSplit.ephemeral5m += Number(split.ephemeral_5m_input_tokens) || 0;
|
|
3693
|
+
report.cacheCreationSplit.ephemeral1h += Number(split.ephemeral_1h_input_tokens) || 0;
|
|
3694
|
+
}
|
|
3695
|
+
return context;
|
|
3696
|
+
}
|
|
3697
|
+
/**
|
|
3698
|
+
* True for a real human-authored `type:"user"` prompt line: content is a
|
|
3699
|
+
* plain string, or a content array with no `tool_result` block. Claude Code
|
|
3700
|
+
* encodes tool results as `type:"user"` messages whose content array is
|
|
3701
|
+
* entirely (or partly) tool_result blocks — those must not count as prompts.
|
|
3702
|
+
*/
|
|
3703
|
+
function isRealUserPrompt(line) {
|
|
3704
|
+
if (line.type !== "user") return false;
|
|
3705
|
+
if (line.isMeta) return false;
|
|
3706
|
+
if (line.origin?.kind && line.origin.kind !== "human") return false;
|
|
3707
|
+
const content = line.message?.content;
|
|
3708
|
+
if (typeof content === "string") return true;
|
|
3709
|
+
if (Array.isArray(content)) return !content.some((block) => block?.type === "tool_result");
|
|
3710
|
+
return false;
|
|
3711
|
+
}
|
|
3712
|
+
/** Plain text of a real user prompt: the string content, or its first text block. */
|
|
3713
|
+
function extractUserPromptText(line) {
|
|
3714
|
+
const content = line.message?.content;
|
|
3715
|
+
if (typeof content === "string") return content;
|
|
3716
|
+
if (Array.isArray(content)) return content.find((b) => b?.type === "text")?.text ?? "";
|
|
3717
|
+
return "";
|
|
3718
|
+
}
|
|
3719
|
+
/**
|
|
3720
|
+
* Parse a session/subagent/worker-event JSONL file into a usage report.
|
|
3721
|
+
* `keepaliveWord`, when given, counts real user prompts whose text exactly
|
|
3722
|
+
* matches it (trimmed) — the sessions.cacheKeepalive beat prompt — into
|
|
3723
|
+
* `keepaliveBeats`; omitted, `keepaliveBeats` stays null.
|
|
3724
|
+
*/
|
|
3725
|
+
async function parseSessionUsage(path, threshold = 2e5, keepaliveWord) {
|
|
3726
|
+
const report = {
|
|
3727
|
+
path,
|
|
3728
|
+
sizeBytes: existsSync(path) ? readFileSync(path).byteLength : 0,
|
|
3729
|
+
turns: 0,
|
|
3730
|
+
totals: emptyTotals(),
|
|
3731
|
+
models: {},
|
|
3732
|
+
firstTurnContext: null,
|
|
3733
|
+
lastTurnContext: null,
|
|
3734
|
+
cacheCreationSplit: {
|
|
3735
|
+
ephemeral5m: 0,
|
|
3736
|
+
ephemeral1h: 0
|
|
3737
|
+
},
|
|
3738
|
+
avgContext: null,
|
|
3739
|
+
maxContext: null,
|
|
3740
|
+
turnsAboveThreshold: 0,
|
|
3741
|
+
cacheRebuildTurns: 0,
|
|
3742
|
+
userPrompts: 0,
|
|
3743
|
+
promptExposure: 0,
|
|
3744
|
+
compactions: [],
|
|
3745
|
+
firstTurnAt: null,
|
|
3746
|
+
lastModel: null,
|
|
3747
|
+
modelSwitches: [],
|
|
3748
|
+
fallbacks: [],
|
|
3749
|
+
lastTurnAtMs: null,
|
|
3750
|
+
idleGapsOver60min: 0,
|
|
3751
|
+
keepaliveBeats: keepaliveWord ? 0 : null
|
|
3752
|
+
};
|
|
3753
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
3754
|
+
let contextSum = 0;
|
|
3755
|
+
const rl = createInterface({
|
|
3756
|
+
input: createReadStream(path, "utf8"),
|
|
3757
|
+
crlfDelay: Infinity
|
|
3758
|
+
});
|
|
3759
|
+
for await (const raw of rl) {
|
|
3760
|
+
if (!raw.trim()) continue;
|
|
3761
|
+
let obj;
|
|
3762
|
+
try {
|
|
3763
|
+
obj = JSON.parse(raw);
|
|
3764
|
+
} catch {
|
|
3765
|
+
continue;
|
|
3766
|
+
}
|
|
3767
|
+
if (obj.type === "user") {
|
|
3768
|
+
if (isRealUserPrompt(obj)) {
|
|
3769
|
+
report.userPrompts++;
|
|
3770
|
+
if (keepaliveWord && extractUserPromptText(obj).trim() === keepaliveWord) report.keepaliveBeats = (report.keepaliveBeats ?? 0) + 1;
|
|
3771
|
+
}
|
|
3772
|
+
continue;
|
|
3773
|
+
}
|
|
3774
|
+
if (isCompactBoundary(obj)) {
|
|
3775
|
+
const event = parseCompactionEvent(obj, report.turns);
|
|
3776
|
+
if (event) report.compactions.push(event);
|
|
3777
|
+
continue;
|
|
3778
|
+
}
|
|
3779
|
+
if (isModelFallback(obj)) {
|
|
3780
|
+
report.fallbacks.push(parseFallbackEvent(obj, report.turns));
|
|
3781
|
+
continue;
|
|
3782
|
+
}
|
|
3783
|
+
const context = foldAssistantLine(report, seenIds, obj, threshold);
|
|
3784
|
+
if (context !== null) {
|
|
3785
|
+
contextSum += context;
|
|
3786
|
+
report.promptExposure += report.userPrompts;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
report.avgContext = report.turns > 0 ? Math.round(contextSum / report.turns) : null;
|
|
3790
|
+
return report;
|
|
3791
|
+
}
|
|
3792
|
+
function totalUsageTokens(totals) {
|
|
3793
|
+
return totals.cache_read_input_tokens + totals.cache_creation_input_tokens + totals.input_tokens + totals.output_tokens;
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
//#endregion
|
|
3797
|
+
//#region src/daemon/session-keepalive.ts
|
|
3798
|
+
/**
|
|
3799
|
+
* session-keepalive.ts — idle-triggered prompt-cache keepalive beat for live
|
|
3800
|
+
* interactive Claude Code sessions (`sessions.cacheKeepalive`, see config.ts
|
|
3801
|
+
* and docs/cache-keepalive.md, "Interactive sessions").
|
|
3802
|
+
*
|
|
3803
|
+
* One beat = one trivial prompt typed into a session through AIBroker's
|
|
3804
|
+
* send_to_session (src/cli/lib/aibroker-client.ts), sent with `noReply: true`
|
|
3805
|
+
* so the target sees only the bare word typed into its input line and
|
|
3806
|
+
* nothing is queued into its mailbox as a peer message demanding a reply.
|
|
3807
|
+
* A cache READ refreshes the provider's ephemeral prompt-cache TTL at
|
|
3808
|
+
* roughly 0.1x the cost of the 2x rewrite a cold cache forces on the next
|
|
3809
|
+
* real prompt.
|
|
3810
|
+
*
|
|
3811
|
+
* Distinct from workers/keepalive.ts, which beats a *worker provider's*
|
|
3812
|
+
* cache on a fixed timer regardless of activity: this only beats a session
|
|
3813
|
+
* that has actually been idle long enough to be at risk, and only inside a
|
|
3814
|
+
* configured working-hours window, so it never fires while the user is
|
|
3815
|
+
* present at the keyboard or overnight when nobody will read the reply
|
|
3816
|
+
* before the cache would have expired anyway.
|
|
3817
|
+
*/
|
|
3818
|
+
function sessionKeepaliveStatePath() {
|
|
3819
|
+
return paiHomePath("session-keepalive.json");
|
|
3820
|
+
}
|
|
3821
|
+
function emptyEntry() {
|
|
3822
|
+
return {
|
|
3823
|
+
beats: 0,
|
|
3824
|
+
lastRealPromptKey: null,
|
|
3825
|
+
lastBeatAt: null
|
|
3826
|
+
};
|
|
3827
|
+
}
|
|
3828
|
+
function loadSessionKeepaliveState(path = sessionKeepaliveStatePath()) {
|
|
3829
|
+
if (!existsSync(path)) return {};
|
|
3830
|
+
try {
|
|
3831
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
3832
|
+
} catch {
|
|
3833
|
+
return {};
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
function saveSessionKeepaliveState(state, path = sessionKeepaliveStatePath()) {
|
|
3837
|
+
writeJsonAtomic(path, state, {
|
|
3838
|
+
backup: false,
|
|
3839
|
+
label: path
|
|
3840
|
+
});
|
|
3841
|
+
}
|
|
3842
|
+
/** Parse "HH:MM-HH:MM" into minutes-since-midnight. Throws on a malformed
|
|
3843
|
+
* spec — an explicit config error beats a window that is silently always
|
|
3844
|
+
* on or always off. */
|
|
3845
|
+
function parseActiveHours(spec) {
|
|
3846
|
+
const m = spec.match(/^(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})$/);
|
|
3847
|
+
if (!m) throw new Error(`sessions.cacheKeepalive.activeHours: invalid "${spec}" (want "HH:MM-HH:MM")`);
|
|
3848
|
+
return {
|
|
3849
|
+
startMin: Number(m[1]) * 60 + Number(m[2]),
|
|
3850
|
+
endMin: Number(m[3]) * 60 + Number(m[4])
|
|
3851
|
+
};
|
|
3852
|
+
}
|
|
3853
|
+
/**
|
|
3854
|
+
* Whether `now` (local time) sits inside the window. A window that wraps
|
|
3855
|
+
* midnight (e.g. "22:00-06:00") is honoured by inverting the test instead of
|
|
3856
|
+
* requiring startMin < endMin.
|
|
3857
|
+
*/
|
|
3858
|
+
function isWithinActiveHours(now, spec) {
|
|
3859
|
+
const { startMin, endMin } = parseActiveHours(spec);
|
|
3860
|
+
const nowMin = now.getHours() * 60 + now.getMinutes();
|
|
3861
|
+
if (startMin <= endMin) return nowMin >= startMin && nowMin < endMin;
|
|
3862
|
+
return nowMin >= startMin || nowMin < endMin;
|
|
3863
|
+
}
|
|
3864
|
+
/**
|
|
3865
|
+
* Full path to a live session's transcript under ~/.claude/projects, or null
|
|
3866
|
+
* when none is found — the case for a session too new to have written a file
|
|
3867
|
+
* yet. A worker running in a worktree DOES write a transcript here (Claude
|
|
3868
|
+
* Code encodes its worktree cwd as the project dir name), so workers are not
|
|
3869
|
+
* excluded "for free" — see isWorkerSession.
|
|
3870
|
+
*/
|
|
3871
|
+
function findSessionTranscript(sessionId, projectsDir = join(homedir(), ".claude", "projects")) {
|
|
3872
|
+
let projectDirs;
|
|
3873
|
+
try {
|
|
3874
|
+
projectDirs = readdirSync(projectsDir);
|
|
3875
|
+
} catch {
|
|
3876
|
+
return null;
|
|
3877
|
+
}
|
|
3878
|
+
for (const projectDir of projectDirs) {
|
|
3879
|
+
const candidate = join(projectsDir, projectDir, `${sessionId}.jsonl`);
|
|
3880
|
+
if (existsSync(candidate)) return candidate;
|
|
3881
|
+
}
|
|
3882
|
+
return null;
|
|
3883
|
+
}
|
|
3884
|
+
/**
|
|
3885
|
+
* AIBroker's `fetchLiveSessions()` identifies a session by its iTerm2 pane id
|
|
3886
|
+
* (e.g. an AIBroker/iTerm UUID), not the Claude session id `<uuid>.jsonl`
|
|
3887
|
+
* transcripts are named after — the two are unrelated identifiers. The
|
|
3888
|
+
* status line bridges them on every refresh via `claude-session-map.json`
|
|
3889
|
+
* (see `recordSessionMapEntry` in ../workers/scope.ts): this picks, among the
|
|
3890
|
+
* map entries whose `term` pane UUID matches, the most recently written one.
|
|
3891
|
+
* Returns null when the pane has no (fresh enough) mapped Claude session.
|
|
3892
|
+
*/
|
|
3893
|
+
function resolveClaudeSessionIdFromMap(paneId, logDir) {
|
|
3894
|
+
const path = sessionMapPath(logDir);
|
|
3895
|
+
if (!paneId || !existsSync(path)) return null;
|
|
3896
|
+
let map;
|
|
3897
|
+
try {
|
|
3898
|
+
map = JSON.parse(readFileSync(path, "utf8"));
|
|
3899
|
+
} catch {
|
|
3900
|
+
return null;
|
|
3901
|
+
}
|
|
3902
|
+
let best = null;
|
|
3903
|
+
for (const entry of Object.values(map)) {
|
|
3904
|
+
if (!entry.term || itermUuid(entry.term) !== paneId) continue;
|
|
3905
|
+
if (!best || entry.ts > best.ts) best = entry;
|
|
3906
|
+
}
|
|
3907
|
+
return best?.session ?? null;
|
|
3908
|
+
}
|
|
3909
|
+
/** Claude Code's project-dir encoding of a cwd: every "/" becomes "-". */
|
|
3910
|
+
function encodeProjectDirName(cwd) {
|
|
3911
|
+
return cwd.replace(/\//g, "-");
|
|
3912
|
+
}
|
|
3913
|
+
/**
|
|
3914
|
+
* Is this live "claude"-kind session actually a worker pane rather than an
|
|
3915
|
+
* interactive one? `claude -p` workers running in a worktree write their
|
|
3916
|
+
* transcript under ~/.claude/projects too (encoded cwd = the worktree dir),
|
|
3917
|
+
* so a missing transcript is NOT how workers get excluded — this predicate
|
|
3918
|
+
* is. Either signal is enough:
|
|
3919
|
+
* (a) the transcript's project-dir name is the encoded form of a path
|
|
3920
|
+
* under <logDir>/worktrees (every worker worktree lives there), or
|
|
3921
|
+
* (b) the broker's session name/paiName names a path under the workers
|
|
3922
|
+
* log dir or one of its worktrees (best-effort: AIBroker does not
|
|
3923
|
+
* guarantee this, but honors it when present).
|
|
3924
|
+
*/
|
|
3925
|
+
function isWorkerSession(meta, transcriptPath, logDir) {
|
|
3926
|
+
const worktreesPrefix = encodeProjectDirName(worktreesDir(logDir));
|
|
3927
|
+
if (transcriptPath) {
|
|
3928
|
+
if ((transcriptPath.split("/").slice(0, -1).pop() ?? "").startsWith(worktreesPrefix)) return true;
|
|
3929
|
+
}
|
|
3930
|
+
const rawPrefix = worktreesDir(logDir);
|
|
3931
|
+
for (const field of [meta.name, meta.paiName]) if (field && (field.includes(rawPrefix) || field.includes(logDir))) return true;
|
|
3932
|
+
return false;
|
|
3933
|
+
}
|
|
3934
|
+
function isMidTurn(line) {
|
|
3935
|
+
if (!line) return false;
|
|
3936
|
+
if (line.type === "user" && isRealUserPrompt(line)) return true;
|
|
3937
|
+
if (line.type === "assistant") {
|
|
3938
|
+
const stopReason = line.message?.stop_reason;
|
|
3939
|
+
if (stopReason === void 0 || stopReason === null) return true;
|
|
3940
|
+
return stopReason !== "end_turn" && stopReason !== "stop_sequence";
|
|
3941
|
+
}
|
|
3942
|
+
return false;
|
|
3943
|
+
}
|
|
3944
|
+
function readTranscriptSnapshot(path) {
|
|
3945
|
+
let lines;
|
|
3946
|
+
try {
|
|
3947
|
+
lines = readFileSync(path, "utf8").split("\n");
|
|
3948
|
+
} catch {
|
|
3949
|
+
return {
|
|
3950
|
+
context: null,
|
|
3951
|
+
midTurn: false,
|
|
3952
|
+
lastRealPrompt: null
|
|
3953
|
+
};
|
|
3954
|
+
}
|
|
3955
|
+
let lastLine = null;
|
|
3956
|
+
let context = null;
|
|
3957
|
+
let lastRealPrompt = null;
|
|
3958
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3959
|
+
const raw = lines[i].trim();
|
|
3960
|
+
if (!raw) continue;
|
|
3961
|
+
let obj;
|
|
3962
|
+
try {
|
|
3963
|
+
obj = JSON.parse(raw);
|
|
3964
|
+
} catch {
|
|
3965
|
+
continue;
|
|
3966
|
+
}
|
|
3967
|
+
if (lastLine === null) lastLine = obj;
|
|
3968
|
+
if (context === null && obj.type === "assistant" && obj.message?.usage) {
|
|
3969
|
+
const usage = obj.message.usage;
|
|
3970
|
+
context = (Number(usage.cache_read_input_tokens) || 0) + (Number(usage.cache_creation_input_tokens) || 0) + (Number(usage.input_tokens) || 0);
|
|
3971
|
+
}
|
|
3972
|
+
if (lastRealPrompt === null && isRealUserPrompt(obj)) lastRealPrompt = {
|
|
3973
|
+
key: obj.uuid ?? obj.timestamp ?? String(i),
|
|
3974
|
+
text: extractUserPromptText(obj).trim()
|
|
3975
|
+
};
|
|
3976
|
+
if (context !== null && lastRealPrompt !== null) break;
|
|
3977
|
+
}
|
|
3978
|
+
return {
|
|
3979
|
+
context,
|
|
3980
|
+
midTurn: isMidTurn(lastLine),
|
|
3981
|
+
lastRealPrompt
|
|
3982
|
+
};
|
|
3983
|
+
}
|
|
3984
|
+
/** Ledger event tag; `pai daemon keepalive` and any log-tailer key off this. */
|
|
3985
|
+
const SESSION_KEEPALIVE_EVENT = "SESSION-KEEPALIVE";
|
|
3986
|
+
function sessionKeepaliveLedgerPath() {
|
|
3987
|
+
const { workers } = readWorkersSection();
|
|
3988
|
+
return join(workersLogDir(workers), "ledger.log");
|
|
3989
|
+
}
|
|
3990
|
+
/** The counts/last lines `pai daemon keepalive` prints, scoped to this event. */
|
|
3991
|
+
function sessionKeepaliveLedgerSummary(path = sessionKeepaliveLedgerPath(), lastN = 10) {
|
|
3992
|
+
if (!existsSync(path)) return {
|
|
3993
|
+
sent: 0,
|
|
3994
|
+
skipped: 0,
|
|
3995
|
+
lastLines: []
|
|
3996
|
+
};
|
|
3997
|
+
const lines = readFileSync(path, "utf8").split("\n").filter((l) => l.includes(SESSION_KEEPALIVE_EVENT));
|
|
3998
|
+
const sent = lines.filter((l) => / result=sent(\s|$)/.test(l)).length;
|
|
3999
|
+
return {
|
|
4000
|
+
sent,
|
|
4001
|
+
skipped: lines.length - sent,
|
|
4002
|
+
lastLines: lines.slice(-lastN)
|
|
4003
|
+
};
|
|
4004
|
+
}
|
|
4005
|
+
function defaultDeps() {
|
|
4006
|
+
const { workers } = readWorkersSection();
|
|
4007
|
+
const logDir = workersLogDir(workers);
|
|
4008
|
+
return {
|
|
4009
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
4010
|
+
fetchLiveSessions,
|
|
4011
|
+
resolveClaudeSessionId: (paneId) => resolveClaudeSessionIdFromMap(paneId, logDir),
|
|
4012
|
+
findTranscript: (id) => findSessionTranscript(id),
|
|
4013
|
+
mtimeMs: (p) => {
|
|
4014
|
+
try {
|
|
4015
|
+
return statSync(p).mtimeMs;
|
|
4016
|
+
} catch {
|
|
4017
|
+
return null;
|
|
4018
|
+
}
|
|
4019
|
+
},
|
|
4020
|
+
readSnapshot: readTranscriptSnapshot,
|
|
4021
|
+
sendBeat: (id, text) => sendToSession(id, text, void 0, { noReply: true }),
|
|
4022
|
+
loadState: () => loadSessionKeepaliveState(),
|
|
4023
|
+
saveState: (s) => saveSessionKeepaliveState(s),
|
|
4024
|
+
ledger: (kv) => appendLedger(sessionKeepaliveLedgerPath(), SESSION_KEEPALIVE_EVENT, kv)
|
|
4025
|
+
};
|
|
4026
|
+
}
|
|
4027
|
+
/**
|
|
4028
|
+
* One tick over every live interactive session: beat the ones that qualify,
|
|
4029
|
+
* skip (with a reason) the ones that don't. Returns one result per live
|
|
4030
|
+
* "claude"-kind session for tests to assert against; writes the ledger line
|
|
4031
|
+
* and (when any counter changed) the state file as side effects.
|
|
4032
|
+
*/
|
|
4033
|
+
async function runSessionKeepaliveTick(config, overrides = {}) {
|
|
4034
|
+
if (!config.enabled) return [];
|
|
4035
|
+
const d = {
|
|
4036
|
+
...defaultDeps(),
|
|
4037
|
+
...overrides
|
|
4038
|
+
};
|
|
4039
|
+
const sessions = await d.fetchLiveSessions();
|
|
4040
|
+
const state = d.loadState();
|
|
4041
|
+
const now = d.now();
|
|
4042
|
+
const { workers } = readWorkersSection();
|
|
4043
|
+
const logDir = workersLogDir(workers);
|
|
4044
|
+
const results = [];
|
|
4045
|
+
let anyChanged = false;
|
|
4046
|
+
for (const s of sessions) {
|
|
4047
|
+
if (s.kind !== "claude") continue;
|
|
4048
|
+
const paneId = s.sessionId;
|
|
4049
|
+
const claudeId = d.resolveClaudeSessionId(paneId);
|
|
4050
|
+
if (!claudeId) {
|
|
4051
|
+
d.ledger({
|
|
4052
|
+
session: null,
|
|
4053
|
+
pane: paneId,
|
|
4054
|
+
result: "skipped:unmapped"
|
|
4055
|
+
});
|
|
4056
|
+
results.push({
|
|
4057
|
+
sessionId: paneId,
|
|
4058
|
+
result: "skipped:unmapped"
|
|
4059
|
+
});
|
|
4060
|
+
continue;
|
|
4061
|
+
}
|
|
4062
|
+
const sessionId = claudeId;
|
|
4063
|
+
const entry = { ...state[sessionId] ?? emptyEntry() };
|
|
4064
|
+
let changed = false;
|
|
4065
|
+
const transcript = d.findTranscript(sessionId);
|
|
4066
|
+
if (isWorkerSession(s, transcript, logDir)) {
|
|
4067
|
+
d.ledger({
|
|
4068
|
+
session: sessionId,
|
|
4069
|
+
pane: paneId,
|
|
4070
|
+
result: "skipped:worker"
|
|
4071
|
+
});
|
|
4072
|
+
results.push({
|
|
4073
|
+
sessionId,
|
|
4074
|
+
result: "skipped:worker"
|
|
4075
|
+
});
|
|
4076
|
+
continue;
|
|
4077
|
+
}
|
|
4078
|
+
if (!transcript) {
|
|
4079
|
+
d.ledger({
|
|
4080
|
+
session: sessionId,
|
|
4081
|
+
pane: paneId,
|
|
4082
|
+
result: "skipped:no-transcript"
|
|
4083
|
+
});
|
|
4084
|
+
results.push({
|
|
4085
|
+
sessionId,
|
|
4086
|
+
result: "skipped:no-transcript"
|
|
4087
|
+
});
|
|
4088
|
+
continue;
|
|
4089
|
+
}
|
|
4090
|
+
const snapshot = d.readSnapshot(transcript);
|
|
4091
|
+
if (snapshot.lastRealPrompt && snapshot.lastRealPrompt.key !== entry.lastRealPromptKey) {
|
|
4092
|
+
entry.lastRealPromptKey = snapshot.lastRealPrompt.key;
|
|
4093
|
+
if (snapshot.lastRealPrompt.text !== config.prompt) entry.beats = 0;
|
|
4094
|
+
changed = true;
|
|
4095
|
+
}
|
|
4096
|
+
const mtime = d.mtimeMs(transcript);
|
|
4097
|
+
const idleMin = mtime === null ? null : (now.getTime() - mtime) / 6e4;
|
|
4098
|
+
const skip = mtime === null || idleMin === null ? "no-mtime" : idleMin < config.idleMinutes ? `idle:${idleMin.toFixed(1)}min` : !isWithinActiveHours(now, config.activeHours) ? "hours" : snapshot.context === null || snapshot.context < config.minContextTokens ? "context" : snapshot.midTurn ? "mid-turn" : entry.beats >= config.maxBeats ? "max-beats" : null;
|
|
4099
|
+
const ledgerBase = {
|
|
4100
|
+
session: sessionId,
|
|
4101
|
+
pane: paneId,
|
|
4102
|
+
idle_min: idleMin === null ? null : idleMin.toFixed(1),
|
|
4103
|
+
context: snapshot.context
|
|
4104
|
+
};
|
|
4105
|
+
if (skip) {
|
|
4106
|
+
d.ledger({
|
|
4107
|
+
...ledgerBase,
|
|
4108
|
+
beat: `${entry.beats}/${config.maxBeats}`,
|
|
4109
|
+
result: `skipped:${skip}`
|
|
4110
|
+
});
|
|
4111
|
+
results.push({
|
|
4112
|
+
sessionId,
|
|
4113
|
+
result: `skipped:${skip}`
|
|
4114
|
+
});
|
|
4115
|
+
} else if ((await d.sendBeat(paneId, config.prompt)).ok) {
|
|
4116
|
+
entry.beats += 1;
|
|
4117
|
+
entry.lastBeatAt = now.toISOString();
|
|
4118
|
+
changed = true;
|
|
4119
|
+
d.ledger({
|
|
4120
|
+
...ledgerBase,
|
|
4121
|
+
beat: `${entry.beats}/${config.maxBeats}`,
|
|
4122
|
+
result: "sent"
|
|
4123
|
+
});
|
|
4124
|
+
results.push({
|
|
4125
|
+
sessionId,
|
|
4126
|
+
result: "sent"
|
|
4127
|
+
});
|
|
4128
|
+
} else {
|
|
4129
|
+
d.ledger({
|
|
4130
|
+
...ledgerBase,
|
|
4131
|
+
beat: `${entry.beats}/${config.maxBeats}`,
|
|
4132
|
+
result: "skipped:send-failed"
|
|
4133
|
+
});
|
|
4134
|
+
results.push({
|
|
4135
|
+
sessionId,
|
|
4136
|
+
result: "skipped:send-failed"
|
|
4137
|
+
});
|
|
4138
|
+
}
|
|
4139
|
+
if (changed) {
|
|
4140
|
+
state[sessionId] = entry;
|
|
4141
|
+
anyChanged = true;
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
if (anyChanged) d.saveState(state);
|
|
4145
|
+
return results;
|
|
4146
|
+
}
|
|
4147
|
+
|
|
3174
4148
|
//#endregion
|
|
3175
4149
|
//#region src/hooks/ts/lib/context-handover-cache.ts
|
|
3176
4150
|
/**
|
|
@@ -3203,5 +4177,5 @@ function writeContextHandoverCache(cache) {
|
|
|
3203
4177
|
}
|
|
3204
4178
|
|
|
3205
4179
|
//#endregion
|
|
3206
|
-
export {
|
|
3207
|
-
//# sourceMappingURL=context-handover-cache-
|
|
4180
|
+
export { vaultIndexInProgress as $, lastIndexTime as A, formatDetectionJson as At, setIndexSchedulerTimer as B, cacheKeepaliveTimer as C, upsertSession as Ct, indexInProgress as D, transcriptFiles as Dt, embedSchedulerTimer as E, scanTranscriptFolders as Et, setCacheKeepaliveTimer as F, setRegistryBackend as G, setLastIndexTime as H, setDaemonConfig as I, setStorageBackend as J, setShutdownRequested as K, setEmbedInProgress as L, notificationConfig as M, passFailures as N, indexSchedulerTimer as O, detectProject as Ot, registryBackend as P, storageBackend as Q, setEmbedSchedulerTimer as R, spawnSummarizer as S, upsertProject as St, embedInProgress as T, findMovedProjects as Tt, setLastVaultIndexTime as U, setLastEmbedTime as V, setNotificationConfig as W, shutdownRequested as X, setVaultIndexInProgress as Y, startTime as Z, contentToText as _, loadScanConfig as _t, sessionKeepaliveLedgerSummary as a, findLatestNote as at, handleSessionSummary as b, resolveHome as bt, cleanup as c, addWorkToSessionNote as ct, getStats as d, getCurrentNotePath as dt, extractAndStoreTriples$1 as et, hasPendingOrProcessingOfType as f, archiveSessionFilesToSessionsDir as ft, migrateWorkQueue as g, cmdScan as gt, markFailed as h, migrateIdentityFile as ht, runSessionKeepaliveTick as i, applyContinue as it, lastVaultIndexTime as j, lastEmbedTime as k, formatDetection as kt, dequeue as l, appendCheckpoint as lt, markCompleted as m, buildWakeupContext as mt, writeContextHandoverCache as n, updateTodoContinue as nt, parseSessionUsage as o, findNotesDir as ot, loadQueue as p, findNotesDir$1 as pt, setStartTime as q, loadSessionKeepaliveState as r, appendCheckpointToNote as rt, totalUsageTokens as s, readBodyFile as st, readContextHandoverCache as t, sessionIdFromTranscript as tt, enqueue as u, finalizeSessionNote as ut, findLatestJsonl as v, migrateScanConfig as vt, daemonConfig as w, claudeProjectsDir as wt, migrateSummaryCooldowns as x, saveScanConfig as xt, getGitContext as y, performScan as yt, setIndexInProgress as z };
|
|
4181
|
+
//# sourceMappingURL=context-handover-cache-9PGvXRIw.mjs.map
|