@hanhnd/agent-kit 1.0.32 → 1.0.34
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/cli/commands/cli-digest-pending.test.js +111 -0
- package/dist/cli/commands/cli-memory.test.js +34 -0
- package/dist/cli/{memory.js → commands/memory.js} +24 -11
- package/dist/cli/index.js +2 -2
- package/dist/{utils → cli/utils}/args.d.ts +2 -1
- package/dist/{kit-cli.js → cli.js} +342 -58
- package/dist/{tools/config.d.ts → core/config/index.d.ts} +2 -7
- package/dist/{tools/config.js → core/config/index.js} +2 -13
- package/dist/core/handoffs/index.d.ts +17 -0
- package/dist/{tools/handoffs.js → core/handoffs/index.js} +15 -7
- package/dist/core/handoffs/index.test.js +88 -0
- package/dist/{tools/security.d.ts → core/security/index.d.ts} +0 -19
- package/dist/{tools/security.js → core/security/index.js} +3 -76
- package/dist/entrypoints/cli.js +11 -0
- package/dist/entrypoints/server.js +37 -0
- package/dist/{tools → mcp}/agent.d.ts +1 -1
- package/dist/{tools → mcp}/agent.js +4 -4
- package/dist/mcp/core.d.ts +17 -0
- package/dist/mcp/core.js +60 -0
- package/dist/mcp/core.test.js +98 -0
- package/dist/{tools → mcp}/integration.js +3 -3
- package/dist/mcp/memory.d.ts +15 -0
- package/dist/{tools → mcp}/memory.js +15 -13
- package/dist/{kit-server.js → server.js} +129 -98
- package/dist/services/digest/background.d.ts +11 -0
- package/dist/services/digest/background.js +258 -0
- package/dist/{memory → services}/digest/constants.d.ts +4 -1
- package/dist/{memory → services}/digest/constants.js +6 -2
- package/dist/services/digest/digest-background.test.js +206 -0
- package/dist/{tools/__tests__ → services/digest}/digest-init.test.js +4 -3
- package/dist/{memory/__tests__ → services/digest}/digest-llama-local.test.js +1 -13
- package/dist/{memory/__tests__ → services/digest}/digest-pending.test.js +39 -11
- package/dist/{memory/__tests__ → services/digest}/digest-processor.test.js +5 -7
- package/dist/{memory → services}/digest/files.d.ts +0 -1
- package/dist/{memory → services}/digest/files.js +2 -2
- package/dist/services/digest/model-registry.d.ts +2 -0
- package/dist/{memory → services}/digest/model-registry.js +9 -8
- package/dist/services/digest/processor.d.ts +13 -0
- package/dist/{memory → services}/digest/processor.js +48 -31
- package/dist/services/digest/providers/llama-local.d.ts +2 -0
- package/dist/{memory → services}/digest/providers/llama-local.js +3 -4
- package/dist/{memory → services}/digest/types.d.ts +34 -2
- package/dist/services/digest/types.js +6 -0
- package/dist/{memory → services}/digest/worker.d.ts +0 -1
- package/dist/{memory → services}/digest/worker.js +2 -2
- package/dist/{utils → services/integration}/credentials.js +1 -1
- package/dist/{utils/parser.js → services/integration/parser/adf.js} +1 -0
- package/dist/{memory → services/memory}/chunker.d.ts +0 -2
- package/dist/{memory → services/memory}/chunker.js +3 -3
- package/dist/{memory/__tests__ → services/memory}/chunker.test.js +1 -34
- package/dist/{memory → services/memory}/constants.d.ts +1 -1
- package/dist/{memory → services/memory}/constants.js +2 -1
- package/dist/{memory → services/memory}/embedder.d.ts +2 -2
- package/dist/{memory → services/memory}/embedder.js +5 -5
- package/dist/services/memory/embedder.test.js +13 -0
- package/dist/{memory → services/memory}/indexer.d.ts +2 -1
- package/dist/{memory → services/memory}/indexer.js +1 -1
- package/dist/{memory/__tests__ → services/memory}/indexer.test.js +4 -3
- package/dist/{tools/__tests__ → services/memory}/memory.test.js +54 -13
- package/dist/{memory/__tests__ → services/memory}/store.test.js +3 -2
- package/dist/{memory → services/memory}/types.d.ts +7 -2
- package/dist/services/memory/types.js +6 -0
- package/dist/utils/files.d.ts +0 -1
- package/dist/utils/files.js +1 -1
- package/dist/{memory/__tests__/lockfile.test.js → utils/files.test.js} +14 -14
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/json.d.ts +0 -1
- package/dist/utils/json.js +0 -10
- package/dist/utils/paths.d.ts +0 -1
- package/dist/utils/paths.js +1 -1
- package/package.json +6 -6
- package/dist/memory/__tests__/digest-worker.test.js +0 -46
- package/dist/memory/__tests__/embedder.test.js +0 -14
- package/dist/memory/digest/model-registry.d.ts +0 -6
- package/dist/memory/digest/processor.d.ts +0 -7
- package/dist/memory/digest/providers/llama-local.d.ts +0 -7
- package/dist/tools/__tests__/cli-digest-pending.test.js +0 -51
- package/dist/tools/__tests__/cli-memory.test.js +0 -22
- package/dist/tools/core.d.ts +0 -10
- package/dist/tools/core.js +0 -46
- package/dist/tools/handoffs.d.ts +0 -22
- package/dist/tools/memory.d.ts +0 -16
- /package/dist/{tools/__tests__ → cli/commands}/cli-digest-pending.test.d.ts +0 -0
- /package/dist/{tools/__tests__ → cli/commands}/cli-memory.test.d.ts +0 -0
- /package/dist/cli/{memory.d.ts → commands/memory.d.ts} +0 -0
- /package/dist/{utils → cli/utils}/args.js +0 -0
- /package/dist/{memory/__tests__/chunker.test.d.ts → core/handoffs/index.test.d.ts} +0 -0
- /package/dist/{kit-cli.d.ts → entrypoints/cli.d.ts} +0 -0
- /package/dist/{kit-server.d.ts → entrypoints/server.d.ts} +0 -0
- /package/dist/{memory/__tests__/digest-llama-local.test.d.ts → mcp/core.test.d.ts} +0 -0
- /package/dist/{tools → mcp}/integration.d.ts +0 -0
- /package/dist/{memory/__tests__/digest-pending.test.d.ts → services/digest/digest-background.test.d.ts} +0 -0
- /package/dist/{tools/__tests__ → services/digest}/digest-init.test.d.ts +0 -0
- /package/dist/{memory/__tests__/digest-processor.test.d.ts → services/digest/digest-llama-local.test.d.ts} +0 -0
- /package/dist/{memory/__tests__/digest-worker.test.d.ts → services/digest/digest-pending.test.d.ts} +0 -0
- /package/dist/{memory/__tests__/embedder.test.d.ts → services/digest/digest-processor.test.d.ts} +0 -0
- /package/dist/{utils → services/integration}/credentials.d.ts +0 -0
- /package/dist/{utils/parser.d.ts → services/integration/parser/adf.d.ts} +0 -0
- /package/dist/{memory/__tests__/indexer.test.d.ts → services/memory/chunker.test.d.ts} +0 -0
- /package/dist/{memory/__tests__/lockfile.test.d.ts → services/memory/embedder.test.d.ts} +0 -0
- /package/dist/{memory → services/memory}/index.d.ts +0 -0
- /package/dist/{memory → services/memory}/index.js +0 -0
- /package/dist/{memory/__tests__/store.test.d.ts → services/memory/indexer.test.d.ts} +0 -0
- /package/dist/{tools/__tests__ → services/memory}/memory.test.d.ts +0 -0
- /package/dist/{memory → services/memory}/store.d.ts +0 -0
- /package/dist/{memory → services/memory}/store.js +0 -0
- /package/dist/{memory/digest/types.js → services/memory/store.test.d.ts} +0 -0
- /package/dist/{memory/types.js → utils/files.test.d.ts} +0 -0
- /package/dist/{__tests__/temp-dir.d.ts → utils/temp-dir.test.d.ts} +0 -0
- /package/dist/{__tests__/temp-dir.js → utils/temp-dir.test.js} +0 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { DIGEST_LOCKFILE_REL_PATH, DIGEST_WORKER_FLAG, DIGEST_WORKER_LOG_REL_PATH, DIGEST_WORKER_STATUS_REL_PATH, } from './constants.js';
|
|
5
|
+
import { digestPendingConversations, summarizePendingConversations } from './processor.js';
|
|
6
|
+
import { atomicWriteJsonFile } from '../../utils/files.js';
|
|
7
|
+
import { isRecord } from '../../utils/json.js';
|
|
8
|
+
function digestWorkerStatusPath(workspaceRoot) {
|
|
9
|
+
return path.join(workspaceRoot, DIGEST_WORKER_STATUS_REL_PATH);
|
|
10
|
+
}
|
|
11
|
+
function digestWorkerLogPath(workspaceRoot) {
|
|
12
|
+
return path.join(workspaceRoot, DIGEST_WORKER_LOG_REL_PATH);
|
|
13
|
+
}
|
|
14
|
+
function digestWorkerLockPath(workspaceRoot) {
|
|
15
|
+
return path.join(workspaceRoot, DIGEST_LOCKFILE_REL_PATH);
|
|
16
|
+
}
|
|
17
|
+
function isoNow() {
|
|
18
|
+
return new Date().toISOString();
|
|
19
|
+
}
|
|
20
|
+
function errorMessage(err) {
|
|
21
|
+
return err instanceof Error ? err.message : String(err);
|
|
22
|
+
}
|
|
23
|
+
function emptyStatus(state, pendingAtStart, startedAt = isoNow()) {
|
|
24
|
+
return {
|
|
25
|
+
state,
|
|
26
|
+
pid: null,
|
|
27
|
+
startedAt,
|
|
28
|
+
updatedAt: startedAt,
|
|
29
|
+
pendingAtStart,
|
|
30
|
+
processed: 0,
|
|
31
|
+
skipped: 0,
|
|
32
|
+
errors: 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const WORKER_STATES = new Set([
|
|
36
|
+
'running',
|
|
37
|
+
'complete',
|
|
38
|
+
'failed',
|
|
39
|
+
'not-initialized',
|
|
40
|
+
'no-pending',
|
|
41
|
+
'locked',
|
|
42
|
+
'stale',
|
|
43
|
+
]);
|
|
44
|
+
function isDigestWorkerStatus(value) {
|
|
45
|
+
if (!isRecord(value))
|
|
46
|
+
return false;
|
|
47
|
+
return (typeof value.state === 'string' &&
|
|
48
|
+
WORKER_STATES.has(value.state) &&
|
|
49
|
+
(typeof value.pid === 'number' || value.pid === null) &&
|
|
50
|
+
typeof value.startedAt === 'string' &&
|
|
51
|
+
typeof value.updatedAt === 'string' &&
|
|
52
|
+
typeof value.pendingAtStart === 'number' &&
|
|
53
|
+
typeof value.processed === 'number' &&
|
|
54
|
+
typeof value.skipped === 'number' &&
|
|
55
|
+
typeof value.errors === 'number' &&
|
|
56
|
+
(value.lastError === undefined || typeof value.lastError === 'string'));
|
|
57
|
+
}
|
|
58
|
+
function readDigestWorkerStatus(workspaceRoot) {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(fs.readFileSync(digestWorkerStatusPath(workspaceRoot), 'utf8'));
|
|
61
|
+
return isDigestWorkerStatus(parsed) ? parsed : undefined;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function writeDigestWorkerStatus(workspaceRoot, status) {
|
|
68
|
+
atomicWriteJsonFile(digestWorkerStatusPath(workspaceRoot), status);
|
|
69
|
+
}
|
|
70
|
+
function appendDigestWorkerLog(workspaceRoot, message) {
|
|
71
|
+
try {
|
|
72
|
+
const logPath = digestWorkerLogPath(workspaceRoot);
|
|
73
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
74
|
+
fs.appendFileSync(logPath, `[${isoNow()}] ${message}\n`, 'utf8');
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Diagnostics must never block hook startup or worker completion.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function isLivePid(pid) {
|
|
81
|
+
if (pid === null || !Number.isInteger(pid) || pid <= 0)
|
|
82
|
+
return false;
|
|
83
|
+
try {
|
|
84
|
+
process.kill(pid, 0);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
return err.code !== 'ESRCH';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function readLockPid(workspaceRoot) {
|
|
92
|
+
const lockPath = digestWorkerLockPath(workspaceRoot);
|
|
93
|
+
if (!fs.existsSync(lockPath))
|
|
94
|
+
return undefined;
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
97
|
+
return isRecord(parsed) && typeof parsed.pid === 'number' ? parsed.pid : null;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function statusFromPendingResult(result, startedAt, pendingAtStart) {
|
|
104
|
+
const updatedAt = isoNow();
|
|
105
|
+
if (!result.ok) {
|
|
106
|
+
return {
|
|
107
|
+
...emptyStatus('failed', pendingAtStart, startedAt),
|
|
108
|
+
updatedAt,
|
|
109
|
+
errors: 1,
|
|
110
|
+
lastError: result.error,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (result.action === 'digested') {
|
|
114
|
+
return {
|
|
115
|
+
state: 'complete',
|
|
116
|
+
pid: null,
|
|
117
|
+
startedAt,
|
|
118
|
+
updatedAt,
|
|
119
|
+
pendingAtStart,
|
|
120
|
+
processed: result.count,
|
|
121
|
+
skipped: result.skipped,
|
|
122
|
+
errors: result.errors,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
...emptyStatus(result.reason, pendingAtStart, startedAt),
|
|
127
|
+
updatedAt,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export async function runDigestPendingWorker(input) {
|
|
131
|
+
const startedAt = isoNow();
|
|
132
|
+
let pendingAtStart = input.pendingAtStart ?? 0;
|
|
133
|
+
try {
|
|
134
|
+
const summary = summarizePendingConversations(input.workspaceRoot);
|
|
135
|
+
if (input.pendingAtStart === undefined) {
|
|
136
|
+
pendingAtStart = summary.initialized ? summary.pending : 0;
|
|
137
|
+
}
|
|
138
|
+
const runningStatus = {
|
|
139
|
+
...emptyStatus('running', pendingAtStart, startedAt),
|
|
140
|
+
pid: process.pid,
|
|
141
|
+
};
|
|
142
|
+
writeDigestWorkerStatus(input.workspaceRoot, runningStatus);
|
|
143
|
+
appendDigestWorkerLog(input.workspaceRoot, `worker started pid=${process.pid} pending=${pendingAtStart}`);
|
|
144
|
+
const result = await digestPendingConversations({
|
|
145
|
+
workspaceRoot: input.workspaceRoot,
|
|
146
|
+
digestFn: input.digestFn,
|
|
147
|
+
onProgress: (progress) => {
|
|
148
|
+
const currentStatus = {
|
|
149
|
+
...runningStatus,
|
|
150
|
+
updatedAt: isoNow(),
|
|
151
|
+
processed: progress.processed,
|
|
152
|
+
skipped: progress.skipped,
|
|
153
|
+
errors: progress.errors,
|
|
154
|
+
};
|
|
155
|
+
writeDigestWorkerStatus(input.workspaceRoot, currentStatus);
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
const status = statusFromPendingResult(result, startedAt, pendingAtStart);
|
|
159
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
160
|
+
appendDigestWorkerLog(input.workspaceRoot, `worker finished state=${status.state} processed=${status.processed} skipped=${status.skipped} errors=${status.errors}`);
|
|
161
|
+
return status;
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
const status = {
|
|
165
|
+
...emptyStatus('failed', pendingAtStart, startedAt),
|
|
166
|
+
updatedAt: isoNow(),
|
|
167
|
+
errors: 1,
|
|
168
|
+
lastError: errorMessage(err),
|
|
169
|
+
};
|
|
170
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
171
|
+
appendDigestWorkerLog(input.workspaceRoot, `worker failed error=${status.lastError}`);
|
|
172
|
+
return status;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export function launchDigestPendingWorker(input) {
|
|
176
|
+
const startedAt = isoNow();
|
|
177
|
+
let pendingAtStart = 0;
|
|
178
|
+
try {
|
|
179
|
+
const previousStatus = readDigestWorkerStatus(input.workspaceRoot);
|
|
180
|
+
if (previousStatus?.state === 'running' && isLivePid(previousStatus.pid)) {
|
|
181
|
+
const status = {
|
|
182
|
+
...previousStatus,
|
|
183
|
+
state: 'locked',
|
|
184
|
+
updatedAt: isoNow(),
|
|
185
|
+
lastError: undefined,
|
|
186
|
+
};
|
|
187
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
188
|
+
return { ok: true, mode: 'background-launcher', status, spawned: false, reason: 'locked' };
|
|
189
|
+
}
|
|
190
|
+
const lockPid = readLockPid(input.workspaceRoot);
|
|
191
|
+
if (lockPid !== undefined && isLivePid(lockPid)) {
|
|
192
|
+
const status = emptyStatus('locked', 0, startedAt);
|
|
193
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
194
|
+
return { ok: true, mode: 'background-launcher', status, spawned: false, reason: 'locked' };
|
|
195
|
+
}
|
|
196
|
+
const summary = summarizePendingConversations(input.workspaceRoot);
|
|
197
|
+
if (!summary.initialized) {
|
|
198
|
+
const status = emptyStatus('not-initialized', 0, startedAt);
|
|
199
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
200
|
+
return { ok: true, mode: 'background-launcher', status, spawned: false, reason: 'not-initialized' };
|
|
201
|
+
}
|
|
202
|
+
if (summary.pending === 0) {
|
|
203
|
+
const status = emptyStatus('no-pending', 0, startedAt);
|
|
204
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
205
|
+
return { ok: true, mode: 'background-launcher', status, spawned: false, reason: 'no-pending' };
|
|
206
|
+
}
|
|
207
|
+
pendingAtStart = summary.pending;
|
|
208
|
+
if (!input.entrypoint) {
|
|
209
|
+
const status = {
|
|
210
|
+
...emptyStatus('failed', pendingAtStart, startedAt),
|
|
211
|
+
errors: 1,
|
|
212
|
+
lastError: 'CLI entrypoint is unavailable',
|
|
213
|
+
};
|
|
214
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
mode: 'background-launcher',
|
|
218
|
+
status,
|
|
219
|
+
spawned: false,
|
|
220
|
+
reason: 'spawn-failed',
|
|
221
|
+
error: status.lastError,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const child = spawn(process.execPath, [input.entrypoint, 'memory', 'digest-pending', ...input.args, `--${DIGEST_WORKER_FLAG}`], {
|
|
225
|
+
cwd: input.workspaceRoot,
|
|
226
|
+
detached: true,
|
|
227
|
+
stdio: 'ignore',
|
|
228
|
+
});
|
|
229
|
+
child.unref();
|
|
230
|
+
if (!child.pid) {
|
|
231
|
+
throw new Error('worker process did not expose a pid');
|
|
232
|
+
}
|
|
233
|
+
const status = {
|
|
234
|
+
...emptyStatus('running', pendingAtStart, startedAt),
|
|
235
|
+
pid: child.pid,
|
|
236
|
+
};
|
|
237
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
238
|
+
appendDigestWorkerLog(input.workspaceRoot, `launcher spawned pid=${child.pid} pending=${pendingAtStart}`);
|
|
239
|
+
return { ok: true, mode: 'background-launcher', status, spawned: true, pid: child.pid };
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
const status = {
|
|
243
|
+
...emptyStatus('failed', pendingAtStart, startedAt),
|
|
244
|
+
errors: 1,
|
|
245
|
+
lastError: errorMessage(err),
|
|
246
|
+
};
|
|
247
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
248
|
+
appendDigestWorkerLog(input.workspaceRoot, `launcher failed error=${status.lastError}`);
|
|
249
|
+
return {
|
|
250
|
+
ok: false,
|
|
251
|
+
mode: 'background-launcher',
|
|
252
|
+
status,
|
|
253
|
+
spawned: false,
|
|
254
|
+
reason: 'spawn-failed',
|
|
255
|
+
error: status.lastError,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { DigestModelId } from './types.js';
|
|
2
|
+
export declare const DEFAULT_DIGEST_MODEL_ID = DigestModelId.DEFAULT;
|
|
2
3
|
export declare const DEFAULT_DIGEST_MAX_INPUT_CHARS = 3000;
|
|
3
4
|
export declare const DEFAULT_DIGEST_TIMEOUT_MS = 120000;
|
|
4
5
|
export declare const DIGEST_TIMEOUT_GRACE_MS = 5000;
|
|
@@ -9,6 +10,8 @@ export declare const WIKI_DIR: string;
|
|
|
9
10
|
export declare const WIKI_RAW_DIR: string;
|
|
10
11
|
export declare const PROVISIONAL_DIGEST_DIR: string;
|
|
11
12
|
export declare const DIGEST_LOCKFILE_REL_PATH: string;
|
|
13
|
+
export declare const DIGEST_WORKER_STATUS_REL_PATH: string;
|
|
14
|
+
export declare const DIGEST_WORKER_LOG_REL_PATH: string;
|
|
12
15
|
export declare const LLAMA_CONTEXT_SIZE = 4096;
|
|
13
16
|
export declare const LLAMA_MAX_GENERATED_TOKENS = 512;
|
|
14
17
|
export declare const LLAMA_TEMPERATURE = 0.1;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
|
-
|
|
2
|
+
import { DigestModelId } from './types.js';
|
|
3
|
+
export const DEFAULT_DIGEST_MODEL_ID = DigestModelId.DEFAULT;
|
|
3
4
|
export const DEFAULT_DIGEST_MAX_INPUT_CHARS = 3000;
|
|
4
5
|
export const DEFAULT_DIGEST_TIMEOUT_MS = 120_000;
|
|
5
6
|
export const DIGEST_TIMEOUT_GRACE_MS = 5_000;
|
|
@@ -9,7 +10,10 @@ export const KIT_DIR = '.agent-kit';
|
|
|
9
10
|
export const WIKI_DIR = path.join(KIT_DIR, 'wiki');
|
|
10
11
|
export const WIKI_RAW_DIR = path.join(WIKI_DIR, 'raw');
|
|
11
12
|
export const PROVISIONAL_DIGEST_DIR = path.join(WIKI_DIR, 'compiled', 'provisional', 'conversation-digests');
|
|
12
|
-
|
|
13
|
+
const DIGEST_WORKER_DIR = path.join(WIKI_DIR, 'digest');
|
|
14
|
+
export const DIGEST_LOCKFILE_REL_PATH = path.join(DIGEST_WORKER_DIR, 'digest-worker.lock');
|
|
15
|
+
export const DIGEST_WORKER_STATUS_REL_PATH = path.join(DIGEST_WORKER_DIR, 'digest-worker.status.json');
|
|
16
|
+
export const DIGEST_WORKER_LOG_REL_PATH = path.join(DIGEST_WORKER_DIR, 'digest-worker.log');
|
|
13
17
|
export const LLAMA_CONTEXT_SIZE = 4096;
|
|
14
18
|
export const LLAMA_MAX_GENERATED_TOKENS = 512;
|
|
15
19
|
export const LLAMA_TEMPERATURE = 0.1;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { afterEach, describe, test } from 'node:test';
|
|
5
|
+
import { createTempDirTracker } from '../../utils/temp-dir.test.js';
|
|
6
|
+
import { writeConversationDigestSettings } from './files.js';
|
|
7
|
+
import { launchDigestPendingWorker, runDigestPendingWorker } from './background.js';
|
|
8
|
+
import { DIGEST_WORKER_LOG_REL_PATH, DIGEST_WORKER_STATUS_REL_PATH } from './constants.js';
|
|
9
|
+
import { DigestModelId } from './types.js';
|
|
10
|
+
const tempDirs = createTempDirTracker();
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
tempDirs.cleanup();
|
|
13
|
+
});
|
|
14
|
+
function digestWorkerLogPath(workspace) {
|
|
15
|
+
return path.join(workspace, DIGEST_WORKER_LOG_REL_PATH);
|
|
16
|
+
}
|
|
17
|
+
function digestWorkerStatusPath(workspace) {
|
|
18
|
+
return path.join(workspace, DIGEST_WORKER_STATUS_REL_PATH);
|
|
19
|
+
}
|
|
20
|
+
function readDigestWorkerStatus(workspace) {
|
|
21
|
+
try {
|
|
22
|
+
const p = digestWorkerStatusPath(workspace);
|
|
23
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function writeDigestWorkerStatus(workspace, status) {
|
|
30
|
+
const p = digestWorkerStatusPath(workspace);
|
|
31
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
32
|
+
fs.writeFileSync(p, JSON.stringify(status), 'utf8');
|
|
33
|
+
}
|
|
34
|
+
function writeInitState(workspace) {
|
|
35
|
+
writeConversationDigestSettings(workspace, {
|
|
36
|
+
enabled: true,
|
|
37
|
+
initialized: true,
|
|
38
|
+
modelId: DigestModelId.DEFAULT,
|
|
39
|
+
initializedAt: new Date().toISOString(),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function writeConvFile(workspace) {
|
|
43
|
+
const rawDir = path.join(workspace, '.agent-kit', 'wiki', 'raw');
|
|
44
|
+
fs.mkdirSync(rawDir, { recursive: true });
|
|
45
|
+
fs.writeFileSync(path.join(rawDir, 'conv_2026-01-01T00-00-00-001Z.md'), '**User:** hello\n', 'utf8');
|
|
46
|
+
}
|
|
47
|
+
function status(overrides = {}) {
|
|
48
|
+
const now = new Date().toISOString();
|
|
49
|
+
return {
|
|
50
|
+
state: 'running',
|
|
51
|
+
pid: process.pid,
|
|
52
|
+
startedAt: now,
|
|
53
|
+
updatedAt: now,
|
|
54
|
+
pendingAtStart: 1,
|
|
55
|
+
processed: 0,
|
|
56
|
+
skipped: 0,
|
|
57
|
+
errors: 0,
|
|
58
|
+
...overrides,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
describe('digest background helpers', () => {
|
|
62
|
+
test('uses .agent-kit/wiki/digest paths for worker files', () => {
|
|
63
|
+
const workspace = tempDirs.makeTempDir('digest-bg-paths-');
|
|
64
|
+
assert.equal(digestWorkerStatusPath(workspace), path.join(workspace, '.agent-kit', 'wiki', 'digest', 'digest-worker.status.json'));
|
|
65
|
+
assert.equal(digestWorkerLogPath(workspace), path.join(workspace, '.agent-kit', 'wiki', 'digest', 'digest-worker.log'));
|
|
66
|
+
});
|
|
67
|
+
test('writes valid status atomically and tolerates invalid status reads', () => {
|
|
68
|
+
const workspace = tempDirs.makeTempDir('digest-bg-status-');
|
|
69
|
+
const written = status({ state: 'complete', pid: null, processed: 1 });
|
|
70
|
+
writeDigestWorkerStatus(workspace, written);
|
|
71
|
+
assert.deepEqual(readDigestWorkerStatus(workspace), written);
|
|
72
|
+
fs.writeFileSync(digestWorkerStatusPath(workspace), '{bad json', 'utf8');
|
|
73
|
+
assert.equal(readDigestWorkerStatus(workspace), undefined);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe('launchDigestPendingWorker', () => {
|
|
77
|
+
test('writes not-initialized and does not spawn before digest init', () => {
|
|
78
|
+
const workspace = tempDirs.makeTempDir('digest-bg-not-init-');
|
|
79
|
+
const result = launchDigestPendingWorker({
|
|
80
|
+
workspaceRoot: workspace,
|
|
81
|
+
args: ['--background'],
|
|
82
|
+
entrypoint: 'cli.js',
|
|
83
|
+
});
|
|
84
|
+
assert.equal(result.ok, true);
|
|
85
|
+
assert.equal(result.spawned, false);
|
|
86
|
+
assert.equal(result.status.state, 'not-initialized');
|
|
87
|
+
assert.equal(readDigestWorkerStatus(workspace)?.state, 'not-initialized');
|
|
88
|
+
});
|
|
89
|
+
test('writes no-pending and does not spawn when initialized with no candidates', () => {
|
|
90
|
+
const workspace = tempDirs.makeTempDir('digest-bg-no-pending-');
|
|
91
|
+
writeInitState(workspace);
|
|
92
|
+
const result = launchDigestPendingWorker({
|
|
93
|
+
workspaceRoot: workspace,
|
|
94
|
+
args: ['--background'],
|
|
95
|
+
entrypoint: 'cli.js',
|
|
96
|
+
});
|
|
97
|
+
assert.equal(result.ok, true);
|
|
98
|
+
assert.equal(result.spawned, false);
|
|
99
|
+
assert.equal(result.status.state, 'no-pending');
|
|
100
|
+
});
|
|
101
|
+
test('writes locked when an existing running worker pid is live', () => {
|
|
102
|
+
const workspace = tempDirs.makeTempDir('digest-bg-locked-status-');
|
|
103
|
+
writeDigestWorkerStatus(workspace, status());
|
|
104
|
+
const result = launchDigestPendingWorker({
|
|
105
|
+
workspaceRoot: workspace,
|
|
106
|
+
args: ['--background'],
|
|
107
|
+
entrypoint: 'cli.js',
|
|
108
|
+
});
|
|
109
|
+
assert.equal(result.spawned, false);
|
|
110
|
+
assert.equal(result.status.state, 'locked');
|
|
111
|
+
});
|
|
112
|
+
test('marks stale running status before continuing to no-pending preflight', () => {
|
|
113
|
+
const workspace = tempDirs.makeTempDir('digest-bg-stale-status-');
|
|
114
|
+
writeInitState(workspace);
|
|
115
|
+
writeDigestWorkerStatus(workspace, status({ pid: 999999999 }));
|
|
116
|
+
const result = launchDigestPendingWorker({
|
|
117
|
+
workspaceRoot: workspace,
|
|
118
|
+
args: ['--background'],
|
|
119
|
+
entrypoint: 'cli.js',
|
|
120
|
+
});
|
|
121
|
+
assert.equal(result.spawned, false);
|
|
122
|
+
assert.equal(result.status.state, 'no-pending');
|
|
123
|
+
});
|
|
124
|
+
test('tolerates corrupt stale lock files and continues preflight', () => {
|
|
125
|
+
const workspace = tempDirs.makeTempDir('digest-bg-stale-lock-');
|
|
126
|
+
writeInitState(workspace);
|
|
127
|
+
const lockDir = path.join(workspace, '.agent-kit', 'wiki', 'digest');
|
|
128
|
+
fs.mkdirSync(lockDir, { recursive: true });
|
|
129
|
+
fs.writeFileSync(path.join(lockDir, 'digest-worker.lock'), 'not-json', 'utf8');
|
|
130
|
+
const result = launchDigestPendingWorker({
|
|
131
|
+
workspaceRoot: workspace,
|
|
132
|
+
args: ['--background'],
|
|
133
|
+
entrypoint: 'cli.js',
|
|
134
|
+
});
|
|
135
|
+
assert.equal(result.spawned, false);
|
|
136
|
+
assert.equal(result.status.state, 'no-pending');
|
|
137
|
+
});
|
|
138
|
+
test('writes failed status when pending work exists but entrypoint is unavailable', () => {
|
|
139
|
+
const workspace = tempDirs.makeTempDir('digest-bg-spawn-failed-');
|
|
140
|
+
writeInitState(workspace);
|
|
141
|
+
writeConvFile(workspace);
|
|
142
|
+
const result = launchDigestPendingWorker({
|
|
143
|
+
workspaceRoot: workspace,
|
|
144
|
+
args: ['--background'],
|
|
145
|
+
entrypoint: undefined,
|
|
146
|
+
});
|
|
147
|
+
assert.equal(result.ok, false);
|
|
148
|
+
assert.equal(result.spawned, false);
|
|
149
|
+
assert.equal(result.status.state, 'failed');
|
|
150
|
+
assert.equal(result.status.pendingAtStart, 1);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe('runDigestPendingWorker', () => {
|
|
154
|
+
test('writes final no-pending status for initialized workspace without candidates', async () => {
|
|
155
|
+
const workspace = tempDirs.makeTempDir('digest-bg-worker-no-pending-');
|
|
156
|
+
writeInitState(workspace);
|
|
157
|
+
const result = await runDigestPendingWorker({ workspaceRoot: workspace, pendingAtStart: 0 });
|
|
158
|
+
assert.equal(result.state, 'no-pending');
|
|
159
|
+
assert.equal(readDigestWorkerStatus(workspace)?.state, 'no-pending');
|
|
160
|
+
assert.match(fs.readFileSync(digestWorkerLogPath(workspace), 'utf8'), /worker finished/);
|
|
161
|
+
});
|
|
162
|
+
test('writes failed final status for run-level processor failure', async () => {
|
|
163
|
+
const workspace = tempDirs.makeTempDir('digest-bg-worker-failed-');
|
|
164
|
+
writeInitState(workspace);
|
|
165
|
+
const wikiDir = path.join(workspace, '.agent-kit', 'wiki');
|
|
166
|
+
fs.mkdirSync(wikiDir, { recursive: true });
|
|
167
|
+
fs.writeFileSync(path.join(wikiDir, 'raw'), 'not-a-directory', 'utf8');
|
|
168
|
+
const result = await runDigestPendingWorker({ workspaceRoot: workspace, pendingAtStart: 1 });
|
|
169
|
+
assert.equal(result.state, 'failed');
|
|
170
|
+
assert.equal(result.errors, 1);
|
|
171
|
+
assert.ok(result.lastError);
|
|
172
|
+
});
|
|
173
|
+
test('updates status file in real-time as conversations are digested', async () => {
|
|
174
|
+
const workspace = tempDirs.makeTempDir('digest-bg-worker-realtime-');
|
|
175
|
+
writeInitState(workspace);
|
|
176
|
+
const rawDir = path.join(workspace, '.agent-kit', 'wiki', 'raw');
|
|
177
|
+
fs.mkdirSync(rawDir, { recursive: true });
|
|
178
|
+
fs.writeFileSync(path.join(rawDir, 'conv_1.md'), '**User:** one\n', 'utf8');
|
|
179
|
+
fs.writeFileSync(path.join(rawDir, 'conv_2.md'), '**User:** two\n', 'utf8');
|
|
180
|
+
const statusesSeen = [];
|
|
181
|
+
const mockDigestFn = async () => {
|
|
182
|
+
statusesSeen.push(readDigestWorkerStatus(workspace));
|
|
183
|
+
return {
|
|
184
|
+
markdown: 'out.md',
|
|
185
|
+
status: 'provisional',
|
|
186
|
+
contentHash: 'hash',
|
|
187
|
+
skipped: false,
|
|
188
|
+
indexed: false,
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
const result = await runDigestPendingWorker({
|
|
192
|
+
workspaceRoot: workspace,
|
|
193
|
+
pendingAtStart: 2,
|
|
194
|
+
digestFn: mockDigestFn,
|
|
195
|
+
});
|
|
196
|
+
assert.equal(statusesSeen.length, 2);
|
|
197
|
+
assert.ok(statusesSeen[0]);
|
|
198
|
+
assert.equal(statusesSeen[0].state, 'running');
|
|
199
|
+
assert.equal(statusesSeen[0].processed, 0);
|
|
200
|
+
assert.ok(statusesSeen[1]);
|
|
201
|
+
assert.equal(statusesSeen[1].state, 'running');
|
|
202
|
+
assert.equal(statusesSeen[1].processed, 1);
|
|
203
|
+
assert.equal(result.state, 'complete');
|
|
204
|
+
assert.equal(result.processed, 2);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -2,8 +2,9 @@ import * as assert from 'node:assert/strict';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { afterEach, describe, test } from 'node:test';
|
|
5
|
-
import { createTempDirTracker } from '../../
|
|
6
|
-
import { initializeConversationDigestModel } from '
|
|
5
|
+
import { createTempDirTracker } from '../../utils/temp-dir.test.js';
|
|
6
|
+
import { initializeConversationDigestModel } from './processor.js';
|
|
7
|
+
import { DigestModelId } from './types.js';
|
|
7
8
|
const tempDirs = createTempDirTracker();
|
|
8
9
|
afterEach(() => {
|
|
9
10
|
tempDirs.cleanup();
|
|
@@ -23,7 +24,7 @@ describe('initializeConversationDigestModel', () => {
|
|
|
23
24
|
const workspace = tempDirs.makeTempDir('digest-init-');
|
|
24
25
|
const result = await initializeConversationDigestModel({
|
|
25
26
|
workspaceRoot: workspace,
|
|
26
|
-
modelId:
|
|
27
|
+
modelId: DigestModelId.DEFAULT,
|
|
27
28
|
allowDownload: false,
|
|
28
29
|
});
|
|
29
30
|
assert.equal(result.initialized, false);
|
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
import * as assert from 'node:assert/strict';
|
|
2
2
|
import { describe, test } from 'node:test';
|
|
3
|
-
import { createLlamaLocalDigestProvider
|
|
3
|
+
import { createLlamaLocalDigestProvider } from '../digest/providers/llama-local.js';
|
|
4
4
|
const INPUT = {
|
|
5
5
|
sourcePath: '.agent-kit/wiki/archive/conversations/conv_2026-05-19T00-45-17-697Z.md',
|
|
6
6
|
contentHash: 'tiny',
|
|
7
7
|
content: '**User:** use local digest generation for archived conversations',
|
|
8
8
|
};
|
|
9
9
|
describe('createLlamaLocalDigestProvider', () => {
|
|
10
|
-
test('strips code fences without rewriting model output', () => {
|
|
11
|
-
const markdown = sanitizeConversationDigestMarkdown('```md\n# Notes\n\n**Summary**\n\nUse local digesting.\n```');
|
|
12
|
-
assert.match(markdown, /^# Notes/);
|
|
13
|
-
assert.match(markdown, /\*\*Summary\*\*/);
|
|
14
|
-
assert.doesNotMatch(markdown, /```/);
|
|
15
|
-
});
|
|
16
|
-
test('trims conversation exports from the end without parsing turns', () => {
|
|
17
|
-
const trimmed = trimConversationExport('first decision\nsecond decision\nfinal decision', 14);
|
|
18
|
-
assert.match(trimmed, /omitted/);
|
|
19
|
-
assert.doesNotMatch(trimmed, /first decision/);
|
|
20
|
-
assert.match(trimmed, /final decision/);
|
|
21
|
-
});
|
|
22
10
|
test('runs an opt-in smoke test against a real model', { skip: !process.env.AGENT_KIT_DIGEST_MODEL_ID }, async () => {
|
|
23
11
|
const modelId = process.env.AGENT_KIT_DIGEST_MODEL_ID;
|
|
24
12
|
const provider = await createLlamaLocalDigestProvider(modelId);
|
|
@@ -2,14 +2,15 @@ import * as assert from 'node:assert/strict';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { afterEach, describe, test } from 'node:test';
|
|
5
|
-
import { createTempDirTracker } from '../../
|
|
6
|
-
import { writeConversationDigestSettings } from '
|
|
7
|
-
import {
|
|
5
|
+
import { createTempDirTracker } from '../../utils/temp-dir.test.js';
|
|
6
|
+
import { defaultProvisionalDigestDir, readConversationDigestInput, writeConversationDigestSettings, writeProvisionalDigestFile, } from './files.js';
|
|
7
|
+
import { DigestModelId } from './types.js';
|
|
8
|
+
import { digestPendingConversations, summarizePendingConversations } from './processor.js';
|
|
8
9
|
const tempDirs = createTempDirTracker();
|
|
9
10
|
afterEach(() => {
|
|
10
11
|
tempDirs.cleanup();
|
|
11
12
|
});
|
|
12
|
-
function writeInitState(workspace, modelId =
|
|
13
|
+
function writeInitState(workspace, modelId = DigestModelId.DEFAULT) {
|
|
13
14
|
writeConversationDigestSettings(workspace, {
|
|
14
15
|
enabled: true,
|
|
15
16
|
initialized: true,
|
|
@@ -47,7 +48,7 @@ describe('digestPendingConversations', () => {
|
|
|
47
48
|
test('C14: init state, live lockfile → noop locked', async () => {
|
|
48
49
|
const workspace = tempDirs.makeTempDir('pending-c14-');
|
|
49
50
|
writeInitState(workspace);
|
|
50
|
-
const lockDir = path.join(workspace, '.agent-kit');
|
|
51
|
+
const lockDir = path.join(workspace, '.agent-kit', 'wiki', 'digest');
|
|
51
52
|
fs.mkdirSync(lockDir, { recursive: true });
|
|
52
53
|
fs.writeFileSync(path.join(lockDir, 'digest-worker.lock'), JSON.stringify({ pid: process.pid }));
|
|
53
54
|
const result = await digestPendingConversations({ workspaceRoot: workspace });
|
|
@@ -65,7 +66,7 @@ describe('digestPendingConversations', () => {
|
|
|
65
66
|
assert.equal(result.action, 'noop');
|
|
66
67
|
if (result.action === 'noop')
|
|
67
68
|
assert.equal(result.reason, 'no-pending');
|
|
68
|
-
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'digest-worker.lock')), false);
|
|
69
|
+
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'wiki', 'digest', 'digest-worker.lock')), false);
|
|
69
70
|
});
|
|
70
71
|
test('C19: wiki/raw dir does not exist → noop no-pending', async () => {
|
|
71
72
|
const workspace = tempDirs.makeTempDir('pending-c19-');
|
|
@@ -89,7 +90,7 @@ describe('digestPendingConversations', () => {
|
|
|
89
90
|
assert.equal(result.count, 1);
|
|
90
91
|
assert.equal(result.errors, 0);
|
|
91
92
|
}
|
|
92
|
-
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'digest-worker.lock')), false);
|
|
93
|
+
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'wiki', 'digest', 'digest-worker.lock')), false);
|
|
93
94
|
});
|
|
94
95
|
test('C17: digestFn throws for one file → errors:1, lock released', async () => {
|
|
95
96
|
const workspace = tempDirs.makeTempDir('pending-c17-');
|
|
@@ -108,19 +109,19 @@ describe('digestPendingConversations', () => {
|
|
|
108
109
|
assert.equal(result.errors, 1);
|
|
109
110
|
assert.equal(result.count, 0);
|
|
110
111
|
}
|
|
111
|
-
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'digest-worker.lock')), false);
|
|
112
|
+
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'wiki', 'digest', 'digest-worker.lock')), false);
|
|
112
113
|
});
|
|
113
114
|
test('C18: stale lockfile (dead PID) → reclaims lock and returns (no-pending since no conv files)', async () => {
|
|
114
115
|
const workspace = tempDirs.makeTempDir('pending-c18-');
|
|
115
116
|
writeInitState(workspace);
|
|
116
|
-
const lockDir = path.join(workspace, '.agent-kit');
|
|
117
|
+
const lockDir = path.join(workspace, '.agent-kit', 'wiki', 'digest');
|
|
117
118
|
fs.mkdirSync(lockDir, { recursive: true });
|
|
118
119
|
fs.writeFileSync(path.join(lockDir, 'digest-worker.lock'), JSON.stringify({ pid: 999999999 }));
|
|
119
120
|
const result = await digestPendingConversations({ workspaceRoot: workspace });
|
|
120
121
|
assert.ok(result.ok);
|
|
121
122
|
if (result.action === 'noop')
|
|
122
123
|
assert.notEqual(result.reason, 'locked');
|
|
123
|
-
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'digest-worker.lock')), false);
|
|
124
|
+
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'wiki', 'digest', 'digest-worker.lock')), false);
|
|
124
125
|
});
|
|
125
126
|
test('C20: 3 undigested conv files → digestFn called 3 times, count+skipped+errors=3', async () => {
|
|
126
127
|
const workspace = tempDirs.makeTempDir('pending-c20-');
|
|
@@ -159,6 +160,33 @@ describe('digestPendingConversations', () => {
|
|
|
159
160
|
const result = await digestPendingConversations({ workspaceRoot: workspace });
|
|
160
161
|
assert.equal(result.ok, false);
|
|
161
162
|
assert.equal(result.action, 'error');
|
|
162
|
-
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'digest-worker.lock')), false);
|
|
163
|
+
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'wiki', 'digest', 'digest-worker.lock')), false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
describe('summarizePendingConversations', () => {
|
|
167
|
+
test('returns not-initialized without raw discovery side effects', () => {
|
|
168
|
+
const workspace = tempDirs.makeTempDir('pending-summary-init-');
|
|
169
|
+
const result = summarizePendingConversations(workspace);
|
|
170
|
+
assert.equal(result.initialized, false);
|
|
171
|
+
assert.equal(result.pending, 0);
|
|
172
|
+
assert.equal(result.reason, 'not-initialized');
|
|
173
|
+
});
|
|
174
|
+
test('returns no-pending when initialized workspace has no raw conversations', () => {
|
|
175
|
+
const workspace = tempDirs.makeTempDir('pending-summary-empty-');
|
|
176
|
+
writeInitState(workspace);
|
|
177
|
+
const result = summarizePendingConversations(workspace);
|
|
178
|
+
assert.equal(result.initialized, true);
|
|
179
|
+
assert.equal(result.pending, 0);
|
|
180
|
+
assert.equal(result.reason, 'no-pending');
|
|
181
|
+
});
|
|
182
|
+
test('counts only conversations without existing provisional digests', () => {
|
|
183
|
+
const workspace = tempDirs.makeTempDir('pending-summary-count-');
|
|
184
|
+
writeInitState(workspace);
|
|
185
|
+
const first = writeConvFile(workspace, 'conv_2026-01-01T00-00-00-001Z.md', '**User:** a\n');
|
|
186
|
+
writeConvFile(workspace, 'conv_2026-01-01T00-00-00-002Z.md', '**User:** b\n');
|
|
187
|
+
writeProvisionalDigestFile(defaultProvisionalDigestDir(workspace), readConversationDigestInput(workspace, first), '# Existing\n');
|
|
188
|
+
const result = summarizePendingConversations(workspace);
|
|
189
|
+
assert.equal(result.initialized, true);
|
|
190
|
+
assert.equal(result.pending, 1);
|
|
163
191
|
});
|
|
164
192
|
});
|