@davesheffer/hunch 1.32.3 → 1.32.5
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/README.md +1 -1
- package/dist/cli/index.js +40 -7
- package/dist/cli/taskReport.js +80 -1
- package/dist/constitution/behaviorEvaluator.js +1 -1
- package/dist/constitution/repository.d.ts +1 -0
- package/dist/constitution/repository.js +68 -42
- package/dist/core/agenthook.d.ts +1 -1
- package/dist/core/agenthook.js +30 -6
- package/dist/core/checkreport.js +1 -1
- package/dist/core/config.d.ts +3 -0
- package/dist/core/config.js +4 -1
- package/dist/core/events.js +19 -3
- package/dist/core/io.js +30 -19
- package/dist/core/jsonc.js +13 -3
- package/dist/core/storeArtifact.d.ts +7 -0
- package/dist/core/storeArtifact.js +62 -0
- package/dist/core/taskReport.d.ts +49 -0
- package/dist/core/taskReport.js +99 -0
- package/dist/core/taskReportHook.d.ts +7 -1
- package/dist/core/taskReportHook.js +18 -5
- package/dist/integrations/claudeConfig.js +20 -3
- package/dist/integrations/claudemd.js +1 -1
- package/dist/integrations/health.d.ts +19 -5
- package/dist/integrations/health.js +36 -11
- package/dist/integrations/providers.d.ts +7 -0
- package/dist/integrations/providers.js +85 -16
- package/dist/integrations/registry.d.ts +15 -0
- package/dist/integrations/registry.js +41 -0
- package/dist/integrations/scaffold.js +17 -3
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.js +350 -324
- package/dist/mcp/toolset.d.ts +30 -0
- package/dist/mcp/toolset.js +72 -0
- package/dist/serve/app.js +9 -6
- package/dist/serve/writelock.js +8 -2
- package/dist/store/changeLedger.js +7 -6
- package/dist/store/jsonStore.d.ts +3 -3
- package/dist/store/jsonStore.js +65 -14
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const MCP_TOOL_GROUPS: {
|
|
2
|
+
/** nuryel.state/1 — the state-partition contract (Sofia-style agents). */
|
|
3
|
+
readonly nuryel: readonly ["nuryel_capabilities", "nuryel_read", "nuryel_write", "nuryel_capture", "nuryel_capture_batch", "nuryel_subscribe", "nuryel_records"];
|
|
4
|
+
/** Constitution G2/G3 experiment-track tools; the CLI remains the primary surface. */
|
|
5
|
+
readonly "constitution-experiments": readonly ["hunch_constitution_g2_readiness", "hunch_constitution_g3_readiness", "hunch_constitution_g2_shadow_queue", "hunch_constitution_g2_operational_drill", "hunch_constitution_g2_candidates", "hunch_constitution_g2_behavior_candidates", "hunch_constitution_g2_behavior_replay", "hunch_constitution_g2_behavior_materialization", "hunch_constitution_g2_behavior_policy_materialize"];
|
|
6
|
+
};
|
|
7
|
+
export type McpToolGroup = keyof typeof MCP_TOOL_GROUPS;
|
|
8
|
+
export declare const MCP_TOOL_GROUP_NAMES: McpToolGroup[];
|
|
9
|
+
export interface McpToolset {
|
|
10
|
+
enabled: (group: McpToolGroup) => boolean;
|
|
11
|
+
groups: McpToolGroup[];
|
|
12
|
+
hidden: string[];
|
|
13
|
+
/** Where the selection came from, for the startup log and doctor. */
|
|
14
|
+
source: "env" | "config" | "default";
|
|
15
|
+
}
|
|
16
|
+
/** Grammar shared by the env var and the config value: `all`, `core`, or a
|
|
17
|
+
* comma-separated list of extra groups on top of core (`core,nuryel`). Unknown
|
|
18
|
+
* words are ignored rather than failing the server. */
|
|
19
|
+
export declare function parseToolsetSpec(spec: string): McpToolGroup[] | null;
|
|
20
|
+
/** A root that already stores nuryel state records is a state partition and
|
|
21
|
+
* needs the nuryel tools; every other root gets the everyday set by default. */
|
|
22
|
+
export declare function rootStoresState(root: string): boolean;
|
|
23
|
+
/** `pinned` is `hunch mcp --root <dir>`: a server dedicated to one root, which is
|
|
24
|
+
* how state partitions are served — including a brand-new partition that has no
|
|
25
|
+
* state records yet and could never receive its first nuryel_write otherwise. */
|
|
26
|
+
export declare function resolveMcpToolset(root: string, opts?: {
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
configSpec?: string | null;
|
|
29
|
+
pinned?: boolean;
|
|
30
|
+
}): McpToolset;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Which MCP tool groups a server exposes. Every host shares one server, so the
|
|
2
|
+
* selection surface is the same for all of them: 57 tools with ~24 KB of
|
|
3
|
+
* descriptions dilute tool choice for everyday grounding. The everyday set is
|
|
4
|
+
* the default; the two specialist groups are enabled by evidence (a root that
|
|
5
|
+
* stores nuryel state records), by `.hunch/config.json` `mcp_tools`, or by the
|
|
6
|
+
* `HUNCH_MCP_TOOLS` environment variable. Hidden tools are not registered at
|
|
7
|
+
* all, so a client never sees them in tools/list. */
|
|
8
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { STATE_KINDS } from "../core/stateDelivery.js";
|
|
11
|
+
export const MCP_TOOL_GROUPS = {
|
|
12
|
+
/** nuryel.state/1 — the state-partition contract (Sofia-style agents). */
|
|
13
|
+
nuryel: ["nuryel_capabilities", "nuryel_read", "nuryel_write", "nuryel_capture", "nuryel_capture_batch", "nuryel_subscribe", "nuryel_records"],
|
|
14
|
+
/** Constitution G2/G3 experiment-track tools; the CLI remains the primary surface. */
|
|
15
|
+
"constitution-experiments": [
|
|
16
|
+
"hunch_constitution_g2_readiness", "hunch_constitution_g3_readiness", "hunch_constitution_g2_shadow_queue",
|
|
17
|
+
"hunch_constitution_g2_operational_drill", "hunch_constitution_g2_candidates", "hunch_constitution_g2_behavior_candidates",
|
|
18
|
+
"hunch_constitution_g2_behavior_replay", "hunch_constitution_g2_behavior_materialization", "hunch_constitution_g2_behavior_policy_materialize",
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
export const MCP_TOOL_GROUP_NAMES = Object.keys(MCP_TOOL_GROUPS);
|
|
22
|
+
/** Grammar shared by the env var and the config value: `all`, `core`, or a
|
|
23
|
+
* comma-separated list of extra groups on top of core (`core,nuryel`). Unknown
|
|
24
|
+
* words are ignored rather than failing the server. */
|
|
25
|
+
export function parseToolsetSpec(spec) {
|
|
26
|
+
const words = spec.split(",").map(w => w.trim().toLowerCase()).filter(Boolean);
|
|
27
|
+
if (!words.length)
|
|
28
|
+
return null;
|
|
29
|
+
if (words.includes("all"))
|
|
30
|
+
return [...MCP_TOOL_GROUP_NAMES];
|
|
31
|
+
return MCP_TOOL_GROUP_NAMES.filter(g => words.includes(g));
|
|
32
|
+
}
|
|
33
|
+
/** A root that already stores nuryel state records is a state partition and
|
|
34
|
+
* needs the nuryel tools; every other root gets the everyday set by default. */
|
|
35
|
+
export function rootStoresState(root) {
|
|
36
|
+
return STATE_KINDS.some(kind => {
|
|
37
|
+
const dir = join(root, ".hunch", kind);
|
|
38
|
+
try {
|
|
39
|
+
return existsSync(dir) && readdirSync(dir).some(f => f.endsWith(".json"));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** `pinned` is `hunch mcp --root <dir>`: a server dedicated to one root, which is
|
|
47
|
+
* how state partitions are served — including a brand-new partition that has no
|
|
48
|
+
* state records yet and could never receive its first nuryel_write otherwise. */
|
|
49
|
+
export function resolveMcpToolset(root, opts = {}) {
|
|
50
|
+
const env = opts.env ?? process.env;
|
|
51
|
+
let groups = null;
|
|
52
|
+
let source = "default";
|
|
53
|
+
const fromEnv = env.HUNCH_MCP_TOOLS?.trim();
|
|
54
|
+
if (fromEnv) {
|
|
55
|
+
groups = parseToolsetSpec(fromEnv);
|
|
56
|
+
if (groups)
|
|
57
|
+
source = "env";
|
|
58
|
+
}
|
|
59
|
+
if (!groups && opts.configSpec?.trim()) {
|
|
60
|
+
groups = parseToolsetSpec(opts.configSpec);
|
|
61
|
+
if (groups)
|
|
62
|
+
source = "config";
|
|
63
|
+
}
|
|
64
|
+
if (!groups) {
|
|
65
|
+
groups = opts.pinned || rootStoresState(root) ? ["nuryel"] : [];
|
|
66
|
+
source = "default";
|
|
67
|
+
}
|
|
68
|
+
const set = new Set(groups);
|
|
69
|
+
const hidden = MCP_TOOL_GROUP_NAMES.filter(g => !set.has(g)).flatMap(g => [...MCP_TOOL_GROUPS[g]]);
|
|
70
|
+
return { enabled: g => set.has(g), groups: [...set], hidden, source };
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=toolset.js.map
|
package/dist/serve/app.js
CHANGED
|
@@ -19,7 +19,7 @@ import { createServer } from "node:http";
|
|
|
19
19
|
import { HunchStore } from "../store/hunchStore.js";
|
|
20
20
|
import { hunchPaths } from "../core/paths.js";
|
|
21
21
|
import { flushCapture } from "../integrations/sync.js";
|
|
22
|
-
import { StateRefusal, capabilities, mergeReadResponses, readState, recordsState, subscribeState, writeState } from "../store/stateBinding.js";
|
|
22
|
+
import { StateRefusal, capabilities, mergeReadResponses, readState, recordsState, stateHomeFor, subscribeState, writeState } from "../store/stateBinding.js";
|
|
23
23
|
import { STATE_READ_VERSION, STATE_RECORDS_VERSION, STATE_SUBSCRIBE_VERSION, STATE_WRITE_VERSION, ReadScopesSchema, ScopeSchema, scopePath } from "../core/stateContract.js";
|
|
24
24
|
import { partitionFor, resolvePrincipal } from "./config.js";
|
|
25
25
|
import { WriteLockTimeout, withWriteLock } from "./writelock.js";
|
|
@@ -177,23 +177,26 @@ export function createServeApp(config, opts = {}) {
|
|
|
177
177
|
if (url.pathname === "/nuryel/v1/write") {
|
|
178
178
|
const scope = requireScope(principal, body);
|
|
179
179
|
const { store, root } = storeFor(scope);
|
|
180
|
-
const
|
|
180
|
+
const { hunchDir } = stateHomeFor(store, scope);
|
|
181
|
+
const result = await withWriteLock(hunchDir, () => writeState(store, { schema: STATE_WRITE_VERSION, principal, ...body }, {
|
|
181
182
|
flush: (isPrivate, message) => flushCapture(store, hunchPaths(root).hunch, isPrivate, message),
|
|
182
183
|
}));
|
|
183
184
|
return send(res, result.outcome === "created" ? 201 : 200, result);
|
|
184
185
|
}
|
|
185
186
|
if (url.pathname === "/nuryel/v1/capture") {
|
|
186
187
|
const scope = requireScope(principal, body);
|
|
187
|
-
const { store } = storeFor(scope);
|
|
188
|
-
const
|
|
189
|
-
|
|
188
|
+
const { store, root } = storeFor(scope);
|
|
189
|
+
const { hunchDir } = stateHomeFor(store, scope);
|
|
190
|
+
const result = await withWriteLock(hunchDir, () => captureState(store, { schema: STATE_CAPTURE_VERSION, principal, ...body }, {
|
|
191
|
+
flush: (isPrivate, message) => flushCapture(store, hunchPaths(root).hunch, isPrivate, message),
|
|
190
192
|
}));
|
|
191
193
|
return send(res, result.outcome === "created" ? 201 : 200, result);
|
|
192
194
|
}
|
|
193
195
|
if (url.pathname === "/nuryel/v1/capture-batch") {
|
|
194
196
|
const scope = requireScope(principal, body);
|
|
195
197
|
const { store, root } = storeFor(scope);
|
|
196
|
-
const
|
|
198
|
+
const { hunchDir } = stateHomeFor(store, scope);
|
|
199
|
+
const result = await withWriteLock(hunchDir, () => captureBatchState(store, { schema: STATE_CAPTURE_BATCH_VERSION, principal, ...body }, {
|
|
197
200
|
flush: (isPrivate, message) => flushCapture(store, hunchPaths(root).hunch, isPrivate, message),
|
|
198
201
|
}));
|
|
199
202
|
return send(res, 200, result);
|
package/dist/serve/writelock.js
CHANGED
|
@@ -65,8 +65,14 @@ function stealable(path, owner, now) {
|
|
|
65
65
|
catch {
|
|
66
66
|
return false;
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// A same-host live PID is authoritative even when a long-running write has
|
|
69
|
+
// exceeded the stale-age heuristic. Age alone cannot distinguish a slow
|
|
70
|
+
// writer from a dead one; stealing here would let two writers interleave
|
|
71
|
+
// their record and ledger updates. The age fallback is only safe when the
|
|
72
|
+
// owner is from another host (whose PID we cannot probe) or its metadata is
|
|
73
|
+
// unreadable.
|
|
74
|
+
if (owner && owner.host === hostname())
|
|
75
|
+
return !pidAlive(owner.pid);
|
|
70
76
|
return ageMs > STALE_AFTER_MS;
|
|
71
77
|
}
|
|
72
78
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
* sequence to reconcile. Merging two clones' ledgers for the same scope is not decided
|
|
10
10
|
* here (see docs/nuryel-state-contract.md, "Not decided here").
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import { join, resolve } from "node:path";
|
|
12
|
+
import { mkdirSync } from "node:fs";
|
|
13
|
+
import { basename, join, resolve } from "node:path";
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
15
15
|
import { z } from "zod";
|
|
16
16
|
import { writeFileAtomic } from "../core/io.js";
|
|
17
|
+
import { readStoreArtifact, storeArtifactPath } from "../core/storeArtifact.js";
|
|
17
18
|
import { ChangeEventSchema, ScopeSchema, scopePath } from "../core/stateContract.js";
|
|
18
19
|
export const LEDGER_SCHEMA_VERSION = "nuryel.ledger/1";
|
|
19
20
|
export const CHANGES_DIR = "changes";
|
|
@@ -60,11 +61,11 @@ export function emptyLedger(scope) {
|
|
|
60
61
|
* error (never silently treated as empty — that would restart the sequence). */
|
|
61
62
|
export function readLedger(hunchDir, scope) {
|
|
62
63
|
const file = resolve(ledgerFile(hunchDir, scope));
|
|
63
|
-
|
|
64
|
+
const text = readStoreArtifact(hunchDir, [CHANGES_DIR, basename(file)]);
|
|
65
|
+
if (text === null) {
|
|
64
66
|
validatedSnapshots.delete(file);
|
|
65
67
|
return emptyLedger(scope);
|
|
66
68
|
}
|
|
67
|
-
const text = readFileSync(file, "utf8");
|
|
68
69
|
const cached = validatedSnapshots.get(file);
|
|
69
70
|
if (cached?.text === text && cached.scope === scopePath(scope)) {
|
|
70
71
|
validatedSnapshots.delete(file);
|
|
@@ -96,8 +97,8 @@ export function writeLedger(hunchDir, ledger) {
|
|
|
96
97
|
writeValidatedLedger(hunchDir, LedgerSchema.parse(ledger));
|
|
97
98
|
}
|
|
98
99
|
function writeValidatedLedger(hunchDir, ledger) {
|
|
99
|
-
const file = ledgerFile(hunchDir, ledger.scope);
|
|
100
|
-
mkdirSync(
|
|
100
|
+
const file = storeArtifactPath(hunchDir, CHANGES_DIR, basename(ledgerFile(hunchDir, ledger.scope)));
|
|
101
|
+
mkdirSync(storeArtifactPath(hunchDir, CHANGES_DIR), { recursive: true });
|
|
101
102
|
writeFileAtomic(file, JSON.stringify(ledger, null, 2) + "\n");
|
|
102
103
|
}
|
|
103
104
|
/** Append events (in order) and remember an idempotency key in ONE atomic write, so a
|
|
@@ -79,9 +79,9 @@ export declare class JsonStore {
|
|
|
79
79
|
* two unsynchronized RMWs over index.json each read the same base array and the
|
|
80
80
|
* second rename silently erases the first's record. `mkdirSync` is the atomic
|
|
81
81
|
* acquire (EEXIST = held). A stale lock (killed process) is taken over by age;
|
|
82
|
-
* against a live contender we wait briefly and then
|
|
83
|
-
*
|
|
84
|
-
*
|
|
82
|
+
* against a live contender we wait briefly and then refuse the write. Proceeding
|
|
83
|
+
* without the lock would reintroduce the record-loss race this mutex exists to
|
|
84
|
+
* prevent. */
|
|
85
85
|
private withSingleFileLock;
|
|
86
86
|
/** Write a single record (validated) to its JSON file / into the index array. */
|
|
87
87
|
put<K extends EntityKind>(kind: K, record: EntityFor[K]): EntityFor[K];
|
package/dist/store/jsonStore.js
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* authoritative read/write surface; SQLite is rebuilt from it.
|
|
5
5
|
*/
|
|
6
6
|
import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, opendirSync, readSync, realpathSync, rmSync, } from "node:fs";
|
|
7
|
+
import { hostname } from "node:os";
|
|
7
8
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
9
|
import { ENTITY_KINDS, SCHEMAS } from "../core/types.js";
|
|
9
10
|
import { BASELINE_VERSION, migrateRaw, SCHEMA_VERSION } from "../core/migrate.js";
|
|
10
11
|
import { writeFileAtomic } from "../core/io.js";
|
|
12
|
+
import { readStoreArtifact } from "../core/storeArtifact.js";
|
|
11
13
|
/** High-cardinality collections (symbols, edges) are stored as a single
|
|
12
14
|
* index.json array — there can be thousands, and one file per edge would create
|
|
13
15
|
* enormous git noise. Curated, low-volume entities (components, decisions, bugs,
|
|
@@ -35,6 +37,29 @@ export const MAX_JSON_RECORD_BYTES = 8 * 1024 * 1024;
|
|
|
35
37
|
export const MAX_JSON_INDEX_BYTES = 256 * 1024 * 1024;
|
|
36
38
|
export const MAX_JSON_MANIFEST_BYTES = 64 * 1024;
|
|
37
39
|
export const MAX_JSON_DIRECTORY_ENTRIES_PER_KIND = 100_000;
|
|
40
|
+
function readRmwOwner(lock) {
|
|
41
|
+
const text = readStoreArtifact(lock, ["owner.tmp.json"], 4096);
|
|
42
|
+
if (text === null)
|
|
43
|
+
return undefined;
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(text);
|
|
46
|
+
if (typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid < 1 || typeof parsed.host !== "string")
|
|
47
|
+
return undefined;
|
|
48
|
+
return { pid: parsed.pid, host: parsed.host };
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function rmwPidAlive(pid) {
|
|
55
|
+
try {
|
|
56
|
+
process.kill(pid, 0);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return error.code === "EPERM";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
38
63
|
function missing(error) {
|
|
39
64
|
return error.code === "ENOENT";
|
|
40
65
|
}
|
|
@@ -431,30 +456,51 @@ export class JsonStore {
|
|
|
431
456
|
* two unsynchronized RMWs over index.json each read the same base array and the
|
|
432
457
|
* second rename silently erases the first's record. `mkdirSync` is the atomic
|
|
433
458
|
* acquire (EEXIST = held). A stale lock (killed process) is taken over by age;
|
|
434
|
-
* against a live contender we wait briefly and then
|
|
435
|
-
*
|
|
436
|
-
*
|
|
459
|
+
* against a live contender we wait briefly and then refuse the write. Proceeding
|
|
460
|
+
* without the lock would reintroduce the record-loss race this mutex exists to
|
|
461
|
+
* prevent. */
|
|
437
462
|
withSingleFileLock(kind, directory, fn) {
|
|
438
463
|
const lock = join(directory.lexical, ".rmw-lock");
|
|
439
464
|
const deadline = Date.now() + 2_000;
|
|
440
465
|
for (;;) {
|
|
466
|
+
if (Date.now() >= deadline)
|
|
467
|
+
throw new Error(`[hunch] timed out acquiring the ${kind} index lock (still held: ${lock})`);
|
|
441
468
|
try {
|
|
442
469
|
mkdirSync(lock);
|
|
470
|
+
// Record ownership inside the already-exclusive directory. A live local
|
|
471
|
+
// writer may exceed the stale-age heuristic while serializing a large
|
|
472
|
+
// index; its PID must prevent a second writer from taking over.
|
|
473
|
+
try {
|
|
474
|
+
writeFileAtomic(join(lock, "owner.tmp.json"), JSON.stringify({ pid: process.pid, host: hostname() }));
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
try {
|
|
478
|
+
rmSync(lock, { recursive: true, force: true });
|
|
479
|
+
}
|
|
480
|
+
catch { /* report the ownership failure below */ }
|
|
481
|
+
throw new Error(`[hunch] could not record ownership for the ${kind} index lock: ${error.message}`, { cause: error });
|
|
482
|
+
}
|
|
443
483
|
break;
|
|
444
484
|
}
|
|
445
|
-
catch {
|
|
485
|
+
catch (error) {
|
|
486
|
+
if (error.code !== "EEXIST")
|
|
487
|
+
throw error;
|
|
488
|
+
let stat;
|
|
446
489
|
try {
|
|
447
|
-
|
|
448
|
-
rmSync(lock, { recursive: true, force: true }); // no live spawn holds a lock this old
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
490
|
+
stat = lstatSync(lock);
|
|
451
491
|
}
|
|
452
|
-
catch {
|
|
453
|
-
|
|
492
|
+
catch (statError) {
|
|
493
|
+
if (statError.code === "ENOENT")
|
|
494
|
+
continue; // vanished between mkdir and inspect
|
|
495
|
+
throw statError;
|
|
454
496
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
497
|
+
const owner = readRmwOwner(lock);
|
|
498
|
+
const stale = owner && owner.host === hostname()
|
|
499
|
+
? !rmwPidAlive(owner.pid)
|
|
500
|
+
: Date.now() - stat.mtimeMs > 10_000;
|
|
501
|
+
if (stale) {
|
|
502
|
+
rmSync(lock, { recursive: true, force: true });
|
|
503
|
+
continue;
|
|
458
504
|
}
|
|
459
505
|
Atomics.wait(RMW_LOCK_WAITER, 0, 0, 25);
|
|
460
506
|
}
|
|
@@ -512,7 +558,12 @@ export class JsonStore {
|
|
|
512
558
|
// Sorted by id so the index has ONE canonical order — re-indexing after a
|
|
513
559
|
// git merge (which the driver also id-sorts) doesn't churn the whole file.
|
|
514
560
|
validated.sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
515
|
-
|
|
561
|
+
// A rebuild is also a read-modify-write boundary from the perspective of
|
|
562
|
+
// concurrent put/delete callers: without the same mutex it can publish
|
|
563
|
+
// over an update that acquired the lock moments earlier (or vice versa).
|
|
564
|
+
this.withSingleFileLock(kind, directory, () => {
|
|
565
|
+
this.writeContainedFile(directory, this.fileFor(kind, "index"), encode(validated), this.maxBytes(kind));
|
|
566
|
+
});
|
|
516
567
|
return;
|
|
517
568
|
}
|
|
518
569
|
// One file per record: preflight EVERY existing JSON file before touching
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://www.hunchmemory.com",
|
|
10
|
-
"version": "1.32.
|
|
10
|
+
"version": "1.32.5",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@davesheffer/hunch",
|
|
16
|
-
"version": "1.32.
|
|
16
|
+
"version": "1.32.5",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|