@drewpayment/mink 0.8.0 → 0.9.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/dashboard/out/404.html +1 -1
- package/dashboard/out/action-log.html +1 -1
- package/dashboard/out/action-log.txt +1 -1
- package/dashboard/out/activity.html +1 -1
- package/dashboard/out/activity.txt +1 -1
- package/dashboard/out/bugs.html +1 -1
- package/dashboard/out/bugs.txt +1 -1
- package/dashboard/out/capture.html +1 -1
- package/dashboard/out/capture.txt +1 -1
- package/dashboard/out/config.html +1 -1
- package/dashboard/out/config.txt +1 -1
- package/dashboard/out/daemon.html +1 -1
- package/dashboard/out/daemon.txt +1 -1
- package/dashboard/out/design.html +1 -1
- package/dashboard/out/design.txt +1 -1
- package/dashboard/out/discord.html +1 -1
- package/dashboard/out/discord.txt +1 -1
- package/dashboard/out/file-index.html +1 -1
- package/dashboard/out/file-index.txt +1 -1
- package/dashboard/out/index.html +1 -1
- package/dashboard/out/index.txt +1 -1
- package/dashboard/out/insights.html +1 -1
- package/dashboard/out/insights.txt +1 -1
- package/dashboard/out/learning.html +1 -1
- package/dashboard/out/learning.txt +1 -1
- package/dashboard/out/overview.html +1 -1
- package/dashboard/out/overview.txt +1 -1
- package/dashboard/out/scheduler.html +1 -1
- package/dashboard/out/scheduler.txt +1 -1
- package/dashboard/out/sync.html +1 -1
- package/dashboard/out/sync.txt +1 -1
- package/dashboard/out/tokens.html +1 -1
- package/dashboard/out/tokens.txt +1 -1
- package/dashboard/out/waste.html +1 -1
- package/dashboard/out/waste.txt +1 -1
- package/dashboard/out/wiki.html +1 -1
- package/dashboard/out/wiki.txt +1 -1
- package/dist/cli.js +2088 -1051
- package/package.json +1 -1
- package/src/commands/bug-search.ts +3 -3
- package/src/commands/detect-waste.ts +34 -25
- package/src/commands/post-read.ts +6 -3
- package/src/commands/post-write.ts +6 -3
- package/src/commands/pre-read.ts +14 -10
- package/src/commands/pre-write.ts +8 -5
- package/src/commands/reflect.ts +12 -7
- package/src/commands/session-start.ts +34 -3
- package/src/commands/session-stop.ts +10 -6
- package/src/commands/status.ts +29 -17
- package/src/commands/sync-migrate.ts +330 -0
- package/src/commands/sync.ts +75 -1
- package/src/core/conflict-park.ts +84 -0
- package/src/core/dashboard-api.ts +12 -31
- package/src/core/note-writer.ts +52 -6
- package/src/core/paths.ts +66 -10
- package/src/core/state-aggregator.ts +304 -0
- package/src/core/state-counters.ts +46 -0
- package/src/core/sync-merge-drivers.ts +247 -0
- package/src/core/sync.ts +150 -68
- package/src/core/token-ledger.ts +19 -3
- /package/dashboard/out/_next/static/{EC-_8nIOf1GnPrIqZ7Mk3 → FLxzihv7lbkF71kIxdNQT}/_buildManifest.js +0 -0
- /package/dashboard/out/_next/static/{EC-_8nIOf1GnPrIqZ7Mk3 → FLxzihv7lbkF71kIxdNQT}/_ssgManifest.js +0 -0
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { searchBugs } from "../core/bug-memory";
|
|
2
|
+
import { aggregateBugMemory } from "../core/state-aggregator";
|
|
3
3
|
|
|
4
4
|
export function bugSearch(cwd: string, query: string): void {
|
|
5
5
|
if (!query) {
|
|
@@ -7,7 +7,7 @@ export function bugSearch(cwd: string, query: string): void {
|
|
|
7
7
|
process.exit(1);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
const memory =
|
|
10
|
+
const memory = aggregateBugMemory(cwd);
|
|
11
11
|
const results = searchBugs(memory, query);
|
|
12
12
|
|
|
13
13
|
if (results.length === 0) {
|
|
@@ -1,36 +1,31 @@
|
|
|
1
1
|
import { statSync } from "fs";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
tokenLedgerShardPath,
|
|
4
4
|
fileIndexPath,
|
|
5
|
-
actionLogPath,
|
|
6
5
|
learningMemoryPath,
|
|
7
6
|
} from "../core/paths";
|
|
8
|
-
import {
|
|
7
|
+
import { loadLedger, saveLedger } from "../core/token-ledger";
|
|
9
8
|
import { isFileIndex, createEmptyIndex } from "../core/index-store";
|
|
10
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
aggregateTokenLedger,
|
|
11
|
+
aggregateActionLog,
|
|
12
|
+
} from "../core/state-aggregator";
|
|
13
|
+
import { loadCounters } from "../core/state-counters";
|
|
11
14
|
import { safeReadJson } from "../core/fs-utils";
|
|
12
15
|
import { runDetection } from "../core/waste-detection";
|
|
16
|
+
import { getOrCreateDeviceId } from "../core/device";
|
|
13
17
|
import type { TokenLedger } from "../types/token-ledger";
|
|
14
18
|
import type { FileIndex } from "../types/file-index";
|
|
15
19
|
|
|
16
20
|
export function detectWaste(cwd: string): void {
|
|
17
|
-
const ledgerPath = tokenLedgerPath(cwd);
|
|
18
21
|
const idxPath = fileIndexPath(cwd);
|
|
19
|
-
const logPath = actionLogPath(cwd);
|
|
20
22
|
const lmPath = learningMemoryPath(cwd);
|
|
21
23
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
ledger = createEmptyLedger();
|
|
28
|
-
} else if (!isTokenLedger(rawLedger)) {
|
|
29
|
-
console.warn("[mink] Warning: corrupt token ledger, skipping waste detection");
|
|
30
|
-
return;
|
|
31
|
-
} else {
|
|
32
|
-
ledger = rawLedger;
|
|
33
|
-
}
|
|
24
|
+
// Aggregated ledger (across all device shards + legacy). Aggregator returns
|
|
25
|
+
// an empty ledger when no sources exist, so the empty-vs-corrupt distinction
|
|
26
|
+
// collapses into "treat missing as empty" — corrupt files inside a shard are
|
|
27
|
+
// already logged by loadLedger.
|
|
28
|
+
const ledger: TokenLedger = aggregateTokenLedger(cwd);
|
|
34
29
|
|
|
35
30
|
// Load file index
|
|
36
31
|
const rawIndex = safeReadJson(idxPath);
|
|
@@ -41,8 +36,8 @@ export function detectWaste(cwd: string): void {
|
|
|
41
36
|
fileIndex = createEmptyIndex();
|
|
42
37
|
}
|
|
43
38
|
|
|
44
|
-
//
|
|
45
|
-
const actionLogContent =
|
|
39
|
+
// Aggregated action log content (across all device shards + legacy)
|
|
40
|
+
const actionLogContent = aggregateActionLog(cwd);
|
|
46
41
|
|
|
47
42
|
// Get learning memory mtime
|
|
48
43
|
let learningMemoryMtimeMs: number | null = null;
|
|
@@ -52,18 +47,32 @@ export function detectWaste(cwd: string): void {
|
|
|
52
47
|
// File missing — will be flagged as stale
|
|
53
48
|
}
|
|
54
49
|
|
|
55
|
-
//
|
|
50
|
+
// Pull hit/miss telemetry from the per-device counter file, falling back to
|
|
51
|
+
// the legacy header counters when unmigrated. We feed runDetection a synthetic
|
|
52
|
+
// header so it works without knowing about the split.
|
|
53
|
+
const counters = loadCounters(cwd);
|
|
54
|
+
const headerForDetection = {
|
|
55
|
+
...fileIndex.header,
|
|
56
|
+
lifetimeHits: counters.fileIndexHits || fileIndex.header.lifetimeHits,
|
|
57
|
+
lifetimeMisses: counters.fileIndexMisses || fileIndex.header.lifetimeMisses,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Run detection on the aggregated cross-device view
|
|
56
61
|
const flags = runDetection(
|
|
57
62
|
ledger,
|
|
58
63
|
fileIndex.entries,
|
|
59
|
-
|
|
64
|
+
headerForDetection,
|
|
60
65
|
actionLogContent,
|
|
61
66
|
learningMemoryMtimeMs
|
|
62
67
|
);
|
|
63
68
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
// Persist flags in THIS device's shard ledger so it's the only writer for
|
|
70
|
+
// that file. The aggregator unions wasteFlags across shards on read, so
|
|
71
|
+
// every device's view stays current without merge conflicts.
|
|
72
|
+
const shardLedgerPath = tokenLedgerShardPath(cwd, getOrCreateDeviceId());
|
|
73
|
+
const shardLedger = loadLedger(shardLedgerPath);
|
|
74
|
+
shardLedger.wasteFlags = flags;
|
|
75
|
+
saveLedger(shardLedgerPath, shardLedger);
|
|
67
76
|
|
|
68
77
|
// Output summary
|
|
69
78
|
if (flags.length === 0) {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { relative } from "path";
|
|
2
2
|
import { readStdinJson } from "../core/stdin";
|
|
3
|
-
import { sessionPath, fileIndexPath,
|
|
3
|
+
import { sessionPath, fileIndexPath, actionLogShardPath } from "../core/paths";
|
|
4
4
|
import { safeReadJson, atomicWriteJson } from "../core/fs-utils";
|
|
5
5
|
import { createSessionState, isSessionState, recordRead } from "../core/session";
|
|
6
6
|
import { isFileIndex, lookupEntry } from "../core/index-store";
|
|
7
7
|
import { estimateTokens, isBinaryFile } from "../core/token-estimate";
|
|
8
8
|
import { createActionLogWriter } from "../core/action-log";
|
|
9
|
+
import { getOrCreateDeviceId } from "../core/device";
|
|
9
10
|
import type { SessionState } from "../types/session";
|
|
10
11
|
import type { FileIndex } from "../types/file-index";
|
|
11
12
|
import type { PostToolUseInput } from "../types/hook-input";
|
|
@@ -100,9 +101,11 @@ export async function postRead(cwd: string): Promise<void> {
|
|
|
100
101
|
// Record the read in session state
|
|
101
102
|
recordRead(state, filePath, result.estimatedTokens, result.indexHit);
|
|
102
103
|
|
|
103
|
-
// Append read entry to action log
|
|
104
|
+
// Append read entry to this device's action log shard
|
|
104
105
|
try {
|
|
105
|
-
const logWriter = createActionLogWriter(
|
|
106
|
+
const logWriter = createActionLogWriter(
|
|
107
|
+
actionLogShardPath(cwd, getOrCreateDeviceId())
|
|
108
|
+
);
|
|
106
109
|
logWriter.appendReadEntry(
|
|
107
110
|
new Date().toISOString(),
|
|
108
111
|
filePath,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { relative } from "path";
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
3
|
import { readStdinJson } from "../core/stdin";
|
|
4
|
-
import { sessionPath, fileIndexPath,
|
|
4
|
+
import { sessionPath, fileIndexPath, actionLogShardPath } from "../core/paths";
|
|
5
|
+
import { getOrCreateDeviceId } from "../core/device";
|
|
5
6
|
import { safeReadJson, atomicWriteJson } from "../core/fs-utils";
|
|
6
7
|
import { createSessionState, isSessionState, recordWrite } from "../core/session";
|
|
7
8
|
import {
|
|
@@ -129,9 +130,11 @@ export async function postWrite(cwd: string): Promise<void> {
|
|
|
129
130
|
upsertEntry(index, result.indexEntry);
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
// 2. Action log entry
|
|
133
|
+
// 2. Action log entry — write to this device's shard
|
|
133
134
|
try {
|
|
134
|
-
const logWriter = createActionLogWriter(
|
|
135
|
+
const logWriter = createActionLogWriter(
|
|
136
|
+
actionLogShardPath(cwd, getOrCreateDeviceId())
|
|
137
|
+
);
|
|
135
138
|
logWriter.appendWriteEntry(
|
|
136
139
|
new Date().toISOString(),
|
|
137
140
|
filePath,
|
package/src/commands/pre-read.ts
CHANGED
|
@@ -3,12 +3,11 @@ import { readStdinJson } from "../core/stdin";
|
|
|
3
3
|
import { sessionPath, fileIndexPath } from "../core/paths";
|
|
4
4
|
import { safeReadJson, atomicWriteJson } from "../core/fs-utils";
|
|
5
5
|
import { createSessionState, isSessionState } from "../core/session";
|
|
6
|
+
import { isFileIndex, lookupEntry } from "../core/index-store";
|
|
6
7
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
recordMiss,
|
|
11
|
-
} from "../core/index-store";
|
|
8
|
+
incrementFileIndexHit,
|
|
9
|
+
incrementFileIndexMiss,
|
|
10
|
+
} from "../core/state-counters";
|
|
12
11
|
import type { SessionState } from "../types/session";
|
|
13
12
|
import type { FileIndex, FileIndexEntry } from "../types/file-index";
|
|
14
13
|
import type { PreToolUseInput } from "../types/hook-input";
|
|
@@ -40,17 +39,17 @@ export function analyzePreRead(
|
|
|
40
39
|
state.counters.repeatedReadWarnings++;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
// File index lookup
|
|
42
|
+
// File index lookup. Hit/miss telemetry is persisted by the caller via
|
|
43
|
+
// increment{Hit,Miss}, not by mutating the shared index — keeping the
|
|
44
|
+
// file-index.json content-addressed by filePath so a JSON union merge driver
|
|
45
|
+
// can resolve cross-device updates without conflict.
|
|
44
46
|
if (index) {
|
|
45
47
|
entry = lookupEntry(index, filePath);
|
|
46
48
|
if (entry) {
|
|
47
49
|
indexHit = true;
|
|
48
|
-
recordHit(index);
|
|
49
50
|
warnings.push(
|
|
50
51
|
`[mink] ${filePath} — ${entry.description} (~${entry.estimatedTokens} tokens)`
|
|
51
52
|
);
|
|
52
|
-
} else {
|
|
53
|
-
recordMiss(index);
|
|
54
53
|
}
|
|
55
54
|
}
|
|
56
55
|
|
|
@@ -99,7 +98,12 @@ export async function preRead(cwd: string): Promise<void> {
|
|
|
99
98
|
// Persist state changes
|
|
100
99
|
atomicWriteJson(sessionPath(cwd), state);
|
|
101
100
|
if (index) {
|
|
102
|
-
|
|
101
|
+
try {
|
|
102
|
+
if (result.indexHit) incrementFileIndexHit(cwd);
|
|
103
|
+
else incrementFileIndexMiss(cwd);
|
|
104
|
+
} catch {
|
|
105
|
+
// Counter file is best-effort telemetry — never block the read hook
|
|
106
|
+
}
|
|
103
107
|
}
|
|
104
108
|
} catch {
|
|
105
109
|
// Never crash — exit silently
|
|
@@ -5,6 +5,10 @@ import { sessionPath, learningMemoryPath, bugMemoryPath } from "../core/paths";
|
|
|
5
5
|
import { safeReadJson, atomicWriteJson } from "../core/fs-utils";
|
|
6
6
|
import { createSessionState, isSessionState } from "../core/session";
|
|
7
7
|
import { parseLearningMemory, getEntries } from "../core/learning-memory";
|
|
8
|
+
import {
|
|
9
|
+
aggregateLearningMemory,
|
|
10
|
+
aggregateBugMemory,
|
|
11
|
+
} from "../core/state-aggregator";
|
|
8
12
|
import { extractPatterns, matchPatterns } from "../core/pattern-engine";
|
|
9
13
|
import {
|
|
10
14
|
loadBugMemory,
|
|
@@ -88,20 +92,19 @@ export async function preWrite(cwd: string): Promise<void> {
|
|
|
88
92
|
const filePath = relative(cwd, absolutePath);
|
|
89
93
|
const writeContent = extractWriteContent(input);
|
|
90
94
|
|
|
91
|
-
// Load learning memory Do-Not-Repeat entries
|
|
95
|
+
// Load learning memory Do-Not-Repeat entries (canonical + sidecars)
|
|
92
96
|
let doNotRepeatEntries: string[] = [];
|
|
93
97
|
try {
|
|
94
|
-
const
|
|
95
|
-
const mem = parseLearningMemory(markdown);
|
|
98
|
+
const mem = aggregateLearningMemory(cwd);
|
|
96
99
|
doNotRepeatEntries = getEntries(mem, "Do-Not-Repeat");
|
|
97
100
|
} catch {
|
|
98
101
|
// Learning memory not found or corrupt — skip enforcement
|
|
99
102
|
}
|
|
100
103
|
|
|
101
|
-
// Load bug memory for this file
|
|
104
|
+
// Load bug memory for this file (aggregated across shards)
|
|
102
105
|
let bugMemory: BugMemory | undefined;
|
|
103
106
|
try {
|
|
104
|
-
bugMemory =
|
|
107
|
+
bugMemory = aggregateBugMemory(cwd);
|
|
105
108
|
} catch {
|
|
106
109
|
// Bug memory not found or corrupt — skip lookup
|
|
107
110
|
}
|
package/src/commands/reflect.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { existsSync
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
import { serializeLearningMemory, totalEntryCount } from "../core/learning-memory";
|
|
4
|
+
import { aggregateLearningMemoryAt } from "../core/state-aggregator";
|
|
3
5
|
import { reflectMemory } from "../core/reflection";
|
|
4
6
|
import { atomicWriteText, safeReadJson } from "../core/fs-utils";
|
|
5
7
|
import type { ReflectionResult } from "../types/learning-memory";
|
|
@@ -8,18 +10,21 @@ import type { ProjectConfig } from "../types/file-index";
|
|
|
8
10
|
const DEFAULT_TOKEN_BUDGET = 2000;
|
|
9
11
|
|
|
10
12
|
export function reflect(
|
|
11
|
-
|
|
13
|
+
_cwd: string,
|
|
12
14
|
memoryPath: string,
|
|
13
15
|
configPath: string
|
|
14
16
|
): ReflectionResult | null {
|
|
15
|
-
|
|
17
|
+
// Aggregate canonical + every device's sidecar that sits next to memoryPath.
|
|
18
|
+
// Using dirname(memoryPath) keeps callers in control of where the project
|
|
19
|
+
// state lives — production passes projectDir(cwd)/learning-memory.md, tests
|
|
20
|
+
// pass arbitrary temp directories.
|
|
21
|
+
const projDir = dirname(memoryPath);
|
|
22
|
+
const mem = aggregateLearningMemoryAt(projDir);
|
|
23
|
+
if (totalEntryCount(mem) === 0 && !existsSync(memoryPath)) {
|
|
16
24
|
console.log("[mink] no learning memory found");
|
|
17
25
|
return null;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
|
-
const markdown = readFileSync(memoryPath, "utf-8");
|
|
21
|
-
const mem = parseLearningMemory(markdown);
|
|
22
|
-
|
|
23
28
|
const config = safeReadJson(configPath) as ProjectConfig | null;
|
|
24
29
|
const tokenBudget = config?.learningMemoryTokenBudget ?? DEFAULT_TOKEN_BUDGET;
|
|
25
30
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { mkdirSync } from "fs";
|
|
2
2
|
import { createSessionState } from "../core/session";
|
|
3
|
-
import { projectDir, sessionPath,
|
|
3
|
+
import { projectDir, sessionPath, actionLogShardPath } from "../core/paths";
|
|
4
4
|
import { atomicWriteJson } from "../core/fs-utils";
|
|
5
5
|
import { createActionLogWriter } from "../core/action-log";
|
|
6
|
+
import { getOrCreateDeviceId } from "../core/device";
|
|
6
7
|
import { isWikiEnabled, isVaultInitialized, isInsideVault } from "../core/vault";
|
|
7
8
|
import { loadVaultIndex } from "../core/note-index";
|
|
8
9
|
|
|
@@ -23,6 +24,17 @@ export function sessionStart(cwd: string): void {
|
|
|
23
24
|
// Never crash hooks
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
// One-shot migration to sync layout v2. Idempotent re-run is a no-op.
|
|
28
|
+
try {
|
|
29
|
+
const { readSyncVersion, MINK_SYNC_VERSION } = require("../core/sync");
|
|
30
|
+
if (readSyncVersion() < MINK_SYNC_VERSION) {
|
|
31
|
+
const { migrateSyncLayout } = require("./sync-migrate");
|
|
32
|
+
migrateSyncLayout();
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// Migration is best-effort; never block session-start
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
// Sync pull before session begins (if enabled)
|
|
27
39
|
try {
|
|
28
40
|
const { isSyncInitialized, syncPull } = require("../core/sync");
|
|
@@ -39,9 +51,11 @@ export function sessionStart(cwd: string): void {
|
|
|
39
51
|
const state = createSessionState();
|
|
40
52
|
atomicWriteJson(sessionPath(cwd), state);
|
|
41
53
|
|
|
42
|
-
// Append session header to action log
|
|
54
|
+
// Append session header to this device's action log shard
|
|
43
55
|
try {
|
|
44
|
-
const logWriter = createActionLogWriter(
|
|
56
|
+
const logWriter = createActionLogWriter(
|
|
57
|
+
actionLogShardPath(cwd, getOrCreateDeviceId())
|
|
58
|
+
);
|
|
45
59
|
logWriter.appendSessionHeader(state.startTimestamp);
|
|
46
60
|
} catch {
|
|
47
61
|
// Never crash hooks
|
|
@@ -55,6 +69,23 @@ export function sessionStart(cwd: string): void {
|
|
|
55
69
|
(e) => e.category === "inbox"
|
|
56
70
|
).length;
|
|
57
71
|
|
|
72
|
+
// Regenerate the master index when missing — it's gitignored under sync
|
|
73
|
+
// v2 so freshly-cloned devices need it materialised before Obsidian can
|
|
74
|
+
// see the vault. updateMasterIndex is idempotent + cheap.
|
|
75
|
+
try {
|
|
76
|
+
const { join } = require("path");
|
|
77
|
+
const { existsSync } = require("fs");
|
|
78
|
+
const { resolveVaultPath } = require("../core/vault");
|
|
79
|
+
const { updateMasterIndex } = require("../core/note-linker");
|
|
80
|
+
const vaultPath = resolveVaultPath();
|
|
81
|
+
const masterIndexPath = join(vaultPath, "_index.md");
|
|
82
|
+
if (!existsSync(masterIndexPath)) {
|
|
83
|
+
updateMasterIndex(vaultPath);
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Never crash hooks on regeneration failure
|
|
87
|
+
}
|
|
88
|
+
|
|
58
89
|
if (inboxCount > 0) {
|
|
59
90
|
console.error(
|
|
60
91
|
`[mink] vault: ${inboxCount} notes in inbox need categorization`
|
|
@@ -4,8 +4,10 @@ import { safeReadJson, atomicWriteJson, atomicWriteText } from "../core/fs-utils
|
|
|
4
4
|
import { isSessionState, buildSummary } from "../core/session";
|
|
5
5
|
import { reflect } from "./reflect";
|
|
6
6
|
import { createLedgerFinalizer } from "../core/token-ledger";
|
|
7
|
-
import {
|
|
7
|
+
import { hasBugForFileInSession } from "../core/bug-memory";
|
|
8
|
+
import { aggregateBugMemoryAt } from "../core/state-aggregator";
|
|
8
9
|
import { createActionLogWriter, consolidateLog } from "../core/action-log";
|
|
10
|
+
import { getOrCreateDeviceId } from "../core/device";
|
|
9
11
|
import {
|
|
10
12
|
isWikiEnabled,
|
|
11
13
|
isVaultInitialized,
|
|
@@ -55,7 +57,8 @@ export function sessionStop(
|
|
|
55
57
|
state.stopCount++;
|
|
56
58
|
|
|
57
59
|
const projDir = dirname(sessionFile);
|
|
58
|
-
const
|
|
60
|
+
const deviceId = getOrCreateDeviceId();
|
|
61
|
+
const effectiveFinalizer = finalizer ?? createLedgerFinalizer(projDir, deviceId);
|
|
59
62
|
|
|
60
63
|
if (hasActivity(state)) {
|
|
61
64
|
const summary = buildSummary(state);
|
|
@@ -66,9 +69,11 @@ export function sessionStop(
|
|
|
66
69
|
effectiveFinalizer.updateSession(summary);
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
// Append session end to action log and run consolidation
|
|
72
|
+
// Append session end to action log and run consolidation. Both writes
|
|
73
|
+
// target THIS device's shard so concurrent sessions on other devices
|
|
74
|
+
// never collide on the action log file.
|
|
70
75
|
try {
|
|
71
|
-
const logPath = join(projDir, "action-log.md");
|
|
76
|
+
const logPath = join(projDir, "state", deviceId, "action-log.md");
|
|
72
77
|
const logWriter = createActionLogWriter(logPath);
|
|
73
78
|
logWriter.appendSessionEnd(summary);
|
|
74
79
|
|
|
@@ -84,8 +89,7 @@ export function sessionStop(
|
|
|
84
89
|
|
|
85
90
|
// Check for files edited 3+ times without a corresponding bug entry
|
|
86
91
|
const editCounts = getEditCounts(state);
|
|
87
|
-
const
|
|
88
|
-
const bugMemory = loadBugMemory(bugMemoryFile);
|
|
92
|
+
const bugMemory = aggregateBugMemoryAt(projDir);
|
|
89
93
|
|
|
90
94
|
for (const [filePath, count] of Object.entries(editCounts)) {
|
|
91
95
|
if (count >= 3) {
|
package/src/commands/status.ts
CHANGED
|
@@ -13,6 +13,12 @@ import { isFileIndex } from "../core/index-store";
|
|
|
13
13
|
import { loadLedger } from "../core/token-ledger";
|
|
14
14
|
import { parseLearningMemory, totalEntryCount } from "../core/learning-memory";
|
|
15
15
|
import { loadBugMemory } from "../core/bug-memory";
|
|
16
|
+
import {
|
|
17
|
+
aggregateTokenLedger,
|
|
18
|
+
aggregateBugMemory,
|
|
19
|
+
aggregateLearningMemory,
|
|
20
|
+
} from "../core/state-aggregator";
|
|
21
|
+
import { loadCounters } from "../core/state-counters";
|
|
16
22
|
import { getDaemonStatus } from "../core/daemon";
|
|
17
23
|
|
|
18
24
|
interface FileCheck {
|
|
@@ -72,12 +78,17 @@ export function status(cwd: string): void {
|
|
|
72
78
|
const raw = safeReadJson(fileIndexPath(cwd));
|
|
73
79
|
if (raw && isFileIndex(raw)) {
|
|
74
80
|
const h = raw.header;
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
// Hit/miss counters live in the per-device counter file, fall back to
|
|
82
|
+
// legacy header counters for unmigrated repos.
|
|
83
|
+
const counters = loadCounters(cwd);
|
|
84
|
+
const hits = counters.fileIndexHits || h.lifetimeHits;
|
|
85
|
+
const misses = counters.fileIndexMisses || h.lifetimeMisses;
|
|
86
|
+
const total = hits + misses;
|
|
87
|
+
const ratio = total > 0 ? ((hits / total) * 100).toFixed(1) : "N/A";
|
|
77
88
|
console.log(" File index:");
|
|
78
89
|
console.log(` Files: ${h.totalFiles}`);
|
|
79
90
|
console.log(` Last scan: ${h.lastScanTimestamp || "never"}`);
|
|
80
|
-
console.log(` Hit/miss ratio: ${ratio}${total > 0 ? "%" : ""} (${
|
|
91
|
+
console.log(` Hit/miss ratio: ${ratio}${total > 0 ? "%" : ""} (${hits} hits, ${misses} misses)`);
|
|
81
92
|
} else {
|
|
82
93
|
console.log(" File index: not available");
|
|
83
94
|
}
|
|
@@ -86,9 +97,9 @@ export function status(cwd: string): void {
|
|
|
86
97
|
}
|
|
87
98
|
console.log();
|
|
88
99
|
|
|
89
|
-
// Section 3: Token ledger
|
|
100
|
+
// Section 3: Token ledger (aggregated across all device shards + legacy)
|
|
90
101
|
try {
|
|
91
|
-
const ledger =
|
|
102
|
+
const ledger = aggregateTokenLedger(cwd);
|
|
92
103
|
const lt = ledger.lifetime;
|
|
93
104
|
console.log(" Token ledger:");
|
|
94
105
|
console.log(` Sessions: ${lt.totalSessions}`);
|
|
@@ -100,32 +111,33 @@ export function status(cwd: string): void {
|
|
|
100
111
|
}
|
|
101
112
|
console.log();
|
|
102
113
|
|
|
103
|
-
// Section 4: Learning memory
|
|
114
|
+
// Section 4: Learning memory (canonical + sidecars)
|
|
104
115
|
try {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const mtime = statSync(memPath).mtime;
|
|
116
|
+
const mem = aggregateLearningMemory(cwd);
|
|
117
|
+
const total = totalEntryCount(mem);
|
|
118
|
+
if (total === 0 && mem.projectName === "unknown") {
|
|
119
|
+
console.log(" Learning memory: not initialized");
|
|
120
|
+
} else {
|
|
111
121
|
console.log(" Learning memory:");
|
|
112
122
|
console.log(` User Preferences: ${mem.sections["User Preferences"].length}`);
|
|
113
123
|
console.log(` Key Learnings: ${mem.sections["Key Learnings"].length}`);
|
|
114
124
|
console.log(` Do-Not-Repeat: ${mem.sections["Do-Not-Repeat"].length}`);
|
|
115
125
|
console.log(` Decision Log: ${mem.sections["Decision Log"].length}`);
|
|
116
126
|
console.log(` Total entries: ${total}`);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
const memPath = learningMemoryPath(cwd);
|
|
128
|
+
if (existsSync(memPath)) {
|
|
129
|
+
const mtime = statSync(memPath).mtime;
|
|
130
|
+
console.log(` Canonical last modified: ${mtime.toISOString()}`);
|
|
131
|
+
}
|
|
120
132
|
}
|
|
121
133
|
} catch {
|
|
122
134
|
console.log(" Learning memory: error reading");
|
|
123
135
|
}
|
|
124
136
|
console.log();
|
|
125
137
|
|
|
126
|
-
// Section 5: Bug log
|
|
138
|
+
// Section 5: Bug log (aggregated across shards)
|
|
127
139
|
try {
|
|
128
|
-
const bugs =
|
|
140
|
+
const bugs = aggregateBugMemory(cwd);
|
|
129
141
|
console.log(` Bug log: ${bugs.entries.length} entries`);
|
|
130
142
|
} catch {
|
|
131
143
|
console.log(" Bug log: error reading");
|