@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.
Files changed (111) hide show
  1. package/dist/cli/commands/cli-digest-pending.test.js +111 -0
  2. package/dist/cli/commands/cli-memory.test.js +34 -0
  3. package/dist/cli/{memory.js → commands/memory.js} +24 -11
  4. package/dist/cli/index.js +2 -2
  5. package/dist/{utils → cli/utils}/args.d.ts +2 -1
  6. package/dist/{kit-cli.js → cli.js} +342 -58
  7. package/dist/{tools/config.d.ts → core/config/index.d.ts} +2 -7
  8. package/dist/{tools/config.js → core/config/index.js} +2 -13
  9. package/dist/core/handoffs/index.d.ts +17 -0
  10. package/dist/{tools/handoffs.js → core/handoffs/index.js} +15 -7
  11. package/dist/core/handoffs/index.test.js +88 -0
  12. package/dist/{tools/security.d.ts → core/security/index.d.ts} +0 -19
  13. package/dist/{tools/security.js → core/security/index.js} +3 -76
  14. package/dist/entrypoints/cli.js +11 -0
  15. package/dist/entrypoints/server.js +37 -0
  16. package/dist/{tools → mcp}/agent.d.ts +1 -1
  17. package/dist/{tools → mcp}/agent.js +4 -4
  18. package/dist/mcp/core.d.ts +17 -0
  19. package/dist/mcp/core.js +60 -0
  20. package/dist/mcp/core.test.js +98 -0
  21. package/dist/{tools → mcp}/integration.js +3 -3
  22. package/dist/mcp/memory.d.ts +15 -0
  23. package/dist/{tools → mcp}/memory.js +15 -13
  24. package/dist/{kit-server.js → server.js} +129 -98
  25. package/dist/services/digest/background.d.ts +11 -0
  26. package/dist/services/digest/background.js +258 -0
  27. package/dist/{memory → services}/digest/constants.d.ts +4 -1
  28. package/dist/{memory → services}/digest/constants.js +6 -2
  29. package/dist/services/digest/digest-background.test.js +206 -0
  30. package/dist/{tools/__tests__ → services/digest}/digest-init.test.js +4 -3
  31. package/dist/{memory/__tests__ → services/digest}/digest-llama-local.test.js +1 -13
  32. package/dist/{memory/__tests__ → services/digest}/digest-pending.test.js +39 -11
  33. package/dist/{memory/__tests__ → services/digest}/digest-processor.test.js +5 -7
  34. package/dist/{memory → services}/digest/files.d.ts +0 -1
  35. package/dist/{memory → services}/digest/files.js +2 -2
  36. package/dist/services/digest/model-registry.d.ts +2 -0
  37. package/dist/{memory → services}/digest/model-registry.js +9 -8
  38. package/dist/services/digest/processor.d.ts +13 -0
  39. package/dist/{memory → services}/digest/processor.js +48 -31
  40. package/dist/services/digest/providers/llama-local.d.ts +2 -0
  41. package/dist/{memory → services}/digest/providers/llama-local.js +3 -4
  42. package/dist/{memory → services}/digest/types.d.ts +34 -2
  43. package/dist/services/digest/types.js +6 -0
  44. package/dist/{memory → services}/digest/worker.d.ts +0 -1
  45. package/dist/{memory → services}/digest/worker.js +2 -2
  46. package/dist/{utils → services/integration}/credentials.js +1 -1
  47. package/dist/{utils/parser.js → services/integration/parser/adf.js} +1 -0
  48. package/dist/{memory → services/memory}/chunker.d.ts +0 -2
  49. package/dist/{memory → services/memory}/chunker.js +3 -3
  50. package/dist/{memory/__tests__ → services/memory}/chunker.test.js +1 -34
  51. package/dist/{memory → services/memory}/constants.d.ts +1 -1
  52. package/dist/{memory → services/memory}/constants.js +2 -1
  53. package/dist/{memory → services/memory}/embedder.d.ts +2 -2
  54. package/dist/{memory → services/memory}/embedder.js +5 -5
  55. package/dist/services/memory/embedder.test.js +13 -0
  56. package/dist/{memory → services/memory}/indexer.d.ts +2 -1
  57. package/dist/{memory → services/memory}/indexer.js +1 -1
  58. package/dist/{memory/__tests__ → services/memory}/indexer.test.js +4 -3
  59. package/dist/{tools/__tests__ → services/memory}/memory.test.js +54 -13
  60. package/dist/{memory/__tests__ → services/memory}/store.test.js +3 -2
  61. package/dist/{memory → services/memory}/types.d.ts +7 -2
  62. package/dist/services/memory/types.js +6 -0
  63. package/dist/utils/files.d.ts +0 -1
  64. package/dist/utils/files.js +1 -1
  65. package/dist/{memory/__tests__/lockfile.test.js → utils/files.test.js} +14 -14
  66. package/dist/utils/index.d.ts +6 -0
  67. package/dist/utils/index.js +6 -0
  68. package/dist/utils/json.d.ts +0 -1
  69. package/dist/utils/json.js +0 -10
  70. package/dist/utils/paths.d.ts +0 -1
  71. package/dist/utils/paths.js +1 -1
  72. package/package.json +6 -6
  73. package/dist/memory/__tests__/digest-worker.test.js +0 -46
  74. package/dist/memory/__tests__/embedder.test.js +0 -14
  75. package/dist/memory/digest/model-registry.d.ts +0 -6
  76. package/dist/memory/digest/processor.d.ts +0 -7
  77. package/dist/memory/digest/providers/llama-local.d.ts +0 -7
  78. package/dist/tools/__tests__/cli-digest-pending.test.js +0 -51
  79. package/dist/tools/__tests__/cli-memory.test.js +0 -22
  80. package/dist/tools/core.d.ts +0 -10
  81. package/dist/tools/core.js +0 -46
  82. package/dist/tools/handoffs.d.ts +0 -22
  83. package/dist/tools/memory.d.ts +0 -16
  84. /package/dist/{tools/__tests__ → cli/commands}/cli-digest-pending.test.d.ts +0 -0
  85. /package/dist/{tools/__tests__ → cli/commands}/cli-memory.test.d.ts +0 -0
  86. /package/dist/cli/{memory.d.ts → commands/memory.d.ts} +0 -0
  87. /package/dist/{utils → cli/utils}/args.js +0 -0
  88. /package/dist/{memory/__tests__/chunker.test.d.ts → core/handoffs/index.test.d.ts} +0 -0
  89. /package/dist/{kit-cli.d.ts → entrypoints/cli.d.ts} +0 -0
  90. /package/dist/{kit-server.d.ts → entrypoints/server.d.ts} +0 -0
  91. /package/dist/{memory/__tests__/digest-llama-local.test.d.ts → mcp/core.test.d.ts} +0 -0
  92. /package/dist/{tools → mcp}/integration.d.ts +0 -0
  93. /package/dist/{memory/__tests__/digest-pending.test.d.ts → services/digest/digest-background.test.d.ts} +0 -0
  94. /package/dist/{tools/__tests__ → services/digest}/digest-init.test.d.ts +0 -0
  95. /package/dist/{memory/__tests__/digest-processor.test.d.ts → services/digest/digest-llama-local.test.d.ts} +0 -0
  96. /package/dist/{memory/__tests__/digest-worker.test.d.ts → services/digest/digest-pending.test.d.ts} +0 -0
  97. /package/dist/{memory/__tests__/embedder.test.d.ts → services/digest/digest-processor.test.d.ts} +0 -0
  98. /package/dist/{utils → services/integration}/credentials.d.ts +0 -0
  99. /package/dist/{utils/parser.d.ts → services/integration/parser/adf.d.ts} +0 -0
  100. /package/dist/{memory/__tests__/indexer.test.d.ts → services/memory/chunker.test.d.ts} +0 -0
  101. /package/dist/{memory/__tests__/lockfile.test.d.ts → services/memory/embedder.test.d.ts} +0 -0
  102. /package/dist/{memory → services/memory}/index.d.ts +0 -0
  103. /package/dist/{memory → services/memory}/index.js +0 -0
  104. /package/dist/{memory/__tests__/store.test.d.ts → services/memory/indexer.test.d.ts} +0 -0
  105. /package/dist/{tools/__tests__ → services/memory}/memory.test.d.ts +0 -0
  106. /package/dist/{memory → services/memory}/store.d.ts +0 -0
  107. /package/dist/{memory → services/memory}/store.js +0 -0
  108. /package/dist/{memory/digest/types.js → services/memory/store.test.d.ts} +0 -0
  109. /package/dist/{memory/types.js → utils/files.test.d.ts} +0 -0
  110. /package/dist/{__tests__/temp-dir.d.ts → utils/temp-dir.test.d.ts} +0 -0
  111. /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
- export declare const DEFAULT_DIGEST_MODEL_ID = "qwen2.5-1.5b-instruct-q4";
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
- export const DEFAULT_DIGEST_MODEL_ID = 'qwen2.5-1.5b-instruct-q4';
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
- export const DIGEST_LOCKFILE_REL_PATH = path.join(KIT_DIR, 'digest-worker.lock');
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 '../../__tests__/temp-dir.js';
6
- import { initializeConversationDigestModel } from '../../memory/digest/processor.js';
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: 'qwen2.5-1.5b-instruct-q4',
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, sanitizeConversationDigestMarkdown, trimConversationExport, } from '../digest/providers/llama-local.js';
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 '../../__tests__/temp-dir.js';
6
- import { writeConversationDigestSettings } from '../digest/files.js';
7
- import { digestPendingConversations } from '../digest/processor.js';
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 = 'qwen2.5-1.5b-instruct-q4') {
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
  });