@gamaze/hicortex 0.13.0 → 0.13.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/README.md +28 -7
- package/dist/capture-cursors.d.ts +73 -0
- package/dist/capture-cursors.js +133 -0
- package/dist/capture.d.ts +124 -0
- package/dist/capture.js +386 -0
- package/dist/cli.js +13 -1
- package/dist/distiller.d.ts +27 -1
- package/dist/distiller.js +81 -9
- package/dist/hermes-transcript-reader.d.ts +6 -2
- package/dist/hermes-transcript-reader.js +41 -4
- package/dist/init.d.ts +8 -0
- package/dist/init.js +15 -2
- package/dist/llm.d.ts +24 -1
- package/dist/llm.js +119 -2
- package/dist/mcp-server.js +57 -19
- package/dist/nightly-status.js +4 -1
- package/dist/nightly.d.ts +2 -0
- package/dist/nightly.js +224 -168
- package/dist/oc-transcript-reader.d.ts +3 -2
- package/dist/oc-transcript-reader.js +5 -3
- package/dist/pi-transcript-reader.d.ts +5 -8
- package/dist/pi-transcript-reader.js +36 -8
- package/dist/transcript-reader.d.ts +22 -1
- package/dist/transcript-reader.js +47 -14
- package/dist/types.d.ts +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ The plugin connects to `http://127.0.0.1:8787` by default. For a remote server,
|
|
|
65
65
|
|
|
66
66
|
| When | What | How |
|
|
67
67
|
|------|------|-----|
|
|
68
|
-
| Agent start |
|
|
68
|
+
| Agent start | Standing context (`## Context`) + recent lessons fetched fresh and injected | CC SessionStart hook (calls `hicortex lessons-context`) / Hermes plugin `system_prompt_block` (+ `prefetch` recall) / OC `before_agent_start` hook |
|
|
69
69
|
| Nightly | Denoise sessions → POST /distill → server distills + embeds + stores → consolidate (score, reflect, link, decay) | Automatic pipeline — no manual steps |
|
|
70
70
|
|
|
71
71
|
## Memory Domains & Tags
|
|
@@ -126,7 +126,7 @@ Beyond auto-distilled memories and lessons, Hicortex holds a **hand-edited conte
|
|
|
126
126
|
|
|
127
127
|
- **Storage:** plain files on the server at `~/.hicortex/context/*.md` — one file per section (recommended starter sections `user.md` + `rules.md`, which you create — nothing is pre-populated; add more by dropping in a file). It lives outside the memories table; consolidation never touches it.
|
|
128
128
|
- **Edit:** the web editor at `http://localhost:8787/context/ui` (one tab per section, Save), or the CLI `hicortex context show [name]` / `hicortex context edit <name>`.
|
|
129
|
-
- **Delivery:** injected into the harnesses listed in `contextClients` (default `["cc"]
|
|
129
|
+
- **Delivery:** injected into the harnesses listed in `contextClients` (default `["cc"]`; `"all"` or any subset of `cc`/`hermes`/`oc` — all three are supported since 0.13).
|
|
130
130
|
- **Deletion** is filesystem-only — remove the file on the server (as the daemon user).
|
|
131
131
|
|
|
132
132
|
### Per-agent context (0.13)
|
|
@@ -191,12 +191,9 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
|
|
|
191
191
|
|-------|-------------|
|
|
192
192
|
| `mode` | `"server"` (default) or `"client"` |
|
|
193
193
|
| `serverUrl` | Remote server URL (client mode) |
|
|
194
|
-
| `llmModel` |
|
|
195
|
-
| `
|
|
196
|
-
| `distillBaseUrl` | Separate Ollama instance for distillation (server mode) |
|
|
194
|
+
| `llmModel` | The one model the whole pipeline uses (server mode). One model is the happy path; route individual stages with `models` — see [Advanced: per-stage models](#advanced-per-stage-models) |
|
|
195
|
+
| `models` | Optional nested per-stage model overrides (`score`/`distill`/`reflect`/`classify`) — see [Advanced: per-stage models](#advanced-per-stage-models) |
|
|
197
196
|
| `distillFallback` | `"strict"` (default) — abort on remote distill failure, retry next run; `"local"` — fall back to base model (lower quality, 0.9.0 behaviour) |
|
|
198
|
-
| `reflectModel` | Model for nightly reflection — largest available (server mode) |
|
|
199
|
-
| `reflectBaseUrl` | Separate Ollama instance for reflection (server mode) |
|
|
200
197
|
| `authToken` | Bearer token for endpoint auth. Generated on first `init` in server mode. Find the active token with `hicortex status` or in `~/.hicortex/config.json`. |
|
|
201
198
|
| `licenseKey` | Commercial license key (optional; for display in `hicortex status`) |
|
|
202
199
|
| `domains` | Your memory domain list (`[{name, description}]`). Scaffolded by `init`; edit freely — see [Memory Domains & Tags](#memory-domains--tags) |
|
|
@@ -263,6 +260,30 @@ LLM selection is **user-controlled**: `npx @gamaze/hicortex init` detects candid
|
|
|
263
260
|
|
|
264
261
|
If no LLM is configured, the server starts in **recall-only mode**: search, lessons, and context work; `/distill` and consolidation are disabled. Run `npx @gamaze/hicortex init` to configure.
|
|
265
262
|
|
|
263
|
+
### Advanced: per-stage models
|
|
264
|
+
|
|
265
|
+
The happy path is **one model** (`llmModel`) for the whole pipeline. If you want to route the four pipeline stages to different models or endpoints, add a nested `models` block to `~/.hicortex/config.json`:
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"llmBackend": "ollama",
|
|
270
|
+
"llmModel": "qwen3.5:4b",
|
|
271
|
+
"models": {
|
|
272
|
+
"score": { "model": "qwen3.5:4b" },
|
|
273
|
+
"distill": { "model": "qwen3.5:35b-a3b", "baseUrl": "http://gpu-box:11434" },
|
|
274
|
+
"reflect": { "model": "qwen3.5:35b-a3b", "baseUrl": "http://gpu-box:11434" },
|
|
275
|
+
"classify": { "model": "gemma4-31b", "baseUrl": "http://gpu-box:11434" }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The four tiers: `score` (importance scoring — this **is** the base model), `distill` (session distillation, 9b+ recommended), `reflect` (nightly reflection, largest available), `classify` (memory-domain tagging). Each accepts `model`, `baseUrl`, `apiKey`, and `provider`. Omitting a tier inherits: `distill` and `reflect` fall back to the **base** (`score`) model; **`classify` falls back to the `reflect` tier** (not the base — `classify` delegates to the reflect path when unset).
|
|
281
|
+
|
|
282
|
+
- **Flat keys still work.** `distillModel`/`distillBaseUrl`/`reflectModel`/`reflectBaseUrl`/`classifyModel`/`classifyBaseUrl` remain supported at **lower precedence** — a `models` entry wins over the flat key of the same name.
|
|
283
|
+
- **A tier's `apiKey`/`provider` require the tier's own `baseUrl`.** They only take effect when the tier sets `baseUrl`; set on a tier without a `baseUrl` they are **ignored with a warning** (a bare `models.reflect: { model, apiKey }` would otherwise silently bill to the base key). Set `provider` when a tier's `baseUrl` points at a different provider type than the base (e.g. an OpenAI-compatible API while the base is Ollama); set `apiKey` for an API-provider tier over an Ollama base, whose base key is empty (`""`).
|
|
284
|
+
- **`init` writes a flat `llmModel`.** The already-configured guard now recognizes a nested-only config (`models.score`), so re-running `init` on one is a no-op. But if a config has **both** a flat `llmModel` and a `models.score.model`, the nested value **shadows** the flat one (nested > flat) — keep the model in one place.
|
|
285
|
+
- **`score.provider` (and `score.apiKey` on an Ollama base) are ignored** (a warning is logged): the base provider comes from `llmBackend` (or is auto-detected from the base endpoint), and the Ollama base path sends no api key.
|
|
286
|
+
|
|
266
287
|
## Database
|
|
267
288
|
|
|
268
289
|
Canonical location: `~/.hicortex/hicortex.db`. The OC plugin no longer owns its own database — it is a thin client to the server. Previously, OC installations at `~/.openclaw/data/hicortex.db` were migrated automatically on upgrade; this migration path remains in the server's `resolveDbPath` for any pre-0.10.0 installations.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session capture cursors (#189).
|
|
3
|
+
*
|
|
4
|
+
* The client is the source of truth for "how much of each session has already
|
|
5
|
+
* been captured". A cursor is advanced ONLY after the server confirms the
|
|
6
|
+
* corresponding segment(s) were ingested, so a crash between POST and cursor
|
|
7
|
+
* write can at worst cause a bounded, idempotent re-send — never silent loss.
|
|
8
|
+
*
|
|
9
|
+
* Storage: a SEPARATE small file `<hicortex-home>/capture-cursors.json`
|
|
10
|
+
* (NOT state.json — that file carries the large moduleIndex, and per-session
|
|
11
|
+
* whole-file rewrites there would be an avoidable corruption/IO surface). Same
|
|
12
|
+
* temp+rename atomic write discipline as state.ts.
|
|
13
|
+
*
|
|
14
|
+
* Cursor unit is reader-defined:
|
|
15
|
+
* - JSONL readers (CC/Pi/OC): count of successfully-PARSED entries consumed.
|
|
16
|
+
* - Hermes: max `messages.id` consumed (INTEGER PRIMARY KEY AUTOINCREMENT —
|
|
17
|
+
* never reused, strictly increasing).
|
|
18
|
+
*
|
|
19
|
+
* `gen` (generation) is bumped by a reader's shrink-guard when a source file is
|
|
20
|
+
* truncated/rotated below the stored cursor. It is woven into the segment id
|
|
21
|
+
* (`g<gen>.<start>-<end>`) so post-reset segments can NEVER collide with a
|
|
22
|
+
* pre-reset id on the server's content-blind segment-exact dedup — a collision
|
|
23
|
+
* there would be silent LOSS, not the intended dup-over-loss (#189 review, fix 8).
|
|
24
|
+
*
|
|
25
|
+
* Keys are `<prefix>:<sessionId>`:
|
|
26
|
+
* cc:<sid> pi:<sid> oc:<agentId>:<sid> hermes:<profile>:<sid>
|
|
27
|
+
*/
|
|
28
|
+
export interface CursorEntry {
|
|
29
|
+
/** Reader-defined cursor value (entry count for JSONL, max id for Hermes). */
|
|
30
|
+
cursor: number;
|
|
31
|
+
/** Shrink-guard generation — woven into segment ids to avoid post-reset id collisions. */
|
|
32
|
+
gen: number;
|
|
33
|
+
/** ISO timestamp of the last advance — used for 90-day pruning. */
|
|
34
|
+
updated: string;
|
|
35
|
+
}
|
|
36
|
+
export type CursorFile = Record<string, CursorEntry>;
|
|
37
|
+
/** Snapshot of a session's captured position, passed to the readers. */
|
|
38
|
+
export interface CursorPosition {
|
|
39
|
+
cursor: number;
|
|
40
|
+
gen: number;
|
|
41
|
+
}
|
|
42
|
+
/** Map of cursor key → captured position. */
|
|
43
|
+
export type CursorMap = Record<string, CursorPosition>;
|
|
44
|
+
/** Load the cursor file. Missing/corrupt → empty (every key defaults to 0). */
|
|
45
|
+
export declare function loadCursors(stateDir?: string): CursorFile;
|
|
46
|
+
/**
|
|
47
|
+
* Atomically persist the cursor file (write-temp + rename). THROWS on failure —
|
|
48
|
+
* a persistent inability to record cursors must surface as a transient failure
|
|
49
|
+
* (hold the watermark), never a silent warn-and-continue that re-captures the
|
|
50
|
+
* same content every night (#189 review, fix 7).
|
|
51
|
+
*/
|
|
52
|
+
export declare function saveCursors(file: CursorFile, stateDir?: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* A read-once, write-on-advance cursor store. Single-flight (see
|
|
55
|
+
* acquireCaptureLock in capture.ts) guarantees no concurrent writer, so an
|
|
56
|
+
* in-memory copy flushed atomically on each advance is safe.
|
|
57
|
+
*/
|
|
58
|
+
export interface CursorStore {
|
|
59
|
+
/** Current position for `key` ({cursor:0, gen:0} when never captured). */
|
|
60
|
+
get(key: string): CursorPosition;
|
|
61
|
+
/** Advance `key` to `cursor` at `gen` and persist. Throws if the write fails. */
|
|
62
|
+
advance(key: string, cursor: number, gen: number): void;
|
|
63
|
+
/** Snapshot of key → position for passing to readers. */
|
|
64
|
+
map(): CursorMap;
|
|
65
|
+
}
|
|
66
|
+
/** Open a file-backed cursor store for `stateDir`. */
|
|
67
|
+
export declare function openCursorStore(stateDir?: string): CursorStore;
|
|
68
|
+
/**
|
|
69
|
+
* Drop cursor entries older than `days` (default 90). Best-effort: a prune-write
|
|
70
|
+
* failure is logged, not thrown (prune runs only after a clean nightly and must
|
|
71
|
+
* not fail the run). Returns the number pruned.
|
|
72
|
+
*/
|
|
73
|
+
export declare function pruneCursors(stateDir?: string, days?: number): number;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Per-session capture cursors (#189).
|
|
4
|
+
*
|
|
5
|
+
* The client is the source of truth for "how much of each session has already
|
|
6
|
+
* been captured". A cursor is advanced ONLY after the server confirms the
|
|
7
|
+
* corresponding segment(s) were ingested, so a crash between POST and cursor
|
|
8
|
+
* write can at worst cause a bounded, idempotent re-send — never silent loss.
|
|
9
|
+
*
|
|
10
|
+
* Storage: a SEPARATE small file `<hicortex-home>/capture-cursors.json`
|
|
11
|
+
* (NOT state.json — that file carries the large moduleIndex, and per-session
|
|
12
|
+
* whole-file rewrites there would be an avoidable corruption/IO surface). Same
|
|
13
|
+
* temp+rename atomic write discipline as state.ts.
|
|
14
|
+
*
|
|
15
|
+
* Cursor unit is reader-defined:
|
|
16
|
+
* - JSONL readers (CC/Pi/OC): count of successfully-PARSED entries consumed.
|
|
17
|
+
* - Hermes: max `messages.id` consumed (INTEGER PRIMARY KEY AUTOINCREMENT —
|
|
18
|
+
* never reused, strictly increasing).
|
|
19
|
+
*
|
|
20
|
+
* `gen` (generation) is bumped by a reader's shrink-guard when a source file is
|
|
21
|
+
* truncated/rotated below the stored cursor. It is woven into the segment id
|
|
22
|
+
* (`g<gen>.<start>-<end>`) so post-reset segments can NEVER collide with a
|
|
23
|
+
* pre-reset id on the server's content-blind segment-exact dedup — a collision
|
|
24
|
+
* there would be silent LOSS, not the intended dup-over-loss (#189 review, fix 8).
|
|
25
|
+
*
|
|
26
|
+
* Keys are `<prefix>:<sessionId>`:
|
|
27
|
+
* cc:<sid> pi:<sid> oc:<agentId>:<sid> hermes:<profile>:<sid>
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.loadCursors = loadCursors;
|
|
31
|
+
exports.saveCursors = saveCursors;
|
|
32
|
+
exports.openCursorStore = openCursorStore;
|
|
33
|
+
exports.pruneCursors = pruneCursors;
|
|
34
|
+
const paths_js_1 = require("./paths.js");
|
|
35
|
+
const node_fs_1 = require("node:fs");
|
|
36
|
+
const node_path_1 = require("node:path");
|
|
37
|
+
const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
|
|
38
|
+
const CURSORS_FILE = "capture-cursors.json";
|
|
39
|
+
/** Coerce a stored cursor value to a valid non-negative integer (fix 13). */
|
|
40
|
+
function sanitizeCursor(raw, key) {
|
|
41
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) {
|
|
42
|
+
return Math.floor(raw);
|
|
43
|
+
}
|
|
44
|
+
console.warn(`[hicortex] capture-cursors: ignoring invalid cursor for ${key} (${JSON.stringify(raw)}) — treating as 0`);
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
function sanitizeGen(raw) {
|
|
48
|
+
return typeof raw === "number" && Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 0;
|
|
49
|
+
}
|
|
50
|
+
/** Load the cursor file. Missing/corrupt → empty (every key defaults to 0). */
|
|
51
|
+
function loadCursors(stateDir = HICORTEX_HOME) {
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(stateDir, CURSORS_FILE), "utf-8"));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
if (!parsed || typeof parsed !== "object")
|
|
60
|
+
return {};
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const [key, val] of Object.entries(parsed)) {
|
|
63
|
+
if (!val || typeof val !== "object")
|
|
64
|
+
continue;
|
|
65
|
+
const v = val;
|
|
66
|
+
out[key] = {
|
|
67
|
+
cursor: sanitizeCursor(v.cursor, key),
|
|
68
|
+
gen: sanitizeGen(v.gen),
|
|
69
|
+
updated: typeof v.updated === "string" ? v.updated : new Date().toISOString(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Atomically persist the cursor file (write-temp + rename). THROWS on failure —
|
|
76
|
+
* a persistent inability to record cursors must surface as a transient failure
|
|
77
|
+
* (hold the watermark), never a silent warn-and-continue that re-captures the
|
|
78
|
+
* same content every night (#189 review, fix 7).
|
|
79
|
+
*/
|
|
80
|
+
function saveCursors(file, stateDir = HICORTEX_HOME) {
|
|
81
|
+
(0, node_fs_1.mkdirSync)(stateDir, { recursive: true });
|
|
82
|
+
const path = (0, node_path_1.join)(stateDir, CURSORS_FILE);
|
|
83
|
+
const tmp = `${path}.tmp`;
|
|
84
|
+
(0, node_fs_1.writeFileSync)(tmp, JSON.stringify(file, null, 2));
|
|
85
|
+
(0, node_fs_1.renameSync)(tmp, path);
|
|
86
|
+
}
|
|
87
|
+
/** Open a file-backed cursor store for `stateDir`. */
|
|
88
|
+
function openCursorStore(stateDir = HICORTEX_HOME) {
|
|
89
|
+
const file = loadCursors(stateDir);
|
|
90
|
+
return {
|
|
91
|
+
get(key) {
|
|
92
|
+
const e = file[key];
|
|
93
|
+
return { cursor: e?.cursor ?? 0, gen: e?.gen ?? 0 };
|
|
94
|
+
},
|
|
95
|
+
advance(key, cursor, gen) {
|
|
96
|
+
file[key] = { cursor, gen, updated: new Date().toISOString() };
|
|
97
|
+
saveCursors(file, stateDir); // throws on failure — caller holds the watermark
|
|
98
|
+
},
|
|
99
|
+
map() {
|
|
100
|
+
const out = {};
|
|
101
|
+
for (const [k, v] of Object.entries(file))
|
|
102
|
+
out[k] = { cursor: v.cursor, gen: v.gen };
|
|
103
|
+
return out;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Drop cursor entries older than `days` (default 90). Best-effort: a prune-write
|
|
109
|
+
* failure is logged, not thrown (prune runs only after a clean nightly and must
|
|
110
|
+
* not fail the run). Returns the number pruned.
|
|
111
|
+
*/
|
|
112
|
+
function pruneCursors(stateDir = HICORTEX_HOME, days = 90) {
|
|
113
|
+
const file = loadCursors(stateDir);
|
|
114
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
115
|
+
let pruned = 0;
|
|
116
|
+
for (const [key, entry] of Object.entries(file)) {
|
|
117
|
+
const t = Date.parse(entry.updated);
|
|
118
|
+
if (!Number.isNaN(t) && t < cutoff) {
|
|
119
|
+
delete file[key];
|
|
120
|
+
pruned++;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (pruned > 0) {
|
|
124
|
+
try {
|
|
125
|
+
saveCursors(file, stateDir);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
console.warn(`[hicortex] capture-cursors prune write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return pruned;
|
|
133
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental, cursor-aware capture loop (#189).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the two near-identical loops that lived in nightly.ts (server
|
|
5
|
+
* and client mode). Both now share this logic: pack each session's delta into
|
|
6
|
+
* ordered segments below the server's distill cap, POST them in order with a
|
|
7
|
+
* deterministic `segment_id`, and advance the per-session cursor ONLY after
|
|
8
|
+
* server-confirmed success — so a multi-day session grows across nights with no
|
|
9
|
+
* loss and no silent truncation.
|
|
10
|
+
*
|
|
11
|
+
* The POST transport is injected (`post`) so the mode-specific bits (localhost
|
|
12
|
+
* vs remote URL, Authorization header, timeout) stay in nightly.ts and the
|
|
13
|
+
* multi-night simulation can run as a pure unit test with no HTTP listener.
|
|
14
|
+
*/
|
|
15
|
+
import type { TranscriptBatch } from "./transcript-reader.js";
|
|
16
|
+
import type { CursorStore } from "./capture-cursors.js";
|
|
17
|
+
/**
|
|
18
|
+
* Max denoised chars per segment. Kept below the server's 80K distill cap
|
|
19
|
+
* (distiller.ts MAX_TRANSCRIPT_CHARS) with ~20K headroom so NO capture path can
|
|
20
|
+
* hit the silent truncation. LOAD-BEARING for #189 recovery: a re-ingested
|
|
21
|
+
* week-long session is re-sliced into ≤60K segments here instead of being
|
|
22
|
+
* truncated at 80K server-side. (Judgment constant — tunable later.)
|
|
23
|
+
*/
|
|
24
|
+
export declare const SEGMENT_MAX_CHARS = 60000;
|
|
25
|
+
/**
|
|
26
|
+
* Minimum denoised chars for a FRESH whole session (startCursor 0) to be worth
|
|
27
|
+
* capturing — mirrors the long-standing pre-#189 200-char degenerate-session
|
|
28
|
+
* gate. It is applied ONLY to a whole-session capture that denoises to a single
|
|
29
|
+
* sub-200 segment. A delta beyond cursor 0 is always sent, however small: a
|
|
30
|
+
* session's concluding tail must never be held back, because once the session
|
|
31
|
+
* stops growing its mtime never re-crosses the watermark and the tail would be
|
|
32
|
+
* lost forever (#189 review, fix 5).
|
|
33
|
+
*/
|
|
34
|
+
export declare const MIN_SEGMENT_CHARS = 200;
|
|
35
|
+
/** One packed, ready-to-POST segment of a session's delta. */
|
|
36
|
+
export interface Segment {
|
|
37
|
+
/** Denoised text body of the POST. */
|
|
38
|
+
text: string;
|
|
39
|
+
/** Cursor value the segment starts at. */
|
|
40
|
+
segStart: number;
|
|
41
|
+
/** Cursor value the segment ends at. */
|
|
42
|
+
segEnd: number;
|
|
43
|
+
/**
|
|
44
|
+
* Disambiguator for hard-split pieces of a single oversized entry (A2). Empty
|
|
45
|
+
* for normal segments; ".p0", ".p1", … when one entry is split mid-text.
|
|
46
|
+
* Keeps the server's `<sid>#<segment_id>#<i>` keys distinct so no piece's
|
|
47
|
+
* memories collide on the UNIQUE index.
|
|
48
|
+
*/
|
|
49
|
+
idSuffix: string;
|
|
50
|
+
}
|
|
51
|
+
/** The wire body for POST /distill. */
|
|
52
|
+
export interface DistillBody {
|
|
53
|
+
text: string;
|
|
54
|
+
source_agent: string;
|
|
55
|
+
project: string;
|
|
56
|
+
session_id: string;
|
|
57
|
+
segment_id: string;
|
|
58
|
+
session_date: string;
|
|
59
|
+
privacy: string;
|
|
60
|
+
}
|
|
61
|
+
/** Normalized POST result the caller's transport returns. */
|
|
62
|
+
export interface PostResult {
|
|
63
|
+
status: number;
|
|
64
|
+
distilled?: number;
|
|
65
|
+
dropped?: string[];
|
|
66
|
+
skipped?: boolean;
|
|
67
|
+
error?: string;
|
|
68
|
+
}
|
|
69
|
+
export type PostFn = (body: DistillBody) => Promise<PostResult>;
|
|
70
|
+
export interface CaptureOptions {
|
|
71
|
+
post: PostFn;
|
|
72
|
+
cursorStore: CursorStore;
|
|
73
|
+
dryRun?: boolean;
|
|
74
|
+
/** Segment size cap; defaults to SEGMENT_MAX_CHARS. Lowered in tests. */
|
|
75
|
+
segmentMaxChars?: number;
|
|
76
|
+
}
|
|
77
|
+
export interface CaptureResult {
|
|
78
|
+
memoriesIngested: number;
|
|
79
|
+
sessionsSent: number;
|
|
80
|
+
hadTransientFailure: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Set when the loop stopped early on a terminal server response: "limit"
|
|
83
|
+
* (429 memory cap) or "auth" (401). The caller decides watermark handling.
|
|
84
|
+
*/
|
|
85
|
+
stopped?: "limit" | "auth";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Split an already-denoised string into ≤maxChars pieces (A2 hard-split).
|
|
89
|
+
* Prefers paragraph, then line, then hard boundaries — mirrors the distiller's
|
|
90
|
+
* own splitIntoChunks, but WITHOUT its <200-char drop (every piece must survive,
|
|
91
|
+
* dup-over-loss).
|
|
92
|
+
*/
|
|
93
|
+
export declare function hardSplitText(text: string, maxChars?: number): string[];
|
|
94
|
+
/**
|
|
95
|
+
* Pack a session's delta entries into ordered ≤maxChars segments.
|
|
96
|
+
*
|
|
97
|
+
* Sizing uses per-entry denoise lengths plus the "\n\n" joiners (A8) so the
|
|
98
|
+
* estimate matches what the server receives; the actual body is a re-denoise of
|
|
99
|
+
* the grouped entries (extractConversationText) so cleaning/redaction stay
|
|
100
|
+
* coherent. A single entry larger than maxChars is emitted as its own run of
|
|
101
|
+
* hard-split pieces (A2).
|
|
102
|
+
*/
|
|
103
|
+
export declare function packSegments(entries: unknown[], startCursor: number, entryCursors: number[], maxChars?: number): Segment[];
|
|
104
|
+
/**
|
|
105
|
+
* Capture a list of session delta batches: pack, POST in order, advance
|
|
106
|
+
* per-session cursors on success. Segments of one session POST in order; the
|
|
107
|
+
* first hard failure stops THAT session (cursor holds at the last confirmed
|
|
108
|
+
* boundary) while other sessions continue. A 429/401 stops the whole loop.
|
|
109
|
+
*/
|
|
110
|
+
export declare function captureBatches(batches: TranscriptBatch[], opts: CaptureOptions): Promise<CaptureResult>;
|
|
111
|
+
/**
|
|
112
|
+
* Acquire an exclusive capture lock for `stateDir`. Returns a release function,
|
|
113
|
+
* or null if another LIVE, non-stale run holds it after waiting up to `waitMs`.
|
|
114
|
+
*
|
|
115
|
+
* Staleness = dead pid OR lockfile mtime older than LOCK_TTL_MS. A stale lock is
|
|
116
|
+
* reclaimed (with a re-verify + O_EXCL re-race to narrow the TOCTOU window,
|
|
117
|
+
* fix 12). `waitMs` lets the full nightly wait out a transient `--capture-only`
|
|
118
|
+
* overlap instead of dropping the night's capture (fix 10); pass 0 to fail fast.
|
|
119
|
+
*
|
|
120
|
+
* This stops a `nightly` and a `nightly --capture-only` (an encouraged workflow)
|
|
121
|
+
* from running the capture loop concurrently, which would race cursor writes and
|
|
122
|
+
* emit divergent segment boundaries → real duplication.
|
|
123
|
+
*/
|
|
124
|
+
export declare function acquireCaptureLock(stateDir: string, waitMs?: number): Promise<(() => void) | null>;
|