@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.
@@ -7,6 +7,8 @@
7
7
  * The reader scans for new sessions since the last nightly run
8
8
  * and feeds them to the existing distiller pipeline.
9
9
  */
10
+ import type { CursorMap } from "./capture-cursors.js";
11
+ export type { CursorMap };
10
12
  export interface TranscriptBatch {
11
13
  sessionId: string;
12
14
  projectName: string;
@@ -18,6 +20,25 @@ export interface TranscriptBatch {
18
20
  * `claude-code/<project>`. Lets per-harness readers stamp their own origin.
19
21
  */
20
22
  sourceAgent?: string;
23
+ /**
24
+ * Per-session cursor key (`<prefix>:<sessionId>`) — the capture-cursors.json
25
+ * key whose value gates and advances this session's incremental capture (#189).
26
+ */
27
+ cursorKey: string;
28
+ /** Cursor value the delta starts from (entries already captured before this run). */
29
+ startCursor: number;
30
+ /**
31
+ * Shrink-guard generation for this session — woven into segment ids so
32
+ * post-reset segments can't collide with pre-reset ids on the content-blind
33
+ * server dedup. Advanced back to the store with the cursor.
34
+ */
35
+ generation: number;
36
+ /**
37
+ * End-cursor value for each delta entry (length === entries.length). The
38
+ * packer uses these to land segment boundaries on exact entry boundaries.
39
+ * JSONL: startCursor + i + 1. Hermes: the row's messages.id.
40
+ */
41
+ entryCursors: number[];
21
42
  }
22
43
  /**
23
44
  * Cheap pre-filter: skip CC session FILES with fewer than this many raw JSONL
@@ -36,4 +57,4 @@ export declare const MIN_TRANSCRIPT_ENTRIES = 4;
36
57
  * Read all CC transcripts modified since `since`.
37
58
  * Returns one batch per session file.
38
59
  */
39
- export declare function readCcTranscripts(since: Date, projectsDir?: string): TranscriptBatch[];
60
+ export declare function readCcTranscripts(since: Date, projectsDir?: string, cursors?: CursorMap): TranscriptBatch[];
@@ -32,7 +32,7 @@ const CC_PROJECTS_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude
32
32
  * Read all CC transcripts modified since `since`.
33
33
  * Returns one batch per session file.
34
34
  */
35
- function readCcTranscripts(since, projectsDir = CC_PROJECTS_DIR) {
35
+ function readCcTranscripts(since, projectsDir = CC_PROJECTS_DIR, cursors = {}) {
36
36
  const batches = [];
37
37
  let projectDirs;
38
38
  try {
@@ -74,7 +74,10 @@ function readCcTranscripts(since, projectsDir = CC_PROJECTS_DIR) {
74
74
  // Skip files not modified since last run
75
75
  if (fileStat.mtime <= since)
76
76
  continue;
77
- const batch = parseTranscriptFile(filePath, projectName);
77
+ const sessionId = (0, node_path_1.basename)(filePath, ".jsonl");
78
+ const key = `cc:${sessionId}`;
79
+ const pos = cursors[key] ?? { cursor: 0, gen: 0 };
80
+ const batch = parseTranscriptFile(filePath, projectName, key, pos.cursor, pos.gen);
78
81
  if (batch) {
79
82
  batches.push(batch);
80
83
  }
@@ -83,10 +86,15 @@ function readCcTranscripts(since, projectsDir = CC_PROJECTS_DIR) {
83
86
  return batches;
84
87
  }
85
88
  /**
86
- * Parse a single .jsonl transcript file into a batch.
87
- * Returns null if the file has too few meaningful entries.
89
+ * Parse a single .jsonl transcript file into a delta batch.
90
+ *
91
+ * Returns null if the file has too few meaningful entries (whole-file gate) or
92
+ * the cursor already covers everything (nothing new since last capture, #189).
93
+ *
94
+ * @param cursorKey capture-cursors.json key for this session
95
+ * @param startCursor entries already captured (delta = entries.slice(startCursor))
88
96
  */
89
- function parseTranscriptFile(filePath, projectName) {
97
+ function parseTranscriptFile(filePath, projectName, cursorKey, startCursor, generation) {
90
98
  let raw;
91
99
  try {
92
100
  raw = (0, node_fs_1.readFileSync)(filePath, "utf-8");
@@ -98,30 +106,55 @@ function parseTranscriptFile(filePath, projectName) {
98
106
  if (lines.length < exports.MIN_TRANSCRIPT_ENTRIES)
99
107
  return null; // degenerate/empty file
100
108
  const entries = [];
101
- let lastTimestamp = "";
109
+ const timestamps = [];
102
110
  for (const line of lines) {
103
111
  try {
104
112
  const entry = JSON.parse(line);
105
113
  entries.push(entry);
106
- if (entry.timestamp) {
107
- lastTimestamp = entry.timestamp;
108
- }
114
+ timestamps.push(typeof entry.timestamp === "string" ? entry.timestamp : "");
109
115
  }
110
116
  catch {
111
- // Skip malformed lines
117
+ // Skip malformed lines — a permanently-malformed line is skipped
118
+ // identically every run, and a partial trailing write fails JSON.parse
119
+ // now and parses (at the same index) once fully flushed.
112
120
  }
113
121
  }
122
+ // Whole-file degeneracy gate stays on the full parse, not the delta.
114
123
  if (entries.length < exports.MIN_TRANSCRIPT_ENTRIES)
115
124
  return null;
116
- // Extract session ID from filename (UUID.jsonl)
117
- const sessionId = (0, node_path_1.basename)(filePath, ".jsonl");
125
+ // Shrink guard: a truncated/rotated file with fewer entries than the stored
126
+ // cursor reset to 0 AND bump the generation. The generation is woven into
127
+ // the segment id downstream so the fresh file's segments can never collide
128
+ // with the pre-reset ids on the server's content-blind dedup (fix 8).
129
+ let start = startCursor;
130
+ let gen = generation;
131
+ if (start > entries.length) {
132
+ start = 0;
133
+ gen = generation + 1;
134
+ }
135
+ const delta = entries.slice(start);
136
+ if (delta.length === 0)
137
+ return null; // cursor already covers the whole file
138
+ // Per-entry end cursors: entry i (0-based in the delta) ends at start+i+1.
139
+ const entryCursors = delta.map((_, i) => start + i + 1);
140
+ // Date from the LAST timestamped entry in the delta (per-night created_at for
141
+ // multi-day sessions), falling back to today.
142
+ let lastTimestamp = "";
143
+ for (let i = start; i < entries.length; i++) {
144
+ if (timestamps[i])
145
+ lastTimestamp = timestamps[i];
146
+ }
118
147
  return {
119
- sessionId,
148
+ sessionId: (0, node_path_1.basename)(filePath, ".jsonl"),
120
149
  projectName,
121
150
  date: lastTimestamp
122
151
  ? lastTimestamp.slice(0, 10)
123
152
  : new Date().toISOString().slice(0, 10),
124
- entries,
153
+ entries: delta,
154
+ cursorKey,
155
+ startCursor: start,
156
+ generation: gen,
157
+ entryCursors,
125
158
  };
126
159
  }
127
160
  /**
package/dist/types.d.ts CHANGED
@@ -115,6 +115,16 @@ export interface ConsolidationReport {
115
115
  calls_by_stage: Record<string, number>;
116
116
  };
117
117
  }
118
+ /**
119
+ * A single per-stage override inside the nested `models` server-config block.
120
+ * Consumed by `applyModelsBlock` (llm.ts), which re-exports this type.
121
+ */
122
+ export interface ModelTierOverride {
123
+ model?: string;
124
+ baseUrl?: string;
125
+ apiKey?: string;
126
+ provider?: string;
127
+ }
118
128
  /** Plugin configuration from openclaw.plugin.json configSchema. */
119
129
  export interface HicortexConfig {
120
130
  licenseKey?: string;
@@ -145,6 +155,16 @@ export interface HicortexConfig {
145
155
  classifyApiKey?: string;
146
156
  /** Optional provider for the classify endpoint (defaults to the base provider). */
147
157
  classifyProvider?: string;
158
+ /**
159
+ * Server config (NOT an OC-plugin key): nested per-stage model overrides.
160
+ * `{ score|distill|reflect|classify: { model?, baseUrl?, apiKey?, provider? } }`.
161
+ * Normalized onto the flat `llm*` / `distill*` / `reflect*` / `classify*`
162
+ * keys at read time (see applyModelsBlock in llm.ts); nested wins, and the
163
+ * flat keys remain supported at lower precedence. Happy path is a single model via
164
+ * `llmModel`; use this block only for per-stage routing. `score.provider` is
165
+ * ignored (base provider comes from llmBackend).
166
+ */
167
+ models?: Record<string, ModelTierOverride>;
148
168
  /** @deprecated Consolidation is owned by the server nightly. */
149
169
  consolidateHour?: number;
150
170
  /** @deprecated The OC plugin no longer opens its own database. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Self-learning memory for AI agents \u2014 experience captured automatically, distilled into lessons overnight, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, and Pi.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {