@davesheffer/hunch 1.45.1 → 1.45.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +41 -16
- package/dist/cli/serve.d.ts +2 -0
- package/dist/cli/serve.js +2 -1
- package/dist/core/agenthook.d.ts +1 -0
- package/dist/core/agenthook.js +56 -20
- package/dist/core/delivery.d.ts +5 -0
- package/dist/core/delivery.js +46 -4
- package/dist/core/pipeline.d.ts +9 -2
- package/dist/core/pipeline.js +13 -3
- package/dist/core/procstart.d.ts +17 -3
- package/dist/core/procstart.js +26 -5
- package/dist/core/taskSelection.d.ts +1 -0
- package/dist/core/taskSelection.js +90 -11
- package/dist/core/whyRecord.d.ts +47 -0
- package/dist/core/whyRecord.js +105 -0
- package/dist/extractors/git.d.ts +32 -0
- package/dist/extractors/git.js +177 -66
- package/dist/integrations/team.js +2 -2
- package/dist/integrations/uninstall.d.ts +7 -0
- package/dist/integrations/uninstall.js +19 -1
- package/dist/mcp/server.js +136 -26
- package/dist/serve/writelock.d.ts +3 -4
- package/dist/serve/writelock.js +9 -18
- package/dist/store/hunchStore.d.ts +10 -0
- package/dist/store/hunchStore.js +80 -1
- package/dist/store/jsonStore.d.ts +11 -1
- package/dist/store/jsonStore.js +38 -13
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/store/jsonStore.js
CHANGED
|
@@ -11,7 +11,7 @@ import { ENTITY_KINDS, SCHEMAS } from "../core/types.js";
|
|
|
11
11
|
import { BASELINE_VERSION, migrateRaw, SCHEMA_VERSION } from "../core/migrate.js";
|
|
12
12
|
import { writeFileAtomic } from "../core/io.js";
|
|
13
13
|
import { readStoreArtifact } from "../core/storeArtifact.js";
|
|
14
|
-
import { heldLockNonces, judgeSameHostOwner, selfStartToken, startTokenOf } from "../core/procstart.js";
|
|
14
|
+
import { createdAfterLock, heldLockNonces, judgeSameHostOwner, processStartedAt, selfStartToken, startTokenOf } from "../core/procstart.js";
|
|
15
15
|
/** High-cardinality collections (symbols, edges) are stored as a single
|
|
16
16
|
* index.json array — there can be thousands, and one file per edge would create
|
|
17
17
|
* enormous git noise. Curated, low-volume entities (components, decisions, bugs,
|
|
@@ -118,6 +118,7 @@ function readRmwOwnerOnce(lock) {
|
|
|
118
118
|
host: parsed.host,
|
|
119
119
|
...(typeof parsed.nonce === "string" ? { nonce: parsed.nonce } : {}),
|
|
120
120
|
...(typeof parsed.start === "string" ? { start: parsed.start } : {}),
|
|
121
|
+
...(typeof parsed.at === "string" ? { at: parsed.at } : {}),
|
|
121
122
|
};
|
|
122
123
|
}
|
|
123
124
|
catch {
|
|
@@ -143,9 +144,14 @@ const RMW_CLAIM_STALE_MS = 10_000;
|
|
|
143
144
|
* The owner's recorded process-instance token settles that by string equality
|
|
144
145
|
* (core/procstart.ts); no clock is compared, because a lock's mtime and a start
|
|
145
146
|
* time come from different clocks and any skew would "prove" a live owner
|
|
146
|
-
* recycled.
|
|
147
|
-
*
|
|
148
|
-
|
|
147
|
+
* recycled. Where no token exists (macOS, Windows) a live pid is "unproven",
|
|
148
|
+
* and an AGED lock is still taken over once the OS proves the process now
|
|
149
|
+
* holding that pid was created after the lock's recorded `at` (issue #457) —
|
|
150
|
+
* the same `createdAfterLock` rule as the write lock. `startedAt` is
|
|
151
|
+
* memoized per acquisition by the caller, because the OS probe spawns.
|
|
152
|
+
* `readRmwOwner` stays FIRST: it throws on unsafe/hardlinked or oversized
|
|
153
|
+
* ownership metadata, and unsafe ownership never licenses removal. */
|
|
154
|
+
function rmwLockIsStale(lock, startedAt) {
|
|
149
155
|
let stat;
|
|
150
156
|
try {
|
|
151
157
|
stat = lstatSync(lock);
|
|
@@ -157,9 +163,10 @@ function rmwLockIsStale(lock) {
|
|
|
157
163
|
}
|
|
158
164
|
const owner = readRmwOwner(lock);
|
|
159
165
|
if (owner && owner.host === hostname()) {
|
|
160
|
-
// One shared table with src/serve/writelock.ts (core/procstart.ts),
|
|
161
|
-
//
|
|
162
|
-
//
|
|
166
|
+
// One shared table with src/serve/writelock.ts (core/procstart.ts), and one
|
|
167
|
+
// shared creation-time check for an "unproven" pid, so the two locks apply
|
|
168
|
+
// the same rule; "age" means nothing was proven and the fallback below
|
|
169
|
+
// governs, exactly as for a foreign host.
|
|
163
170
|
const verdict = judgeSameHostOwner(owner, {
|
|
164
171
|
alive: rmwPidAlive,
|
|
165
172
|
self: selfStartToken(),
|
|
@@ -168,6 +175,8 @@ function rmwLockIsStale(lock) {
|
|
|
168
175
|
selfPid: process.pid,
|
|
169
176
|
heldNonces: heldLockNonces,
|
|
170
177
|
});
|
|
178
|
+
if (verdict === "unproven")
|
|
179
|
+
return Date.now() - stat.mtimeMs > 10_000 && createdAfterLock(owner, startedAt);
|
|
171
180
|
if (verdict !== "age")
|
|
172
181
|
return verdict === "stale";
|
|
173
182
|
}
|
|
@@ -210,7 +219,7 @@ function rmwLockIsStale(lock) {
|
|
|
210
219
|
* so the race persists until every writer on the store is upgraded. Returns true
|
|
211
220
|
* when the path may now be free.
|
|
212
221
|
*/
|
|
213
|
-
function takeOverRmwLock(lock) {
|
|
222
|
+
function takeOverRmwLock(lock, startedAt) {
|
|
214
223
|
const claim = `${lock}.reclaim`;
|
|
215
224
|
try {
|
|
216
225
|
mkdirSync(claim);
|
|
@@ -229,7 +238,7 @@ function takeOverRmwLock(lock) {
|
|
|
229
238
|
return false;
|
|
230
239
|
}
|
|
231
240
|
try {
|
|
232
|
-
if (!rmwLockIsStale(lock))
|
|
241
|
+
if (!rmwLockIsStale(lock, startedAt))
|
|
233
242
|
return false; // re-judged from scratch under the claim
|
|
234
243
|
rmSync(lock, { recursive: true, force: true });
|
|
235
244
|
return true;
|
|
@@ -253,6 +262,7 @@ function unsafePath(path, reason) {
|
|
|
253
262
|
}
|
|
254
263
|
export class JsonStore {
|
|
255
264
|
paths;
|
|
265
|
+
lockOpts;
|
|
256
266
|
_warnedForward = false;
|
|
257
267
|
/** Memoized validated records per kind. loadAll() is on every read-path method
|
|
258
268
|
* (why/checkConstraints/fragility/assembleContext…), each of which previously
|
|
@@ -265,8 +275,13 @@ export class JsonStore {
|
|
|
265
275
|
lexicalRoot;
|
|
266
276
|
canonicalRoot;
|
|
267
277
|
lexicalHunch;
|
|
268
|
-
|
|
278
|
+
/** `lockOpts.startedAt` is an INTERNAL TEST SEAM, not API: it replaces the OS
|
|
279
|
+
* creation-time probe of the `.rmw-lock` "unproven" rule so the suite can
|
|
280
|
+
* drive it on linux, where the real probe reports nothing. Callers outside
|
|
281
|
+
* this repo must leave it unset — `processStartedAt` is the only sound probe. */
|
|
282
|
+
constructor(paths, lockOpts = {}) {
|
|
269
283
|
this.paths = paths;
|
|
284
|
+
this.lockOpts = lockOpts;
|
|
270
285
|
this.lexicalRoot = resolve(paths.root);
|
|
271
286
|
this.canonicalRoot = realpathSync(this.lexicalRoot);
|
|
272
287
|
this.lexicalHunch = resolve(paths.hunch);
|
|
@@ -666,6 +681,15 @@ export class JsonStore {
|
|
|
666
681
|
const lock = join(directory.lexical, ".rmw-lock");
|
|
667
682
|
const deadline = Date.now() + 2_000;
|
|
668
683
|
const nonce = randomBytes(8).toString("hex");
|
|
684
|
+
// The creation-time probe spawns; ask it once per pid for this acquisition,
|
|
685
|
+
// not on every poll nor again under the takeover claim (as writelock.ts).
|
|
686
|
+
const createdAt = new Map();
|
|
687
|
+
const probe = this.lockOpts.startedAt ?? processStartedAt;
|
|
688
|
+
const startedAt = (pid) => {
|
|
689
|
+
if (!createdAt.has(pid))
|
|
690
|
+
createdAt.set(pid, probe(pid));
|
|
691
|
+
return createdAt.get(pid);
|
|
692
|
+
};
|
|
669
693
|
for (;;) {
|
|
670
694
|
try {
|
|
671
695
|
mkdirSync(lock);
|
|
@@ -676,10 +700,11 @@ export class JsonStore {
|
|
|
676
700
|
// writer may exceed the stale-age heuristic while serializing a large
|
|
677
701
|
// index; its PID must prevent a second writer from taking over. The
|
|
678
702
|
// process-instance token (when this platform has one) is what lets a
|
|
679
|
-
// successor tell that PID apart from a recycled one
|
|
703
|
+
// successor tell that PID apart from a recycled one; without one, `at`
|
|
704
|
+
// lets it prove the pid's current process was created after this lock.
|
|
680
705
|
try {
|
|
681
706
|
const start = selfStartToken();
|
|
682
|
-
writeFileAtomic(join(lock, "owner.tmp.json"), JSON.stringify({ pid: process.pid, host: hostname(), nonce, ...(start !== null ? { start } : {}) }));
|
|
707
|
+
writeFileAtomic(join(lock, "owner.tmp.json"), JSON.stringify({ pid: process.pid, host: hostname(), nonce, at: new Date().toISOString(), ...(start !== null ? { start } : {}) }));
|
|
683
708
|
}
|
|
684
709
|
catch (error) {
|
|
685
710
|
try {
|
|
@@ -694,7 +719,7 @@ export class JsonStore {
|
|
|
694
719
|
catch (error) {
|
|
695
720
|
if (error.code !== "EEXIST")
|
|
696
721
|
throw error;
|
|
697
|
-
if (rmwLockIsStale(lock) && takeOverRmwLock(lock))
|
|
722
|
+
if (rmwLockIsStale(lock, startedAt) && takeOverRmwLock(lock, startedAt))
|
|
698
723
|
continue;
|
|
699
724
|
// The deadline is checked only AFTER an acquire has been attempted: with
|
|
700
725
|
// the check at the top of the loop, a successful takeover could still be
|
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.45.
|
|
10
|
+
"version": "1.45.2",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@davesheffer/hunch",
|
|
16
|
-
"version": "1.45.
|
|
16
|
+
"version": "1.45.2",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|