pi-pier 0.1.0

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.
@@ -0,0 +1,115 @@
1
+ /**
2
+ * workspace 历史记录(v1.2,DESIGN.md §13.4)。
3
+ *
4
+ * 任务历史 = append-only JSONL,按 cwd 分区(与 pi 会话分区同构):
5
+ * <agentRoot>/herdr-pi/history/--<cwd>--/history.jsonl
6
+ * 记录不可变:GC 只关 pane,绝不删历史。恢复双保险:sessionFile + launchCommand 快照。
7
+ */
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import { sessionDirName } from './session-tail.ts';
11
+
12
+ /** 旧 short/resident → task;空/缺省 → task;role 名原样。 */
13
+ export function normalizeEntryKind(kind: string | null | undefined): string {
14
+ if (!kind || kind === 'short' || kind === 'resident') return 'task';
15
+ return kind;
16
+ }
17
+
18
+ /** 结算回写:只接受 .jsonl 路径,非法不覆盖(O6)。 */
19
+ export function applyReportedSessionFile(
20
+ current: string | null,
21
+ reported: string | null | undefined,
22
+ ): string | null {
23
+ if (typeof reported === 'string' && /\.jsonl$/i.test(reported)) return reported;
24
+ return current;
25
+ }
26
+
27
+ export interface HistoryEntry {
28
+ taskId: string;
29
+ /** 'task' 或 role 名;读盘时 short/resident 归一为 task。 */
30
+ kind: string;
31
+ paneId: string;
32
+ tabId: string;
33
+ /** v1.3:任务 tab 名(历史回看/复活参考;旧条目缺省)。 */
34
+ tabName?: string;
35
+ workspaceId: string;
36
+ cwd: string;
37
+ description: string;
38
+ /** 子代理会话文件(resume 首选)。 */
39
+ sessionFile: string | null;
40
+ /** 启动命令快照(sessionFile 丢失时重建空会话续接)。 */
41
+ launchCommand: string[];
42
+ status: 'running' | 'settled' | 'consumed' | 'closed';
43
+ outcome?: string | null;
44
+ createdAt: number;
45
+ /** 消费时间(M13c 起随条目落盘;GC TTL 判据的 durable 记录)。 */
46
+ consumedAt?: number | null;
47
+ closedAt?: number | null;
48
+ /** 打回重做/复活时的前代 paneId。 */
49
+ revivedFrom?: string | null;
50
+ }
51
+
52
+ export function historyFilePath(agentRoot: string, cwd: string): string {
53
+ return path.join(agentRoot, 'herdr-pi', 'history', sessionDirName(cwd), 'history.jsonl');
54
+ }
55
+
56
+ /** 容忍式逐行解析(损坏行跳过)。 */
57
+ export function parseHistoryEntries(text: string): HistoryEntry[] {
58
+ const entries: HistoryEntry[] = [];
59
+ for (const line of (text ?? '').split(/\r?\n/)) {
60
+ const t = line.trim();
61
+ if (!t) continue;
62
+ try {
63
+ const obj = JSON.parse(t);
64
+ if (obj && typeof obj === 'object' && typeof obj.taskId === 'string') {
65
+ const raw = obj as HistoryEntry;
66
+ entries.push({ ...raw, kind: normalizeEntryKind(raw.kind) });
67
+ }
68
+ } catch {
69
+ /* 跳过损坏行 */
70
+ }
71
+ }
72
+ return entries;
73
+ }
74
+
75
+ function serializeEntry(entry: HistoryEntry): string {
76
+ return JSON.stringify(entry);
77
+ }
78
+
79
+ /** 读取历史文件;不存在返回 []。 */
80
+ export function readHistory(file: string): HistoryEntry[] {
81
+ try {
82
+ return parseHistoryEntries(fs.readFileSync(file, 'utf8'));
83
+ } catch {
84
+ return [];
85
+ }
86
+ }
87
+
88
+ /** 追加一条(确保目录存在)。 */
89
+ export function appendHistory(file: string, entry: HistoryEntry): void {
90
+ try {
91
+ fs.mkdirSync(path.dirname(file), { recursive: true });
92
+ fs.appendFileSync(file, serializeEntry(entry) + '\n');
93
+ } catch {
94
+ /* 尽力而为:历史写入失败不影响主流程 */
95
+ }
96
+ }
97
+
98
+ /** 折叠:每个 taskId 的全部代际(按 createdAt 升序)。 */
99
+ export function generationsByTask(entries: readonly HistoryEntry[]): Map<string, HistoryEntry[]> {
100
+ const map = new Map<string, HistoryEntry[]>();
101
+ for (const e of entries) {
102
+ const arr = map.get(e.taskId) ?? [];
103
+ arr.push(e);
104
+ map.set(e.taskId, arr);
105
+ }
106
+ for (const arr of map.values()) arr.sort((a, b) => a.createdAt - b.createdAt);
107
+ return map;
108
+ }
109
+
110
+ /** 取 taskId 的最新代(resume 用)。 */
111
+ export function latestGeneration(entries: readonly HistoryEntry[], taskId: string): HistoryEntry | null {
112
+ const gens = generationsByTask(entries).get(taskId);
113
+ if (!gens || gens.length === 0) return null;
114
+ return gens[gens.length - 1];
115
+ }