claude-code-session-manager 0.112.0 → 0.114.0
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/.claude-plugin/marketplace.json +1 -1
- package/dist/assets/{AgentLibrary-RwPvEgqm.js → AgentLibrary-DZFM7Lto.js} +2 -2
- package/dist/assets/{Button-36j3PzYv.js → Button-BJVFKdGH.js} +1 -1
- package/dist/assets/{DataModel-DUhANo4l.js → DataModel-CebvcP_y.js} +1 -1
- package/dist/assets/{History-BeZzNjmv.js → History-BwISEUhR.js} +2 -2
- package/dist/assets/{Hooks-iQ8845Hp.js → Hooks-Dac3smYP.js} +3 -3
- package/dist/assets/{Library-DPE1h-1L.js → Library-DTjsUr9W.js} +7 -7
- package/dist/assets/{ListDetail-PStOuQAo.js → ListDetail-B1L5TbjS.js} +1 -1
- package/dist/assets/{MarkdownEditor-DPw-N2-m.js → MarkdownEditor-BFRhWZD4.js} +1 -1
- package/dist/assets/{McpServers-CUyRTNep.js → McpServers-CBjGCMg0.js} +2 -2
- package/dist/assets/Memory-DdLi5fqe.js +8 -0
- package/dist/assets/{Panel-_VZ_uXNg.js → Panel-BQkkkjex.js} +1 -1
- package/dist/assets/{Permissions-PPRmfNbK.js → Permissions-CaACwY0B.js} +3 -3
- package/dist/assets/{Plugins-BFcvfvlx.js → Plugins-DJeBwVoh.js} +2 -2
- package/dist/assets/ProvenanceBadge--N5j8xiK.js +1 -0
- package/dist/assets/{RemoteMachines-D5V0lzgW.js → RemoteMachines-WjWaDHEA.js} +2 -2
- package/dist/assets/{SaveBar-Dw64FVdM.js → SaveBar-DFmOjb4_.js} +1 -1
- package/dist/assets/{Scheduler-C8s_pJ3I.js → Scheduler-Miap6_aY.js} +8 -8
- package/dist/assets/{ScopeSwitcher-D-nKDMAE.js → ScopeSwitcher-CiEujp4L.js} +1 -1
- package/dist/assets/Settings-UGpW4P0j.js +4 -0
- package/dist/assets/{SkillReferenceGraph-BbZFMHim.js → SkillReferenceGraph-BgyAnxGW.js} +1 -1
- package/dist/assets/Skills-BQUpnNfx.js +2 -0
- package/dist/assets/SystemPrompt-CQjOgHGT.js +1 -0
- package/dist/assets/TagLibrary-CTVpoLl0.js +1 -0
- package/dist/assets/{TiptapBody-Cr-hxX8L.js → TiptapBody-BtOcrdRR.js} +1 -1
- package/dist/assets/{Toggle-7WpPzcZH.js → Toggle-Chri4G19.js} +1 -1
- package/dist/assets/{ViewTabs-9lQCRwWJ.js → ViewTabs-BIenDGVv.js} +1 -1
- package/dist/assets/{historyTotals-BrFxw5J1.js → historyTotals-CX8mUxJY.js} +1 -1
- package/dist/assets/{index-CTBpf-26.js → index-AVrhGmUO.js} +163 -163
- package/dist/assets/{index-Dutcsly-.css → index-HpEbvDiE.css} +1 -1
- package/dist/assets/{settingsSchema-Dh5Kl7VO.js → settingsSchema-uqwlznbH.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-CMpx0d04.js → useAgentsDirWatch-BaezRtWw.js} +1 -1
- package/dist/assets/{useRemotePeers-CI2axd-L.js → useRemotePeers-BuFbBqDt.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/plugins/session-manager-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/session-manager-dev/skills/send-feedback/SKILL.md +13 -0
- package/scripts/scheduler-mcp-server.cjs +23 -5
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +18 -1
- package/src/main/config.cjs +18 -1
- package/src/main/index.cjs +6 -2
- package/src/main/ipcSchemas.cjs +27 -34
- package/src/main/lib/activeSessions.cjs +13 -0
- package/src/main/lib/claude/rateLimitDetect.cjs +29 -10
- package/src/main/lib/claude/rateLimitWindow.cjs +23 -11
- package/src/main/lib/credentials.cjs +16 -90
- package/src/main/lib/feedback-remote/crossProjectFeedback.cjs +107 -9
- package/src/main/lib/feedback-remote/humanPromptLedger.cjs +93 -0
- package/src/main/lib/mcpToolCatalog.cjs +9 -3
- package/src/main/lib/memoryDirResolve.cjs +139 -0
- package/src/main/lib/memoryIndex.cjs +98 -0
- package/src/main/lib/prd/depSlugResolve.cjs +16 -1
- package/src/main/lib/scheduler/finalizeReason.cjs +30 -0
- package/src/main/lib/scheduler/flatSweepThrottle.cjs +17 -0
- package/src/main/lib/scheduler/runLogRetention.cjs +34 -13
- package/src/main/lib/scheduler/schedulerBatch.cjs +8 -12
- package/src/main/lib/telemetry/telemetryClient.cjs +8 -4
- package/src/main/lib/util/atomicFs.cjs +42 -0
- package/src/main/lib/util/loopDelay.cjs +1 -1
- package/src/main/memoryAggregate.cjs +15 -8
- package/src/main/memoryTool.cjs +151 -22
- package/src/main/runVerify.cjs +5 -2
- package/src/main/scheduler.cjs +43 -10
- package/src/main/supervisor.cjs +45 -22
- package/src/main/webRemote.cjs +15 -0
- package/src/preload/api.d.ts +47 -52
- package/src/preload/index.cjs +25 -21
- package/dist/assets/Memory-bZ2xjBGy.js +0 -8
- package/dist/assets/ProvenanceBadge-B1WUknak.js +0 -1
- package/dist/assets/Settings-CQSrR4mO.js +0 -4
- package/dist/assets/Skills-Bzyrh6EQ.js +0 -2
- package/dist/assets/SystemPrompt-DP6i3Dx0.js +0 -1
- package/dist/assets/TagLibrary-iip0C6Bl.js +0 -1
- package/src/main/agentMemory.cjs +0 -246
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* runLogRetention.cjs — computes
|
|
5
|
-
* applies) a retention policy over `scheduled-plans/runs/`.
|
|
4
|
+
* runLogRetention.cjs — computes and applies a retention policy over `scheduled-plans/runs/`.
|
|
6
5
|
*
|
|
7
6
|
* That directory is a machine-level Session-Manager runtime artifact store
|
|
8
7
|
* (execution logs of the scheduler's own `claude -p` runs), not anything
|
|
@@ -26,10 +25,10 @@
|
|
|
26
25
|
* slug's own files.
|
|
27
26
|
*
|
|
28
27
|
* SAFETY MODEL (the point of this module — see the PRD that added it):
|
|
29
|
-
* -
|
|
30
|
-
* `schedulerRunLogRetention
|
|
31
|
-
*
|
|
32
|
-
*
|
|
28
|
+
* - Retention is ON by default (DEFAULT_RUN_LOG_RETENTION) when settings
|
|
29
|
+
* carry no `schedulerRunLogRetention`. An explicit `enabled: false` (or a
|
|
30
|
+
* config with no non-empty policy) opts out: computeReport, and
|
|
31
|
+
* applyRetention when opted out, only READ the filesystem.
|
|
33
32
|
* - An entry belonging to a job that is currently live — status `pending`
|
|
34
33
|
* (queued), `running`, `needs_review`, or `investigating` (the exact
|
|
35
34
|
* literals scheduler.cjs uses; see LIVE_STATUSES) — is never eligible,
|
|
@@ -51,6 +50,16 @@ const schedulerPaths = require('./schedulerPaths.cjs');
|
|
|
51
50
|
// checks) — 'completed' and 'failed' are the only terminal ones.
|
|
52
51
|
const LIVE_STATUSES = new Set(['pending', 'running', 'needs_review', 'investigating']);
|
|
53
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Built-in policy applied when settings carry no `schedulerRunLogRetention`:
|
|
55
|
+
* drop runs older than 14 days but always keep each slug's newest 3.
|
|
56
|
+
* (Policy keys follow the module's own names: maxAgeDays / keepPerSlug.)
|
|
57
|
+
*/
|
|
58
|
+
const DEFAULT_RUN_LOG_RETENTION = Object.freeze({
|
|
59
|
+
enabled: true,
|
|
60
|
+
policy: Object.freeze({ maxAgeDays: 14, keepPerSlug: 3 }),
|
|
61
|
+
});
|
|
62
|
+
|
|
54
63
|
const META_SUFFIX = '.meta.json';
|
|
55
64
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
56
65
|
|
|
@@ -259,11 +268,21 @@ function computeReport(runsDir, policy, opts) {
|
|
|
259
268
|
}
|
|
260
269
|
|
|
261
270
|
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
271
|
+
* The effective retention config: the caller's `schedulerRunLogRetention`
|
|
272
|
+
* when present, else DEFAULT_RUN_LOG_RETENTION.
|
|
264
273
|
*/
|
|
265
|
-
function
|
|
274
|
+
function resolveRetentionConfig(settings) {
|
|
266
275
|
const cfg = settings && settings.schedulerRunLogRetention;
|
|
276
|
+
return cfg == null ? DEFAULT_RUN_LOG_RETENTION : cfg;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* True when the effective config (see resolveRetentionConfig) has
|
|
281
|
+
* `enabled === true` plus a non-empty policy. An absent setting resolves to
|
|
282
|
+
* the default (enabled); an explicit `enabled: false` opts out.
|
|
283
|
+
*/
|
|
284
|
+
function isRetentionEnabled(settings) {
|
|
285
|
+
const cfg = resolveRetentionConfig(settings);
|
|
267
286
|
if (!cfg || cfg.enabled !== true) return false;
|
|
268
287
|
const policy = cfg.policy;
|
|
269
288
|
return !!policy && (policy.maxAgeDays != null || policy.keepPerSlug != null);
|
|
@@ -372,7 +391,7 @@ function sweepOrphanDirs(runsDir, runningRunIds, errors) {
|
|
|
372
391
|
* nothing removed.
|
|
373
392
|
*/
|
|
374
393
|
function applyRetention(runsDir, settings, opts) {
|
|
375
|
-
const cfg = (settings
|
|
394
|
+
const cfg = resolveRetentionConfig(settings);
|
|
376
395
|
const policy = (cfg && cfg.policy) || {};
|
|
377
396
|
const enabled = isRetentionEnabled(settings);
|
|
378
397
|
const liveKeys = resolveLiveKeysForApply(runsDir, opts, enabled);
|
|
@@ -381,7 +400,7 @@ function applyRetention(runsDir, settings, opts) {
|
|
|
381
400
|
if (!enabled) {
|
|
382
401
|
return {
|
|
383
402
|
deleted: false,
|
|
384
|
-
reason: 'dry-run: schedulerRunLogRetention
|
|
403
|
+
reason: 'dry-run: schedulerRunLogRetention is disabled (enabled !== true, or has no policy)',
|
|
385
404
|
report,
|
|
386
405
|
};
|
|
387
406
|
}
|
|
@@ -424,8 +443,8 @@ function applyRetention(runsDir, settings, opts) {
|
|
|
424
443
|
* applyRetention's own internal queueStore fallback (resolveLiveKeysForApply
|
|
425
444
|
* above), which exists only as a defensive net for callers who forget to
|
|
426
445
|
* thread live-job info through, not as this module's primary path. Deletion
|
|
427
|
-
*
|
|
428
|
-
* schedulerRunLogRetention.enabled ===
|
|
446
|
+
* happens unless the read config opts out with
|
|
447
|
+
* schedulerRunLogRetention.enabled === false (see isRetentionEnabled).
|
|
429
448
|
*
|
|
430
449
|
* Intended to be called once per app boot (see index.cjs's deferred 30s
|
|
431
450
|
* startup timer, alongside finalizeClosedDays) — never on its own timer.
|
|
@@ -438,6 +457,8 @@ function runBootSweep(opts) {
|
|
|
438
457
|
}
|
|
439
458
|
|
|
440
459
|
module.exports = {
|
|
460
|
+
DEFAULT_RUN_LOG_RETENTION,
|
|
461
|
+
resolveRetentionConfig,
|
|
441
462
|
LIVE_STATUSES,
|
|
442
463
|
isLiveJob,
|
|
443
464
|
liveKeysFromJobs,
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
const path = require('node:path');
|
|
16
16
|
const os = require('node:os');
|
|
17
17
|
const { projectJobCap, quietMachineWaitMs } = require('./schedulerConfig.cjs');
|
|
18
|
-
const { bareSlug } = require('../prd/depSlugResolve.cjs');
|
|
18
|
+
const { bareSlug, isSatisfiedDepRow } = require('../prd/depSlugResolve.cjs');
|
|
19
19
|
|
|
20
20
|
const DEFAULT_PROJECT_CWD = path.join(os.homedir(), 'Projects', 'session-manager');
|
|
21
21
|
|
|
@@ -127,20 +127,16 @@ function findUnresolvableDepRoots(projectJobs, satisfiedSlugs = new Set()) {
|
|
|
127
127
|
|
|
128
128
|
function findBlockingDep(job, projectJobs, satisfiedSlugs = new Set()) {
|
|
129
129
|
const { rowsForDep, isKnownSatisfied } = makeDepResolver(projectJobs, satisfiedSlugs);
|
|
130
|
-
// A dep row is blocking unless
|
|
131
|
-
//
|
|
132
|
-
// auto-resolve terminal decision (scheduler.cjs's
|
|
133
|
-
// applyNeedsReviewAutoResolve). That skip is deliberately NOT the
|
|
130
|
+
// A dep row is blocking unless isSatisfiedDepRow — 'completed', or a
|
|
131
|
+
// `needsReviewAutoResolvedSkip` skip. That skip is deliberately NOT the
|
|
134
132
|
// "PRD source vanished, a human must author a fresh PRD" skip the
|
|
135
|
-
// heldBySkippedDep messaging
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
const isSatisfiedRow = (dep) => dep.status === 'completed'
|
|
140
|
-
|| (dep.status === 'skipped' && dep.needsReviewAutoResolvedSkip === true);
|
|
133
|
+
// heldBySkippedDep messaging describes — it already got every bounded
|
|
134
|
+
// chance to resolve itself, so treating it as a permanent dependsOn block
|
|
135
|
+
// would defeat the auto-resolve pass. runVerify.checkDeps MUST use the same
|
|
136
|
+
// predicate, or every run of the dependent is downgraded and re-dispatched.
|
|
141
137
|
return (job.dependsOn ?? []).find((slug) => {
|
|
142
138
|
const rows = rowsForDep(slug);
|
|
143
|
-
if (rows.length > 0) return rows.some((dep) => !
|
|
139
|
+
if (rows.length > 0) return rows.some((dep) => !isSatisfiedDepRow(dep));
|
|
144
140
|
return !isKnownSatisfied(slug);
|
|
145
141
|
});
|
|
146
142
|
}
|
|
@@ -647,18 +647,20 @@ function describeFailureStatus(status) {
|
|
|
647
647
|
return status === 0 ? 'network error' : `HTTP ${status}`;
|
|
648
648
|
}
|
|
649
649
|
|
|
650
|
-
function applyFailureBackoff(status) {
|
|
650
|
+
function applyFailureBackoff(status, channel) {
|
|
651
651
|
if (status === 429 || status === 0 || status >= 500) {
|
|
652
652
|
S.consecutiveFailures += 1;
|
|
653
653
|
const ms = Math.min(BACKOFF_BASE_MS * (2 ** (S.consecutiveFailures - 1)), BACKOFF_MAX_MS);
|
|
654
654
|
S.backoffUntil = Date.now() + ms;
|
|
655
655
|
S.lastError = { status, message: describeFailureStatus(status), at: Date.now() };
|
|
656
|
+
logWarn('telemetry send failed', { channel, status, consecutiveFailures: S.consecutiveFailures, backoffMs: ms });
|
|
656
657
|
return;
|
|
657
658
|
}
|
|
658
659
|
if (status >= 400 && status < 500) {
|
|
659
660
|
// Malformed-contract circuit breaker (includes 401) — not retried.
|
|
660
661
|
S.disabledForProcess = true;
|
|
661
662
|
S.lastError = { status, message: describeFailureStatus(status), at: Date.now() };
|
|
663
|
+
logWarn('telemetry disabled for process', { status });
|
|
662
664
|
}
|
|
663
665
|
}
|
|
664
666
|
|
|
@@ -705,7 +707,7 @@ async function flushImpl(reason) {
|
|
|
705
707
|
result.sent.push(r.recordId);
|
|
706
708
|
} else {
|
|
707
709
|
result.failed.push(r.recordId);
|
|
708
|
-
applyFailureBackoff(res.status);
|
|
710
|
+
applyFailureBackoff(res.status, channel);
|
|
709
711
|
sawFailure = true;
|
|
710
712
|
break;
|
|
711
713
|
}
|
|
@@ -726,7 +728,7 @@ async function flushImpl(reason) {
|
|
|
726
728
|
}
|
|
727
729
|
} else {
|
|
728
730
|
for (const r of batch) result.failed.push(r.recordId);
|
|
729
|
-
applyFailureBackoff(res.status);
|
|
731
|
+
applyFailureBackoff(res.status, channel);
|
|
730
732
|
sawFailure = true;
|
|
731
733
|
break;
|
|
732
734
|
}
|
|
@@ -739,7 +741,9 @@ async function flushImpl(reason) {
|
|
|
739
741
|
if (safeReason === 'daily' && result.failed.length === 0) {
|
|
740
742
|
S.settings = await telemetrySettings.save({ lastDailyFlushAt: new Date().toISOString() });
|
|
741
743
|
}
|
|
742
|
-
} catch {
|
|
744
|
+
} catch (e) {
|
|
745
|
+
logWarn('telemetry flush threw', { error: e && e.message ? e.message : String(e) });
|
|
746
|
+
}
|
|
743
747
|
S.lastFlushAt = Date.now();
|
|
744
748
|
S.lastFlushReason = safeReason;
|
|
745
749
|
return result;
|
|
@@ -105,7 +105,49 @@ function readJsonOrSync(file, fallback) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
function defaultIsPidAlive(pid) {
|
|
109
|
+
try {
|
|
110
|
+
process.kill(pid, 0);
|
|
111
|
+
return true;
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return e?.code === 'EPERM'; // exists but owned by someone else
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Remove orphaned `<file>.tmp-<pid>-<ts>-<rand>` files left by crashed
|
|
119
|
+
* writers. Direct children of `dir` only (no recursion); a file goes only when
|
|
120
|
+
* its mtime is older than `olderThanMs` AND its embedded pid is not alive.
|
|
121
|
+
* `.corrupt-*` / `.bak*` forensics never match. Never throws.
|
|
122
|
+
*/
|
|
123
|
+
function sweepStaleTmpFiles(dir, { olderThanMs = 3600000, isPidAlive = defaultIsPidAlive } = {}) {
|
|
124
|
+
const removed = [];
|
|
125
|
+
let names;
|
|
126
|
+
try {
|
|
127
|
+
names = fs.readdirSync(dir);
|
|
128
|
+
} catch {
|
|
129
|
+
return { removed };
|
|
130
|
+
}
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
for (const name of names) {
|
|
133
|
+
try {
|
|
134
|
+
const m = /\.tmp-(\d+)-/.exec(name);
|
|
135
|
+
if (!m) continue;
|
|
136
|
+
if (/\.(corrupt|bak)/.test(name)) continue;
|
|
137
|
+
const full = path.join(dir, name);
|
|
138
|
+
const st = fs.lstatSync(full);
|
|
139
|
+
if (!st.isFile()) continue;
|
|
140
|
+
if (now - st.mtimeMs <= olderThanMs) continue;
|
|
141
|
+
if (isPidAlive(Number(m[1]))) continue;
|
|
142
|
+
fs.unlinkSync(full);
|
|
143
|
+
removed.push(full);
|
|
144
|
+
} catch { /* per-file errors are skipped */ }
|
|
145
|
+
}
|
|
146
|
+
return { removed };
|
|
147
|
+
}
|
|
148
|
+
|
|
108
149
|
module.exports = {
|
|
150
|
+
sweepStaleTmpFiles,
|
|
109
151
|
tmpPathFor,
|
|
110
152
|
writeTextAtomic,
|
|
111
153
|
writeTextAtomicSync,
|
|
@@ -11,7 +11,7 @@ const { monitorEventLoopDelay } = require('node:perf_hooks');
|
|
|
11
11
|
|
|
12
12
|
const LOOP_DELAY_RESOLUTION_MS = 20;
|
|
13
13
|
// A single beat whose worst loop delay exceeds this is a visible main-thread stall.
|
|
14
|
-
const LOOP_STALL_WARN_MS =
|
|
14
|
+
const LOOP_STALL_WARN_MS = 1000;
|
|
15
15
|
|
|
16
16
|
const NS_PER_MS = 1e6;
|
|
17
17
|
const toMs = (ns) => Math.round(ns / NS_PER_MS * 10) / 10;
|
|
@@ -33,8 +33,15 @@ const { MEMORY_SLUG_RE } = require('./lib/memorySlug.cjs');
|
|
|
33
33
|
const HOME = os.homedir();
|
|
34
34
|
const CACHE_WRITER = 'memory-clusters';
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// Resolved through memoryTool.cjs so cwd-addressed callers read Claude Code's
|
|
37
|
+
// native dir (git root + autoMemoryDirectory); no cwd -> legacy workspace dir.
|
|
38
|
+
async function memoryDir(workspace, cwd) {
|
|
39
|
+
const { resolveDir } = require('./memoryTool.cjs');
|
|
40
|
+
try {
|
|
41
|
+
return (await resolveDir({ workspace, cwd })).dir;
|
|
42
|
+
} catch {
|
|
43
|
+
return path.join(HOME, '.claude', 'projects', workspace, 'memory');
|
|
44
|
+
}
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
function clustersCachePath(cwd) {
|
|
@@ -94,15 +101,15 @@ function parseFrontmatter(text) {
|
|
|
94
101
|
return { type, description, body: m[2] };
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
async function listMemoryFiles(workspace) {
|
|
98
|
-
const dir = memoryDir(workspace);
|
|
104
|
+
async function listMemoryFiles(workspace, cwd) {
|
|
105
|
+
const dir = await memoryDir(workspace, cwd);
|
|
99
106
|
const r = await config.listDir(dir, { filesOnly: true });
|
|
100
107
|
if (!r.ok) return [];
|
|
101
108
|
return r.entries.filter((e) => MEMORY_SLUG_RE.test(e.name)).map((e) => e.name).sort();
|
|
102
109
|
}
|
|
103
110
|
|
|
104
|
-
async function readMemories(workspace, names) {
|
|
105
|
-
const dir = memoryDir(workspace);
|
|
111
|
+
async function readMemories(workspace, names, cwd) {
|
|
112
|
+
const dir = await memoryDir(workspace, cwd);
|
|
106
113
|
const out = [];
|
|
107
114
|
for (const name of names) {
|
|
108
115
|
const abs = path.join(dir, name);
|
|
@@ -178,8 +185,8 @@ async function aggregate({ workspace, refresh, cwd }) {
|
|
|
178
185
|
return { workspace: ws, generatedAt: null, clusters: [], orphans: [], cached: false };
|
|
179
186
|
}
|
|
180
187
|
|
|
181
|
-
const names = await listMemoryFiles(ws);
|
|
182
|
-
const memories = await readMemories(ws, names);
|
|
188
|
+
const names = await listMemoryFiles(ws, cwd);
|
|
189
|
+
const memories = await readMemories(ws, names, cwd);
|
|
183
190
|
const slugs = memories.map((m) => m.slug);
|
|
184
191
|
|
|
185
192
|
let clusters = [];
|
package/src/main/memoryTool.cjs
CHANGED
|
@@ -32,6 +32,14 @@ const config = require('./config.cjs');
|
|
|
32
32
|
|
|
33
33
|
const { MEMORY_SLUG_RE: SLUG_RE } = require('./lib/memorySlug.cjs');
|
|
34
34
|
const { scoreMemories } = require('./lib/memoryStale.cjs');
|
|
35
|
+
const { addIndexLine, removeIndexLines, indexHealth } = require('./lib/memoryIndex.cjs');
|
|
36
|
+
const {
|
|
37
|
+
resolveWorkspaceMemoryDir,
|
|
38
|
+
autoMemoryEnabled,
|
|
39
|
+
resolveAgentMemoryDir,
|
|
40
|
+
listMemoryAgents,
|
|
41
|
+
gitRepoRoot,
|
|
42
|
+
} = require('./lib/memoryDirResolve.cjs');
|
|
35
43
|
|
|
36
44
|
const MAX_FILE_BYTES = 1024 * 1024; // 1 MiB
|
|
37
45
|
const MAX_ENTRIES = 1000;
|
|
@@ -52,13 +60,59 @@ function workspaceDir(workspace) {
|
|
|
52
60
|
return path.join(memoryRoot(), workspace, 'memory');
|
|
53
61
|
}
|
|
54
62
|
|
|
63
|
+
/** Read + parse a settings file; missing/malformed -> null. */
|
|
64
|
+
async function readSettings(abs) {
|
|
65
|
+
try {
|
|
66
|
+
const r = await config.readText(abs);
|
|
67
|
+
if (!r.exists) return null;
|
|
68
|
+
const parsed = JSON.parse(r.text);
|
|
69
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The one place a memory directory is chosen.
|
|
77
|
+
* agent -> native subagent memory dir (root = git root of cwd, or cwd)
|
|
78
|
+
* cwd -> Claude Code's native workspace dir (git root + autoMemoryDirectory)
|
|
79
|
+
* else -> legacy ~/.claude/projects/<workspace>/memory
|
|
80
|
+
* Returns { dir, autoMemoryEnabled }. Throws on an invalid cwd/agent; every path
|
|
81
|
+
* the callers derive from `dir` still goes through config.validatePath/validateWrite.
|
|
82
|
+
*/
|
|
83
|
+
async function resolveDir({ workspace, cwd, agent } = {}) {
|
|
84
|
+
const home = os.homedir();
|
|
85
|
+
const ws = validWorkspaceName(workspace) ? workspace : 'default';
|
|
86
|
+
if (typeof cwd !== 'string' || !cwd) {
|
|
87
|
+
if (agent) throw new Error('agent memory requires a cwd');
|
|
88
|
+
return { dir: workspaceDir(ws), autoMemoryEnabled: true };
|
|
89
|
+
}
|
|
90
|
+
const realCwd = config.validatePath(cwd);
|
|
91
|
+
const root = gitRepoRoot(realCwd, require('node:child_process').execFileSync) || realCwd;
|
|
92
|
+
const realRoot = config.validatePath(root);
|
|
93
|
+
// Project-scoped native dirs live under <root>/.claude; validateWrite only
|
|
94
|
+
// permits that subtree for registered roots.
|
|
95
|
+
config.addAllowedRoot(realRoot);
|
|
96
|
+
const settings = {
|
|
97
|
+
user: await readSettings(path.join(home, '.claude', 'settings.json')),
|
|
98
|
+
project: await readSettings(path.join(realRoot, '.claude', 'settings.json')),
|
|
99
|
+
local: await readSettings(path.join(realRoot, '.claude', 'settings.local.json')),
|
|
100
|
+
};
|
|
101
|
+
const enabled = autoMemoryEnabled(settings);
|
|
102
|
+
if (agent) {
|
|
103
|
+
return { dir: resolveAgentMemoryDir({ home, root: realRoot, name: agent.name, scope: agent.scope }), autoMemoryEnabled: enabled };
|
|
104
|
+
}
|
|
105
|
+
const r = resolveWorkspaceMemoryDir({ home, cwd: realCwd, settings, repoRoot: realRoot });
|
|
106
|
+
return { dir: r.dir, autoMemoryEnabled: enabled };
|
|
107
|
+
}
|
|
108
|
+
|
|
55
109
|
/**
|
|
56
110
|
* Reject NUL chars, '..' segments, absolute paths, or anything that would
|
|
57
111
|
* resolve outside the workspace dir. Returns the resolved absolute path.
|
|
58
112
|
* The downstream config.cjs helpers add a second layer of validation, but
|
|
59
113
|
* this lets us return a clean error message before we get there.
|
|
60
114
|
*/
|
|
61
|
-
function resolveEntryPath(
|
|
115
|
+
function resolveEntryPath(dir, name) {
|
|
62
116
|
if (typeof name !== 'string' || !name) {
|
|
63
117
|
throw new Error('invalid entry name');
|
|
64
118
|
}
|
|
@@ -66,7 +120,6 @@ function resolveEntryPath(workspace, name) {
|
|
|
66
120
|
if (!SLUG_RE.test(name)) {
|
|
67
121
|
throw new Error(`invalid slug (must match ${SLUG_RE.source}): ${name}`);
|
|
68
122
|
}
|
|
69
|
-
const dir = workspaceDir(workspace);
|
|
70
123
|
const full = path.resolve(dir, name);
|
|
71
124
|
// path.resolve normalizes; verify the result is still under dir to guard
|
|
72
125
|
// against bizarre name inputs.
|
|
@@ -84,12 +137,15 @@ function validWorkspaceName(name) {
|
|
|
84
137
|
return typeof name === 'string' && /^[a-zA-Z0-9-_]{1,256}$/.test(name);
|
|
85
138
|
}
|
|
86
139
|
|
|
87
|
-
async function list({ workspace }) {
|
|
140
|
+
async function list({ workspace, cwd, agent }) {
|
|
88
141
|
const ws = validWorkspaceName(workspace) ? workspace : 'default';
|
|
89
|
-
|
|
142
|
+
let resolved;
|
|
143
|
+
try { resolved = await resolveDir({ workspace: ws, cwd, agent }); }
|
|
144
|
+
catch (e) { return { entries: [], workspace: ws, dir: null, autoMemoryEnabled: true, error: e.message }; }
|
|
145
|
+
const { dir } = resolved;
|
|
90
146
|
const r = await config.listDir(dir, { filesOnly: true });
|
|
91
147
|
if (!r.ok) {
|
|
92
|
-
return { entries: [], workspace: ws, error: r.error };
|
|
148
|
+
return { entries: [], workspace: ws, dir, autoMemoryEnabled: resolved.autoMemoryEnabled, error: r.error };
|
|
93
149
|
}
|
|
94
150
|
const entries = r.entries
|
|
95
151
|
.filter((e) => e.name.endsWith('.md'))
|
|
@@ -100,12 +156,14 @@ async function list({ workspace }) {
|
|
|
100
156
|
bytes: e.size,
|
|
101
157
|
}))
|
|
102
158
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
103
|
-
return { entries, workspace: ws, error: null };
|
|
159
|
+
return { entries, workspace: ws, dir, autoMemoryEnabled: resolved.autoMemoryEnabled, error: null };
|
|
104
160
|
}
|
|
105
161
|
|
|
106
|
-
async function read({ workspace, name }) {
|
|
162
|
+
async function read({ workspace, name, cwd, agent }) {
|
|
107
163
|
const ws = validWorkspaceName(workspace) ? workspace : 'default';
|
|
108
|
-
|
|
164
|
+
let abs;
|
|
165
|
+
try { abs = resolveEntryPath((await resolveDir({ workspace: ws, cwd, agent })).dir, name); }
|
|
166
|
+
catch (e) { return { content: '', exists: false, mtimeMs: 0, bytes: 0, error: e.message }; }
|
|
109
167
|
const r = await config.readText(abs);
|
|
110
168
|
if (!r.exists) {
|
|
111
169
|
return { content: '', exists: false, mtimeMs: 0, bytes: 0, error: r.error };
|
|
@@ -119,7 +177,7 @@ async function read({ workspace, name }) {
|
|
|
119
177
|
};
|
|
120
178
|
}
|
|
121
179
|
|
|
122
|
-
async function write({ workspace, name, content }) {
|
|
180
|
+
async function write({ workspace, name, content, cwd, agent }) {
|
|
123
181
|
const ws = validWorkspaceName(workspace) ? workspace : 'default';
|
|
124
182
|
if (typeof content !== 'string') {
|
|
125
183
|
return { ok: false, error: 'content must be a string' };
|
|
@@ -128,7 +186,9 @@ async function write({ workspace, name, content }) {
|
|
|
128
186
|
if (bytes > MAX_FILE_BYTES) {
|
|
129
187
|
return { ok: false, error: `file exceeds 1 MiB cap (${bytes} bytes)` };
|
|
130
188
|
}
|
|
131
|
-
|
|
189
|
+
let abs;
|
|
190
|
+
try { abs = resolveEntryPath((await resolveDir({ workspace: ws, cwd, agent })).dir, name); }
|
|
191
|
+
catch (e) { return { ok: false, error: e.message }; }
|
|
132
192
|
// Enforce per-workspace entry cap on creates (writes to existing files are
|
|
133
193
|
// always allowed regardless of count). Cheap fs.stat check rather than a
|
|
134
194
|
// full directory enumeration where possible.
|
|
@@ -136,7 +196,7 @@ async function write({ workspace, name, content }) {
|
|
|
136
196
|
try { await fsp.access(abs, fs.constants.F_OK); }
|
|
137
197
|
catch { exists = false; }
|
|
138
198
|
if (!exists) {
|
|
139
|
-
const l = await list({ workspace: ws });
|
|
199
|
+
const l = await list({ workspace: ws, cwd, agent });
|
|
140
200
|
if (l.entries.length >= MAX_ENTRIES) {
|
|
141
201
|
return { ok: false, error: `workspace at ${MAX_ENTRIES}-entry cap` };
|
|
142
202
|
}
|
|
@@ -149,9 +209,22 @@ async function write({ workspace, name, content }) {
|
|
|
149
209
|
}
|
|
150
210
|
}
|
|
151
211
|
|
|
152
|
-
|
|
212
|
+
const INDEX_NAME = 'MEMORY.md';
|
|
213
|
+
|
|
214
|
+
/** Read <dir>/MEMORY.md; missing/unreadable -> null. */
|
|
215
|
+
async function readIndexText(dir) {
|
|
216
|
+
const r = await config.readText(path.join(dir, INDEX_NAME));
|
|
217
|
+
return r.exists ? r.text : null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function deleteEntry({ workspace, name, cwd, agent }) {
|
|
153
221
|
const ws = validWorkspaceName(workspace) ? workspace : 'default';
|
|
154
|
-
|
|
222
|
+
let abs;
|
|
223
|
+
let dir;
|
|
224
|
+
try {
|
|
225
|
+
dir = (await resolveDir({ workspace: ws, cwd, agent })).dir;
|
|
226
|
+
abs = resolveEntryPath(dir, name);
|
|
227
|
+
} catch (e) { return { ok: false, error: e.message }; }
|
|
155
228
|
// Route through config.cjs::validatePath (home-boundary pin, symlink-safe)
|
|
156
229
|
// plus validateWrite (write-prefix pin) — same contract every other
|
|
157
230
|
// mutating call in the codebase follows. Complexity: O(1) per call.
|
|
@@ -164,16 +237,30 @@ async function deleteEntry({ workspace, name }) {
|
|
|
164
237
|
}
|
|
165
238
|
try {
|
|
166
239
|
await fsp.unlink(real);
|
|
167
|
-
return { ok: true, error: null };
|
|
168
240
|
} catch (e) {
|
|
169
|
-
if (e.code
|
|
170
|
-
|
|
241
|
+
if (e.code !== 'ENOENT') return { ok: false, error: e.message };
|
|
242
|
+
}
|
|
243
|
+
if (name === INDEX_NAME) return { ok: true, error: null };
|
|
244
|
+
try {
|
|
245
|
+
const text = await readIndexText(dir);
|
|
246
|
+
if (text !== null) {
|
|
247
|
+
const next = removeIndexLines(text, name);
|
|
248
|
+
if (next !== text) await config.writeTextAtomic(path.join(dir, INDEX_NAME), next);
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
return { ok: true, error: null, indexError: e.message };
|
|
171
252
|
}
|
|
253
|
+
return { ok: true, error: null };
|
|
172
254
|
}
|
|
173
255
|
|
|
174
|
-
async function create({ workspace, name, description }) {
|
|
256
|
+
async function create({ workspace, name, description, cwd, agent }) {
|
|
175
257
|
const ws = validWorkspaceName(workspace) ? workspace : 'default';
|
|
176
|
-
|
|
258
|
+
let abs;
|
|
259
|
+
let dir;
|
|
260
|
+
try {
|
|
261
|
+
dir = (await resolveDir({ workspace: ws, cwd, agent })).dir;
|
|
262
|
+
abs = resolveEntryPath(dir, name);
|
|
263
|
+
} catch (e) { return { ok: false, error: e.message }; }
|
|
177
264
|
// Refuse to overwrite — create is strictly "new file".
|
|
178
265
|
try {
|
|
179
266
|
await fsp.access(abs, fs.constants.F_OK);
|
|
@@ -190,7 +277,16 @@ async function create({ workspace, name, description }) {
|
|
|
190
277
|
desc || `# ${stem}`,
|
|
191
278
|
'',
|
|
192
279
|
].join('\n');
|
|
193
|
-
|
|
280
|
+
const w = await write({ workspace: ws, name, content: fm, cwd, agent });
|
|
281
|
+
if (!w.ok || name === INDEX_NAME) return w;
|
|
282
|
+
try {
|
|
283
|
+
const text = (await readIndexText(dir)) || '';
|
|
284
|
+
const next = addIndexLine(text, { file: name, title: stem, hook: desc.replace(/\n/g, ' ') });
|
|
285
|
+
if (next !== text) await config.writeTextAtomic(path.join(dir, INDEX_NAME), next);
|
|
286
|
+
} catch (e) {
|
|
287
|
+
return { ...w, indexError: e.message };
|
|
288
|
+
}
|
|
289
|
+
return w;
|
|
194
290
|
}
|
|
195
291
|
|
|
196
292
|
/**
|
|
@@ -200,9 +296,11 @@ async function create({ workspace, name, description }) {
|
|
|
200
296
|
* the dead-ref existence check. Absent/invalid cwd disables dead-ref
|
|
201
297
|
* detection entirely rather than mass-flagging memories.
|
|
202
298
|
*/
|
|
203
|
-
async function stale({ workspace, cwd }) {
|
|
299
|
+
async function stale({ workspace, cwd, agent }) {
|
|
204
300
|
const ws = validWorkspaceName(workspace) ? workspace : 'default';
|
|
205
|
-
|
|
301
|
+
let dir;
|
|
302
|
+
try { dir = (await resolveDir({ workspace: ws, cwd, agent })).dir; }
|
|
303
|
+
catch (e) { return { entries: [], workspace: ws, error: e.message }; }
|
|
206
304
|
const r = await config.listDir(dir, { filesOnly: true });
|
|
207
305
|
if (!r.ok) {
|
|
208
306
|
return { entries: [], workspace: ws, error: r.error };
|
|
@@ -236,7 +334,30 @@ async function stale({ workspace, cwd }) {
|
|
|
236
334
|
entries.push({ name, mtimeMs: r2.mtimeMs, body: r2.text });
|
|
237
335
|
}
|
|
238
336
|
|
|
239
|
-
|
|
337
|
+
const index = indexHealth({ indexText: await readIndexText(dir), files: r.entries.map((e) => e.name).filter((n) => n.endsWith('.md')) });
|
|
338
|
+
return { entries: scoreMemories({ entries, now: Date.now(), existsPath }), workspace: ws, error: null, index };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Agents (user + project) with their `memory:` frontmatter scope. */
|
|
342
|
+
async function agents({ cwd } = {}) {
|
|
343
|
+
const home = os.homedir();
|
|
344
|
+
let root = null;
|
|
345
|
+
if (typeof cwd === 'string' && cwd) {
|
|
346
|
+
try {
|
|
347
|
+
const realCwd = config.validatePath(cwd);
|
|
348
|
+
root = config.validatePath(gitRepoRoot(realCwd, require('node:child_process').execFileSync) || realCwd);
|
|
349
|
+
} catch {
|
|
350
|
+
root = null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
agents: listMemoryAgents({
|
|
355
|
+
home,
|
|
356
|
+
root,
|
|
357
|
+
readDir: (d) => fs.readdirSync(d),
|
|
358
|
+
readText: (f) => fs.readFileSync(f, 'utf8'),
|
|
359
|
+
}),
|
|
360
|
+
};
|
|
240
361
|
}
|
|
241
362
|
|
|
242
363
|
function registerMemoryHandlers() {
|
|
@@ -247,6 +368,7 @@ function registerMemoryHandlers() {
|
|
|
247
368
|
ipcMain.handle('memory:delete', v(s.memoryDelete, deleteEntry));
|
|
248
369
|
ipcMain.handle('memory:create', v(s.memoryCreate, create));
|
|
249
370
|
ipcMain.handle('memory:stale', v(s.memoryStale, stale));
|
|
371
|
+
ipcMain.handle('memory:agents', v(s.memoryAgents, agents));
|
|
250
372
|
}
|
|
251
373
|
|
|
252
374
|
module.exports = {
|
|
@@ -255,5 +377,12 @@ module.exports = {
|
|
|
255
377
|
// exported for tests
|
|
256
378
|
memoryRoot,
|
|
257
379
|
workspaceDir,
|
|
380
|
+
resolveDir,
|
|
381
|
+
list,
|
|
382
|
+
read,
|
|
383
|
+
write,
|
|
384
|
+
create,
|
|
385
|
+
deleteEntry,
|
|
386
|
+
agents,
|
|
258
387
|
stale,
|
|
259
388
|
};
|
package/src/main/runVerify.cjs
CHANGED
|
@@ -41,6 +41,7 @@ const fs = require('node:fs');
|
|
|
41
41
|
const path = require('node:path');
|
|
42
42
|
const { execFileSync } = require('node:child_process');
|
|
43
43
|
const { parsePrdFile } = require('./lib/prd/prdFrontmatter.cjs');
|
|
44
|
+
const { isSatisfiedDepRow } = require('./lib/prd/depSlugResolve.cjs');
|
|
44
45
|
|
|
45
46
|
const VERDICTS_SCHEMA_VERSION = 1;
|
|
46
47
|
|
|
@@ -468,7 +469,9 @@ function checkDeps(queueEntry, allJobs, prdBody) {
|
|
|
468
469
|
// bare dep name that never matched any row, and ended up on disk with no
|
|
469
470
|
// queue row at all.
|
|
470
471
|
if (deps.length === 0) continue;
|
|
471
|
-
|
|
472
|
+
// Same predicate as findBlockingDep — an auto-resolve skip the picker
|
|
473
|
+
// dispatched past must not be re-judged unmet here (same infinite re-run).
|
|
474
|
+
const unfinished = deps.find((d) => !isSatisfiedDepRow(d));
|
|
472
475
|
if (unfinished) {
|
|
473
476
|
return { ok: false, reason: `dependsOn: "${depSlug}" is ${unfinished.status} (need completed)` };
|
|
474
477
|
}
|
|
@@ -488,7 +491,7 @@ function checkDeps(queueEntry, allJobs, prdBody) {
|
|
|
488
491
|
if (matches.length === 0) continue; // unresolvable fragment — skip
|
|
489
492
|
|
|
490
493
|
for (const dep of matches) {
|
|
491
|
-
if (dep
|
|
494
|
+
if (!isSatisfiedDepRow(dep)) {
|
|
492
495
|
return {
|
|
493
496
|
ok: false,
|
|
494
497
|
reason: `PRD body dep "${frag}" → "${dep.slug}" is ${dep.status} (need completed)`,
|