feat(pi-hermes-memory): 导入上游 v0.9.10 源码快照

从官方 Git 仓库导入 chandra447/pi-hermes-memory 在 tag v0.9.10
(df624056a2e8c68bc89ba0a81e1c120856ec9665,2026-10-02)的源码快照。
本次仅导入源码,尚未接入组合包:package.json、加载顺序、依赖与权限策略
均未改动。

- 原样导入 src/、tests/、docs/、scripts/、README、CHANGELOG、PLAN、
  AGENTS 与 LICENSE;该快照共 156 个上游文件。
- 剔除嵌套 .git、上游 .github 工作流、.ralph/、.claude/、node_modules、
  覆盖率与构建产物。
- 新增中文 UPSTREAM.md,记录来源,以及启用前必须完成的待决事项:
  在 pi-compaction 之前注册加载顺序、better-sqlite3 版本与根 allowScripts
  对齐、strip-ansi 解析确认。
- 说明选择 tag 而非 main 的原因:导入时上游 main 已领先至
  24fcd8ad26fc27badb8d7dbaf8034645de07b893(2026-10-11),选 tag 是为
  对应一个已发布、可复现的版本。

尚未接线。启用前还需反转 tests/extension-load.test.ts 中"不得加载
Hermes"的断言,并同步 pi-tool-search 分组、权限策略与文档。

验证:快照 156 个上游文件全部导入,目录内仅多出 UPSTREAM.md 一份;
已确认与上游 main 内容存在差异,证明取的是 tag 而非 main。
This commit is contained in:
云服务部-叶林立
2026-10-13 23:19:35 -07:00
parent 879fc5d37f
commit 312bb2ba5a
157 changed files with 52287 additions and 0 deletions
@@ -0,0 +1,247 @@
import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { DatabaseManager } from '../../src/store/db.js';
import {
scheduleSessionBackfill,
joinSessionBackfill,
waitForSessionBackfill,
type SessionBackfillState,
} from '../../src/handlers/session-backfill.js';
import { indexAllSessions, touchBackfillTimestamp } from '../../src/store/session-indexer.js';
function writeJsonlSession(sessionsDir: string, projectDir: string, sessionId: string, text = 'Hello from backfill'): void {
const projDir = path.join(sessionsDir, projectDir);
fs.mkdirSync(projDir, { recursive: true });
const lines = [
JSON.stringify({ type: 'session', id: sessionId, timestamp: '2026-05-03T00:00:00Z', cwd: `/work/${projectDir}` }),
JSON.stringify({
type: 'message',
id: `${sessionId}-m1`,
parentId: null,
timestamp: '2026-05-03T00:01:00Z',
message: { role: 'user', content: [{ type: 'text', text }], timestamp: Date.now() },
}),
];
fs.writeFileSync(path.join(projDir, `${sessionId}.jsonl`), lines.join('\n'));
}
describe('session backfill handler', () => {
let tmpDir: string;
let sessionsDir: string;
let dbManager: DatabaseManager;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'session-backfill-test-'));
sessionsDir = path.join(tmpDir, 'sessions');
dbManager = new DatabaseManager(path.join(tmpDir, 'memory'));
});
afterEach(() => {
dbManager.close();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('schedules backfill without indexing synchronously, then indexes unindexed sessions', async () => {
writeJsonlSession(sessionsDir, 'project-a', 's1');
const callbacks: (() => void)[] = [];
const state: SessionBackfillState = { inProgress: false, promise: null };
const scheduled = scheduleSessionBackfill(dbManager, sessionsDir, {
state,
setTimeoutFn: (callback) => {
callbacks.push(callback);
return 0;
},
});
assert.equal(scheduled, true);
assert.equal(callbacks.length, 1);
assert.equal(dbManager.getStats().sessions, 0, 'session_start should not index synchronously');
const promise = state.promise;
assert.ok(promise);
callbacks[0]();
await promise;
assert.equal(dbManager.getStats().sessions, 1);
assert.equal(dbManager.getStats().messages, 1);
});
it('does not schedule backfill when counts match and timestamp is recent', () => {
writeJsonlSession(sessionsDir, 'project-a', 's1');
indexAllSessions(dbManager, sessionsDir);
touchBackfillTimestamp(dbManager);
const callbacks: (() => void)[] = [];
const state: SessionBackfillState = { inProgress: false, promise: null };
const scheduled = scheduleSessionBackfill(dbManager, sessionsDir, {
state,
setTimeoutFn: (callback) => {
callbacks.push(callback);
return 0;
},
});
assert.equal(scheduled, false);
assert.equal(callbacks.length, 0);
assert.equal(state.inProgress, false);
});
it('keeps manual indexAllSessions idempotent with auto-backfill', async () => {
writeJsonlSession(sessionsDir, 'project-a', 's1');
const state: SessionBackfillState = { inProgress: false, promise: null };
const scheduled = scheduleSessionBackfill(dbManager, sessionsDir, {
state,
setTimeoutFn: (callback) => {
queueMicrotask(callback);
return 0;
},
});
assert.equal(scheduled, true);
await state.promise;
const manualResult = indexAllSessions(dbManager, sessionsDir);
assert.equal(manualResult.sessionsProcessed, 1);
assert.equal(manualResult.sessionsSkipped, 1);
assert.equal(manualResult.sessionsIndexed, 0);
});
it('does not mark backfill complete when startup parse limit is reached', async () => {
const state: SessionBackfillState = { inProgress: false, promise: null };
let touched = false;
const notifications: { message: string; level: string }[] = [];
const scheduled = scheduleSessionBackfill(dbManager, sessionsDir, {
state,
needsBackfillFn: () => true,
indexSessionsFn: () => ({
sessionsProcessed: 1,
sessionsIndexed: 1,
sessionsSkipped: 0,
messagesIndexed: 1,
errors: [],
reachedLimit: true,
}),
touchBackfillTimestampFn: () => { touched = true; },
notify: (message, level) => notifications.push({ message, level }),
setTimeoutFn: (callback) => {
queueMicrotask(callback);
return 0;
},
});
assert.equal(scheduled, true);
await state.promise;
assert.equal(touched, false);
assert.equal(notifications[0].level, 'warning');
assert.match(notifications[0].message, /startup limit reached/);
});
it('scheduled task is best-effort and does not reject when indexing throws', async () => {
const state: SessionBackfillState = { inProgress: false, promise: null };
const notifications: { message: string; level: string }[] = [];
const scheduled = scheduleSessionBackfill(dbManager, sessionsDir, {
state,
needsBackfillFn: () => true,
indexSessionsFn: () => {
throw new Error('boom');
},
notify: (message, level) => notifications.push({ message, level }),
setTimeoutFn: (callback) => {
queueMicrotask(callback);
return 0;
},
});
assert.equal(scheduled, true);
await state.promise;
assert.equal(state.inProgress, false);
assert.match(notifications[0].message, /Session backfill failed: boom/);
assert.equal(notifications[0].level, 'warning');
});
it('shutdown wait resolves true when an in-progress backfill completes before timeout', async () => {
let resolveBackfill!: () => void;
const state: SessionBackfillState = {
inProgress: true,
promise: new Promise<void>((resolve) => {
resolveBackfill = resolve;
}),
};
setTimeout(resolveBackfill, 5);
const completed = await waitForSessionBackfill(100, state);
assert.equal(completed, true);
});
it('shutdown wait resolves false when an in-progress backfill exceeds timeout', async () => {
const state: SessionBackfillState = {
inProgress: true,
promise: new Promise<void>(() => {}),
};
const completed = await waitForSessionBackfill(5, state);
assert.equal(completed, false);
});
it('first-use join does not inherit the five-second shutdown deadline', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const backfill = Promise.withResolvers<void>();
const state: SessionBackfillState = { inProgress: true, promise: backfill.promise };
let joined = false;
const firstUse = joinSessionBackfill(state).then(() => { joined = true; });
const shutdown = waitForSessionBackfill(5000, state);
t.mock.timers.tick(6000);
assert.equal(await shutdown, false);
assert.equal(joined, false);
backfill.resolve();
await firstUse;
assert.equal(joined, true);
});
it('propagates the retention cutoff to the backfill file set', async () => {
// Only an expired file exists (mtime older than the 30-day cutoff). Even
// with recent backfill timestamps, the backfill must not be scheduled
// because the file is outside the retained window.
writeJsonlSession(sessionsDir, 'project-a', 'old');
const oldFile = path.join(sessionsDir, 'project-a', 'old.jsonl');
const cutoffMs = Date.now() - 30 * 24 * 60 * 60 * 1000;
const oldMtime = cutoffMs - 1000;
fs.utimesSync(oldFile, new Date(oldMtime), new Date(oldMtime));
touchBackfillTimestamp(dbManager);
let receivedCutoff: number | undefined;
let indexOptions: { retentionCutoffMs?: number } | undefined;
const state: SessionBackfillState = { inProgress: false, promise: null };
const scheduled = scheduleSessionBackfill(dbManager, sessionsDir, {
state,
retentionCutoffMs: cutoffMs,
setTimeoutFn: () => 0,
// Capture the cutoff that would be forwarded to the store so we prove
// the wiring reaches both the eligibility check and the indexer.
needsBackfillFn: (_db, _dir, _now, cutoff) => {
receivedCutoff = cutoff;
return false;
},
indexSessionsFn: (_db, _dir, options) => {
indexOptions = options;
return { sessionsProcessed: 0, sessionsIndexed: 0, sessionsSkipped: 0, messagesIndexed: 0, reachedLimit: undefined, errors: [] };
},
});
// needsBackfill ran and received the cutoff; because it returned false for
// an expired file set, no backfill pass was scheduled.
assert.equal(scheduled, false);
assert.equal(receivedCutoff, cutoffMs);
assert.equal(indexOptions, undefined);
});
});