synomem 0.9.0 → 0.9.1

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/resolvers.js CHANGED
@@ -8,7 +8,10 @@
8
8
  * each other's attribution. The resolver narrows (pins a context, restricts to a
9
9
  * preset); the server alone authorizes.
10
10
  */
11
- import { createHash } from 'node:crypto';
11
+ import { randomBytes } from 'node:crypto';
12
+ import { closeSync, existsSync, openSync, readFileSync, rmSync, statSync, writeSync, } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { atomicWriteFile } from './fs-utils.js';
12
15
  import { SynomemClient } from './client.js';
13
16
  import { describeIdentity, discoverContexts } from './discover.js';
14
17
  import { SynomemError } from './errors.js';
@@ -29,15 +32,93 @@ function required() {
29
32
  return new SynomemError('CONTEXT_REQUIRED', 'This connection can act as more than one workspace/actor. Pass contextId: call synomem_context_list and choose the context that matches what the user asked for.');
30
33
  }
31
34
  /**
32
- * Stable local context id: derived from the store's persistent workspace id and the
33
- * canonical actor — never from a display name — so it survives renames and moving the
34
- * store, and an unrelated store with a same-named agent gets a different id.
35
+ * The stable local context id for one actor in one store, from the store's own
36
+ * context registry (`<home>/contexts.json`).
37
+ *
38
+ * The id is cryptographically random (128 bits) and minted the first time the
39
+ * actor is addressed, never derived from a name or from the canonical tuple:
40
+ * an id reveals nothing about its target and cannot be guessed. It survives
41
+ * renames and moving the store (the registry moves with it), and an unrelated
42
+ * store with a same-named agent has its own registry and so its own id.
43
+ * Minting is serialized across processes so two harnesses opening the store
44
+ * at once agree on one id.
35
45
  */
36
- export function localContextId(storeWorkspaceId, actor) {
37
- const digest = createHash('sha256')
38
- .update(`${storeWorkspaceId}\0${actor.kind}\0${actor.id}`)
39
- .digest('hex');
40
- return `lctx_${digest.slice(0, 24)}`;
46
+ export function localContextId(home, actor) {
47
+ const path = join(home, 'contexts.json');
48
+ const key = `${actor.kind}:${actor.id}`;
49
+ const existing = readRegistry(path).contexts[key];
50
+ if (existing)
51
+ return existing;
52
+ return withRegistryLock(home, () => {
53
+ const registry = readRegistry(path);
54
+ const current = registry.contexts[key];
55
+ if (current)
56
+ return current;
57
+ const id = `lctx_${randomBytes(16).toString('hex')}`;
58
+ registry.contexts[key] = id;
59
+ atomicWriteFile(path, `${JSON.stringify(registry, null, 2)}\n`, 0o600);
60
+ return id;
61
+ });
62
+ }
63
+ function readRegistry(path) {
64
+ if (!existsSync(path))
65
+ return { version: 1, contexts: {} };
66
+ try {
67
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
68
+ if (parsed.version === 1 && parsed.contexts && typeof parsed.contexts === 'object') {
69
+ return { version: 1, contexts: { ...parsed.contexts } };
70
+ }
71
+ }
72
+ catch {
73
+ /* fall through */
74
+ }
75
+ throw new SynomemError('CONFIG_INVALID', `${path} is not a valid Synomem context registry.`);
76
+ }
77
+ /** A short synchronous O_EXCL lock; a dead or old owner is broken. */
78
+ function withRegistryLock(home, operation) {
79
+ const lock = join(home, 'contexts.json.lock');
80
+ const deadline = Date.now() + 10_000;
81
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
82
+ for (;;) {
83
+ try {
84
+ const descriptor = openSync(lock, 'wx', 0o600);
85
+ writeSync(descriptor, `${process.pid}\n`);
86
+ closeSync(descriptor);
87
+ break;
88
+ }
89
+ catch (error) {
90
+ if (error.code !== 'EEXIST')
91
+ throw error;
92
+ try {
93
+ const pid = Number(readFileSync(lock, 'utf8').trim());
94
+ const old = Date.now() - statSync(lock).mtimeMs > 5_000;
95
+ let alive = Number.isSafeInteger(pid) && pid > 0;
96
+ if (alive) {
97
+ try {
98
+ process.kill(pid, 0);
99
+ }
100
+ catch (probe) {
101
+ alive = probe.code === 'EPERM';
102
+ }
103
+ }
104
+ if (!alive || old)
105
+ rmSync(lock, { force: true });
106
+ }
107
+ catch {
108
+ /* released meanwhile */
109
+ }
110
+ if (Date.now() > deadline) {
111
+ throw new SynomemError('DATABASE_BUSY', 'The local context registry is locked. Try again.');
112
+ }
113
+ Atomics.wait(sleeper, 0, 0, 20);
114
+ }
115
+ }
116
+ try {
117
+ return operation();
118
+ }
119
+ finally {
120
+ rmSync(lock, { force: true });
121
+ }
41
122
  }
42
123
  /**
43
124
  * A remote (hosted API) resolver over one credential.
@@ -194,7 +275,7 @@ export function createLocalResolver(options) {
194
275
  return {
195
276
  service: client,
196
277
  context: {
197
- contextId: localContextId(capabilities.binding.workspaceId, actor),
278
+ contextId: localContextId(options.home, actor),
198
279
  organizationId: null,
199
280
  workspaceId: capabilities.binding.workspaceId,
200
281
  actor,
@@ -1 +1 @@
1
- {"version":3,"file":"resolvers.js","sourceRoot":"","sources":["../src/resolvers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"resolvers.js","sourceRoot":"","sources":["../src/resolvers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,SAAS,EACT,UAAU,EACV,QAAQ,EACR,YAAY,EACZ,MAAM,EACN,QAAQ,EACR,SAAS,GACV,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AA6B/D,wFAAwF;AACxF,MAAM,CAAC,MAAM,eAAe,GAAG,SAAS,CAAC;AAEzC,MAAM,YAAY,GAAG,MAAM,CAAC;AAE5B,SAAS,SAAS,CAAC,SAA6B;IAC9C,MAAM,KAAK,GAAG,SAAS,EAAE,IAAI,EAAE,CAAC;IAChC,OAAO,CAAC,KAAK,IAAI,KAAK,KAAK,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;AACjE,CAAC;AAED,SAAS,SAAS;IAChB,2EAA2E;IAC3E,6EAA6E;IAC7E,OAAO,IAAI,YAAY,CACrB,mBAAmB,EACnB,uGAAuG,CACxG,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ;IACf,OAAO,IAAI,YAAY,CACrB,kBAAkB,EAClB,kKAAkK,CACnK,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,KAAoB;IAC/D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC;IACzC,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,EAAE,EAAE,CAAC;IACxC,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IAClD,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC9B,OAAO,gBAAgB,CAAC,IAAI,EAAE,GAAG,EAAE;QACjC,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QACpC,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACvC,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC;QAC5B,MAAM,EAAE,GAAG,QAAQ,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACrD,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;QAC5B,eAAe,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACvE,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC,CAAC;AACL,CAAC;AAOD,SAAS,YAAY,CAAC,IAAY;IAChC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAC3D,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAA6B,CAAC;QAClF,IAAI,MAAM,CAAC,OAAO,KAAK,CAAC,IAAI,MAAM,CAAC,QAAQ,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACnF,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;QAC1D,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,kBAAkB;IACpB,CAAC;IACD,MAAM,IAAI,YAAY,CAAC,gBAAgB,EAAE,GAAG,IAAI,2CAA2C,CAAC,CAAC;AAC/F,CAAC;AAED,sEAAsE;AACtE,SAAS,gBAAgB,CAAI,IAAY,EAAE,SAAkB;IAC3D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC;IACrC,MAAM,OAAO,GAAG,IAAI,UAAU,CAAC,IAAI,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC;IACzD,SAAS,CAAC;QACR,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;YAC/C,SAAS,CAAC,UAAU,EAAE,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;YAC1C,SAAS,CAAC,UAAU,CAAC,CAAC;YACtB,MAAM;QACR,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,KAAK,CAAC;YACpE,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;gBACtD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,GAAG,KAAK,CAAC;gBACxD,IAAI,KAAK,GAAG,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC;gBACjD,IAAI,KAAK,EAAE,CAAC;oBACV,IAAI,CAAC;wBACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;oBACvB,CAAC;oBAAC,OAAO,KAAK,EAAE,CAAC;wBACf,KAAK,GAAI,KAA+B,CAAC,IAAI,KAAK,OAAO,CAAC;oBAC5D,CAAC;gBACH,CAAC;gBACD,IAAI,CAAC,KAAK,IAAI,GAAG;oBAAE,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACnD,CAAC;YAAC,MAAM,CAAC;gBACP,wBAAwB;YAC1B,CAAC;YACD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;gBAC1B,MAAM,IAAI,YAAY,CAAC,eAAe,EAAE,kDAAkD,CAAC,CAAC;YAC9F,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IACD,IAAI,CAAC;QACH,OAAO,SAAS,EAAE,CAAC;IACrB,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAChC,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAMpC;IACC,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC9C,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,OAAO,CAAC,iBAAiB,EAAE,MAAM;QAC/C,CAAC,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,iBAAiB,CAAC;QACpC,CAAC,CAAC,SAAS,CAAC;IACd,IAAI,MAA2D,CAAC;IAChE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAyC,CAAC;IAElE,MAAM,WAAW,GAAG,KAAK,IAAqB,EAAE;QAC9C,MAAM,KAAK,GAAG,MAAM,MAAM,EAAE,CAAC;QAC7B,IAAI,CAAC,KAAK;YACR,MAAM,IAAI,YAAY,CAAC,eAAe,EAAE,4CAA4C,CAAC,CAAC;QACxF,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IAEF,MAAM,aAAa,GAAG,KAAK,EAAE,KAAK,GAAG,KAAK,EAA2B,EAAE;QACrE,IAAI,CAAC,KAAK,IAAI,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,EAAE,GAAG,YAAY;YAAE,OAAO,MAAM,CAAC,OAAO,CAAC;QACrF,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC;YACrC,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,WAAW,EAAE,MAAM,WAAW,EAAE;YAChC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnD,CAAC,CAAC;QACH,MAAM,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QACrC,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;IAEF,MAAM,OAAO,GAAG,CAAC,OAAuB,EAAkB,EAAE;QAC1D,IAAI,MAAM,EAAE,CAAC;YACX,OAAO;gBACL,IAAI,EAAE,OAAO;gBACb,cAAc,EAAE,MAAM;gBACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,KAAK,MAAM,CAAC;gBACxE,UAAU,EAAE,IAAI;aACjB,CAAC;QACJ,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO;gBACL,IAAI,EAAE,UAAU;gBAChB,cAAc,EAAE,IAAI;gBACpB,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;gBAC1E,UAAU,EAAE,IAAI;aACjB,CAAC;QACJ,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;IAEF,MAAM,UAAU,GAAG,KAAK,EAAE,SAAiB,EAA2B,EAAE;QACtE,IAAI,KAAK,GAAG,CAAC,MAAM,aAAa,EAAE,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC;QAC5F,sFAAsF;QACtF,kDAAkD;QAClD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,KAAK,GAAG,CAAC,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC;QAC9F,CAAC;QACD,IAAI,CAAC,KAAK;YAAE,MAAM,SAAS,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IAEF,MAAM,UAAU,GAAG,CAAC,OAAuB,EAAiC,EAAE;QAC5E,IAAI,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC9C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,GAAG,CAAC,KAAK,IAAI,EAAE;gBACpB,MAAM,OAAO,GAAG,IAAI,oBAAoB,CAAC;oBACvC,OAAO,EAAE,OAAO,CAAC,OAAO;oBACxB,WAAW,EAAE,OAAO,CAAC,WAAW;oBAChC,SAAS,EAAE,OAAO,CAAC,SAAS;oBAC5B,UAAU,EAAE,OAAO,CAAC,UAAU;oBAC9B,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBACnD,CAAC,CAAC;gBACH,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;gBACrB,OAAO,OAAO,CAAC;YACjB,CAAC,CAAC,EAAE,CAAC;YACL,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YACzC,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;QAC1D,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;IAEF,OAAO;QACL,IAAI;YACF,IAAI,MAAM;gBAAE,OAAO,OAAO,CAAC;YAC3B,IAAI,OAAO;gBAAE,OAAO,UAAU,CAAC;YAC/B,OAAO,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;QAClD,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,SAAS;YACrB,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;YACvC,IAAI,MAAc,CAAC;YACnB,IAAI,MAAM,EAAE,CAAC;gBACX,IAAI,SAAS,IAAI,SAAS,KAAK,MAAM;oBAAE,MAAM,SAAS,EAAE,CAAC;gBACzD,MAAM,GAAG,MAAM,CAAC;YAClB,CAAC;iBAAM,IAAI,SAAS,EAAE,CAAC;gBACrB,IAAI,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;oBAAE,MAAM,SAAS,EAAE,CAAC;gBAC1D,MAAM,GAAG,SAAS,CAAC;YACrB,CAAC;iBAAM,CAAC;gBACN,MAAM,OAAO,GAAG,MAAM,aAAa,EAAE,CAAC;gBACtC,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,IAAI,OAAO,CAAC,cAAc,IAAI,CAAC,OAAO,EAAE,CAAC;oBACnE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;gBAClC,CAAC;qBAAM,CAAC;oBACN,MAAM,QAAQ,EAAE,CAAC;gBACnB,CAAC;YACH,CAAC;YACD,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,CAAC;YACzC,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,CAAC;YAC1C,OAAO;gBACL,OAAO;gBACP,OAAO,EAAE;oBACP,SAAS,EAAE,OAAO,CAAC,SAAS;oBAC5B,cAAc,EAAE,OAAO,CAAC,cAAc;oBACtC,WAAW,EAAE,OAAO,CAAC,WAAW;oBAChC,KAAK,EAAE,OAAO,CAAC,KAAK;iBACrB;aACF,CAAC;QACJ,CAAC;QACD,KAAK,CAAC,IAAI;YACR,OAAO,OAAO,CAAC,MAAM,aAAa,EAAE,CAAC,CAAC;QACxC,CAAC;QACD,KAAK,CAAC,QAAQ;YACZ,OAAO,MAAM,gBAAgB,CAAC;gBAC5B,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,WAAW,EAAE,MAAM,WAAW,EAAE;gBAChC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACnD,CAAC,CAAC;QACL,CAAC;QACD,KAAK,CAAC,KAAK;YACT,MAAM,OAAO,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YACvC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACjB,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,MAAM,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QACpF,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAA8F;IAE9F,IAAI,KAA2C,CAAC;IAChD,MAAM,IAAI,GAAG,GAA6B,EAAE;QAC1C,KAAK,KAAK,CAAC,KAAK,IAAI,EAAE;YACpB,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;YAC3F,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YACpB,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,YAAY,EAAE,CAAC;YACjD,qFAAqF;YACrF,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;YAC3B,OAAO;gBACL,OAAO,EAAE,MAAM;gBACf,OAAO,EAAE;oBACP,SAAS,EAAE,cAAc,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;oBAC9C,cAAc,EAAE,IAAI;oBACpB,WAAW,EAAE,YAAY,CAAC,OAAO,CAAC,WAAW;oBAC7C,KAAK;iBACN;aACF,CAAC;QACJ,CAAC,CAAC,EAAE,CAAC;QACL,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE;YACf,KAAK,GAAG,SAAS,CAAC;QACpB,CAAC,CAAC,CAAC;QACH,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,GAAG,EAAE,CAAC,OAAO;QACnB,KAAK,CAAC,OAAO,CAAC,SAAS;YACrB,MAAM,KAAK,GAAG,MAAM,IAAI,EAAE,CAAC;YAC3B,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;YACvC,IAAI,SAAS,IAAI,SAAS,KAAK,KAAK,CAAC,OAAO,CAAC,SAAS;gBAAE,MAAM,SAAS,EAAE,CAAC;YAC1E,OAAO,KAAK,CAAC;QACf,CAAC;QACD,KAAK,CAAC,IAAI;YACR,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,EAAE,CAAC;YACjC,OAAO;gBACL,IAAI,EAAE,OAAO;gBACb,cAAc,EAAE,OAAO,CAAC,SAAS;gBACjC,QAAQ,EAAE;oBACR;wBACE,SAAS,EAAE,OAAO,CAAC,SAAS;wBAC5B,cAAc,EAAE,IAAI;wBACpB,WAAW,EAAE,OAAO,CAAC,WAAW;wBAChC,KAAK,EAAE,OAAO,CAAC,KAAK;wBACpB,OAAO,EAAE,CAAC,cAAc,EAAE,eAAe,CAAC;wBAC1C,MAAM,EAAE,OAAO;qBAChB;iBACF;gBACD,UAAU,EAAE,IAAI;aACjB,CAAC;QACJ,CAAC;QACD,KAAK,CAAC,QAAQ;YACZ,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,EAAE,CAAC;YACjC,OAAO;gBACL,OAAO,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE;gBACxB,cAAc,EAAE,IAAI;gBACpB,UAAU,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE;gBAC/D,KAAK,EAAE,IAAI;gBACX,cAAc,EAAE,OAAO,CAAC,SAAS;gBACjC,gBAAgB,EAAE,OAAO;aAC1B,CAAC;QACJ,CAAC;QACD,KAAK,CAAC,KAAK;YACT,MAAM,OAAO,GAAG,KAAK,CAAC;YACtB,KAAK,GAAG,SAAS,CAAC;YAClB,IAAI,OAAO;gBAAE,MAAM,CAAC,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QAC7E,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synomem",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Shared memory, durable communication, recognition, and task coordination for AI agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli.ts CHANGED
@@ -4,12 +4,7 @@ import { spawn } from 'node:child_process';
4
4
  import { join, resolve } from 'node:path';
5
5
  import { pathToFileURL } from 'node:url';
6
6
  import { Command, CommanderError, Option } from 'commander';
7
- import {
8
- ensureLocalStore,
9
- LOCAL_OPERATOR,
10
- localStoreWorkspaceId,
11
- openLocalService,
12
- } from './backend.js';
7
+ import { ensureLocalStore, LOCAL_OPERATOR, openLocalService } from './backend.js';
13
8
  import { cloudApiUrl } from './cloud.js';
14
9
  import { resolveHome } from './config.js';
15
10
  import {
@@ -509,7 +504,7 @@ export function createCli(io: CliIo = defaultIo, dependencies: CliDependencies =
509
504
  if (!handle) throw new SynomemError('INVALID_INPUT', 'An agent handle is required.');
510
505
  const profileName = assertName(options.profileName ?? handle, 'profile');
511
506
  const store = profileStoreFor(global.home);
512
- const storeConfig = ensureLocalStore(global.home);
507
+ ensureLocalStore(global.home);
513
508
  const config = store.read();
514
509
 
515
510
  const existing = config.profiles[profileName];
@@ -568,7 +563,7 @@ export function createCli(io: CliIo = defaultIo, dependencies: CliDependencies =
568
563
  ...(global.explicitHome ? { home: global.home } : {}),
569
564
  actorId: agent.id,
570
565
  actorName: agent.displayName,
571
- contextId: localContextId(storeConfig.workspaceId, actor),
566
+ contextId: localContextId(global.home, actor),
572
567
  };
573
568
  const next: ProfilesConfig = {
574
569
  ...config,
@@ -1017,7 +1012,7 @@ export function createCli(io: CliIo = defaultIo, dependencies: CliDependencies =
1017
1012
  ...(storeHome !== global.home ? { home: storeHome } : {}),
1018
1013
  actorId: agent.id,
1019
1014
  actorName: agent.displayName,
1020
- contextId: localContextId(localStoreWorkspaceId(storeHome), {
1015
+ contextId: localContextId(storeHome, {
1021
1016
  kind: 'agent',
1022
1017
  id: agent.id,
1023
1018
  }),
@@ -1629,7 +1624,7 @@ export function createCli(io: CliIo = defaultIo, dependencies: CliDependencies =
1629
1624
  ...(created.storeHome !== global.home ? { home: created.storeHome } : {}),
1630
1625
  actorId: agent.id,
1631
1626
  actorName: agent.displayName,
1632
- contextId: localContextId(localStoreWorkspaceId(created.storeHome), {
1627
+ contextId: localContextId(created.storeHome, {
1633
1628
  kind: 'agent',
1634
1629
  id: agent.id,
1635
1630
  }),
@@ -21,19 +21,21 @@
21
21
  * that names the explicit alternative.
22
22
  */
23
23
  import { spawn } from 'node:child_process';
24
- import { randomUUID } from 'node:crypto';
24
+ import { randomBytes, randomUUID } from 'node:crypto';
25
25
  import {
26
26
  chmodSync,
27
27
  closeSync,
28
28
  existsSync,
29
+ linkSync,
29
30
  mkdirSync,
30
31
  openSync,
31
32
  readFileSync,
33
+ renameSync,
32
34
  rmSync,
33
35
  statSync,
34
- unlinkSync,
35
36
  writeSync,
36
37
  } from 'node:fs';
38
+ import { hostname } from 'node:os';
37
39
  import { join } from 'node:path';
38
40
  import { z } from 'zod';
39
41
  import { SynomemError } from './errors.js';
@@ -154,7 +156,18 @@ export class OsCredentialStore implements CredentialStore {
154
156
 
155
157
  constructor(options: { platform?: NodeJS.Platform; run?: CredentialCommandRunner } = {}) {
156
158
  this.platform = options.platform ?? process.platform;
157
- this.run = options.run ?? command;
159
+ const run = options.run ?? command;
160
+ // A missing helper (a headless Linux box with no Secret Service) is a
161
+ // configuration answer, not a crash: name the explicit alternatives. There
162
+ // is still no fallback — the caller chooses --store file deliberately.
163
+ this.run = async (executable, arguments_, input) => {
164
+ try {
165
+ return await run(executable, arguments_, input);
166
+ } catch (error) {
167
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') this.unsupported();
168
+ throw error;
169
+ }
170
+ };
158
171
  }
159
172
 
160
173
  async get(reference: string): Promise<StoredCredential | undefined> {
@@ -235,7 +248,7 @@ export class OsCredentialStore implements CredentialStore {
235
248
  'CONFIG_INVALID',
236
249
  this.platform === 'win32'
237
250
  ? 'Windows Credential Manager storage is not supported yet. Use --store file, or --store environment with SYNOMEM_ACCESS_TOKEN.'
238
- : 'Operating-system credential storage needs the macOS Keychain, or secret-tool on Linux. Use --store file, or --store environment with SYNOMEM_ACCESS_TOKEN.',
251
+ : 'No operating-system credential store is available here (it needs the macOS Keychain, or secret-tool with a Secret Service on Linux). Choose one explicitly: --store file keeps it in a 0600 file under your Synomem home; --store environment reads SYNOMEM_ACCESS_TOKEN.',
239
252
  );
240
253
  }
241
254
  }
@@ -261,6 +274,8 @@ export class FileCredentialStore implements CredentialStore {
261
274
  async get(reference: string): Promise<StoredCredential | undefined> {
262
275
  const path = this.path(reference);
263
276
  if (!existsSync(path)) return undefined;
277
+ assertPrivate(this.directory, 0o700);
278
+ assertPrivate(path, 0o600);
264
279
  return parseStoredCredential(readFileSync(path, 'utf8'));
265
280
  }
266
281
 
@@ -282,6 +297,31 @@ export class FileCredentialStore implements CredentialStore {
282
297
  }
283
298
  }
284
299
 
300
+ /*
301
+ * Like ssh with a private key: a credential file (or its directory) that other
302
+ * users can read, or that another user owns, is refused rather than used. The
303
+ * fix is the owner's to make; silently tightening it would hide that the
304
+ * secret may already have been exposed.
305
+ */
306
+ function assertPrivate(path: string, expected: number): void {
307
+ if (process.platform === 'win32') return;
308
+ const stat = statSync(path);
309
+ if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
310
+ throw new SynomemError(
311
+ 'CONFIG_INVALID',
312
+ `${path} is owned by another user; refusing to use it.`,
313
+ );
314
+ }
315
+ if ((stat.mode & 0o077) !== 0) {
316
+ throw new SynomemError(
317
+ 'CONFIG_INVALID',
318
+ `${path} is accessible to other users (mode ${(stat.mode & 0o777).toString(8)}). ` +
319
+ `Run \`chmod ${expected.toString(8)} ${path}\` if you are sure it was not exposed, ` +
320
+ 'otherwise remove it and sign in again.',
321
+ );
322
+ }
323
+ }
324
+
285
325
  export interface CredentialStores {
286
326
  keychain: CredentialStore;
287
327
  file: CredentialStore;
@@ -294,10 +334,19 @@ export function defaultCredentialStores(home: string): CredentialStores {
294
334
  /**
295
335
  * A cross-process lock around one credential's refresh.
296
336
  *
297
- * `O_EXCL` creation is the mutual exclusion; a lock older than `staleMs` is
298
- * from a crashed process and is broken. The lock only serializes refreshes —
299
- * correctness against a concurrent writer is the generation compare-and-swap
300
- * in `refreshCredential`.
337
+ * `O_EXCL` creation is the mutual exclusion. The lock file records its owner
338
+ * (pid, host) and a random token. A lock is broken only when its owner is
339
+ * provably gone — a dead pid on this host — or, for an owner on another host
340
+ * or an unreadable file, once it is older than `staleMs`. A live owner is never
341
+ * preempted however long its refresh takes (the refresh request itself times
342
+ * out well inside `staleMs`).
343
+ *
344
+ * Breaking renames the file aside first and checks the token: if another
345
+ * process already broke and re-acquired it in between, the fresh lock is put
346
+ * back rather than deleted. Correctness against a concurrent writer is still
347
+ * the generation compare-and-swap in the refresh path; the lock exists so that
348
+ * only one process ever spends a given refresh token. Locks assume one host's
349
+ * filesystem; a home shared over a network filesystem is not supported.
301
350
  */
302
351
  export async function withCredentialLock<T>(
303
352
  home: string,
@@ -314,22 +363,16 @@ export async function withCredentialLock<T>(
314
363
  const staleMs = options.staleMs ?? 60_000;
315
364
  const deadline = Date.now() + (options.timeoutMs ?? 30_000);
316
365
  const pollMs = options.pollMs ?? 50;
366
+ const token = randomBytes(16).toString('hex');
367
+ const host = hostname();
317
368
  let descriptor: number | undefined;
318
369
  while (descriptor === undefined) {
319
370
  try {
320
371
  descriptor = openSync(path, 'wx', 0o600);
321
- writeSync(descriptor, `${process.pid}\n`);
372
+ writeSync(descriptor, `${process.pid}\n${host}\n${token}\n`);
322
373
  } catch (error) {
323
374
  if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
324
- try {
325
- if (Date.now() - statSync(path).mtimeMs > staleMs) {
326
- unlinkSync(path);
327
- continue;
328
- }
329
- } catch {
330
- // Removed between the failed open and the stat: try again at once.
331
- continue;
332
- }
375
+ if (breakIfAbandoned(path, host, staleMs)) continue;
333
376
  if (Date.now() > deadline) {
334
377
  throw new SynomemError(
335
378
  'DATABASE_BUSY',
@@ -343,6 +386,70 @@ export async function withCredentialLock<T>(
343
386
  return await operation();
344
387
  } finally {
345
388
  closeSync(descriptor);
346
- rmSync(path, { force: true });
389
+ // Release only our own lock.
390
+ if (readLock(path)?.token === token) rmSync(path, { force: true });
391
+ }
392
+ }
393
+
394
+ interface LockOwner {
395
+ pid: number;
396
+ host: string;
397
+ token: string;
398
+ }
399
+
400
+ function readLock(path: string): LockOwner | undefined {
401
+ try {
402
+ const [pid, host, token] = readFileSync(path, 'utf8').split('\n');
403
+ const parsed = Number(pid);
404
+ if (!Number.isSafeInteger(parsed) || !host || !token) return undefined;
405
+ return { pid: parsed, host, token };
406
+ } catch {
407
+ return undefined;
408
+ }
409
+ }
410
+
411
+ function processAlive(pid: number): boolean {
412
+ try {
413
+ process.kill(pid, 0);
414
+ return true;
415
+ } catch (error) {
416
+ // EPERM: it exists but belongs to someone else — alive.
417
+ return (error as NodeJS.ErrnoException).code === 'EPERM';
418
+ }
419
+ }
420
+
421
+ /** Returns true when the caller should immediately retry acquiring. */
422
+ function breakIfAbandoned(path: string, host: string, staleMs: number): boolean {
423
+ let age: number;
424
+ try {
425
+ age = Date.now() - statSync(path).mtimeMs;
426
+ } catch {
427
+ return true; // Released between the failed open and now.
428
+ }
429
+ const owner = readLock(path);
430
+ // A lock whose contents are still being written is fresh, not abandoned.
431
+ if (!owner && age < 1_000) return false;
432
+ const abandoned = owner
433
+ ? owner.host === host
434
+ ? !processAlive(owner.pid)
435
+ : age > staleMs
436
+ : age > staleMs;
437
+ if (!abandoned) return false;
438
+ const aside = `${path}.broken-${randomBytes(6).toString('hex')}`;
439
+ try {
440
+ renameSync(path, aside);
441
+ } catch {
442
+ return true; // Someone else broke or released it first.
443
+ }
444
+ const taken = readLock(aside);
445
+ if (owner && taken?.token !== owner.token) {
446
+ // We grabbed a lock re-acquired after our inspection: restore it.
447
+ try {
448
+ linkSync(aside, path);
449
+ } catch {
450
+ /* a third process holds the path now; ours is the one to discard */
451
+ }
347
452
  }
453
+ rmSync(aside, { force: true });
454
+ return true;
348
455
  }
package/src/oauth.ts CHANGED
@@ -156,6 +156,20 @@ export async function discoverApiAuthorization(
156
156
  };
157
157
  }
158
158
 
159
+ export const TOKEN_REQUEST_TIMEOUT_MS = 20_000;
160
+
161
+ /*
162
+ * Failures that prove the request never reached the server. Anything else — a
163
+ * timeout, a reset mid-response — may have followed a committed rotation, so
164
+ * the refresh token it carried must be treated as spent (plan §6: never retry
165
+ * a possibly consumed token; reauthorize instead).
166
+ */
167
+ const NOT_SENT = new Set(['ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN', 'ENETUNREACH', 'EHOSTUNREACH']);
168
+ function neverDelivered(error: unknown): boolean {
169
+ const cause = (error as { cause?: { code?: unknown } } | undefined)?.cause;
170
+ return typeof cause?.code === 'string' && NOT_SENT.has(cause.code);
171
+ }
172
+
159
173
  async function tokenRequest(
160
174
  endpoint: string,
161
175
  parameters: URLSearchParams,
@@ -168,10 +182,15 @@ async function tokenRequest(
168
182
  redirect: 'error',
169
183
  headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
170
184
  body: parameters,
185
+ // Well inside the credential lock's stale window, so a refresh can never
186
+ // outlive the lock that serializes it.
187
+ signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS),
171
188
  });
172
189
  } catch (error) {
173
190
  if (error instanceof SynomemError) throw error;
174
- throw new SynomemError('REMOTE_UNAVAILABLE', 'The OAuth token endpoint is unavailable.');
191
+ throw new SynomemError('REMOTE_UNAVAILABLE', 'The OAuth token endpoint is unavailable.', {
192
+ delivery: neverDelivered(error) ? 'not_sent' : 'indeterminate',
193
+ });
175
194
  }
176
195
  const text = await response.text();
177
196
  if (Buffer.byteLength(text) > maximumDocumentBytes) {
package/src/profiles.ts CHANGED
@@ -27,7 +27,7 @@
27
27
  import { existsSync } from 'node:fs';
28
28
  import { join } from 'node:path';
29
29
  import { z } from 'zod';
30
- import { localStoreWorkspaceId, readSynomemConfig } from './backend.js';
30
+ import { readSynomemConfig } from './backend.js';
31
31
  import { resolveHome } from './config.js';
32
32
  import {
33
33
  withCredentialLock,
@@ -96,7 +96,7 @@ const localProfileSchema = z
96
96
  home: z.string().optional(),
97
97
  actorId: z.string().min(1),
98
98
  actorName: z.string().optional(),
99
- contextId: z.string().regex(/^lctx_[0-9a-f]{24}$/),
99
+ contextId: z.string().regex(/^lctx_[0-9a-f]{32}$/),
100
100
  })
101
101
  .strict();
102
102
 
@@ -374,7 +374,24 @@ export class ConnectionCredentialSource implements RemoteCredentialSource {
374
374
  try {
375
375
  refreshed = await refreshOAuthCredential(current, this.options.fetch ?? fetch);
376
376
  } catch (error) {
377
- if (error instanceof SynomemError && error.code === 'REMOTE_UNAVAILABLE') throw error;
377
+ if (
378
+ error instanceof SynomemError &&
379
+ error.code === 'REMOTE_UNAVAILABLE' &&
380
+ error.details?.delivery === 'not_sent'
381
+ ) {
382
+ // The refresh token provably never left this machine: safe to retry later.
383
+ throw error;
384
+ }
385
+ // Refused, or delivery unknown (the server may have rotated it): the
386
+ // stored refresh token is treated as spent and removed, so no process
387
+ // can replay it and trip reuse detection for the whole family.
388
+ const withoutRefresh: StoredOAuthCredential = { ...current };
389
+ delete withoutRefresh.refreshToken;
390
+ await store.set(entry.secretRef!, {
391
+ ...withoutRefresh,
392
+ expiresAt: 0,
393
+ generation: current.generation + 1,
394
+ });
378
395
  throw this.reauthorize(`Connection ${name} could not be refreshed.`);
379
396
  }
380
397
  // Compare-and-swap: only this process may have spent the refresh token
@@ -440,7 +457,7 @@ export function localProfileTarget(
440
457
  id: profile.actorId,
441
458
  ...(profile.actorName ? { displayName: profile.actorName } : {}),
442
459
  };
443
- const expected = localContextId(localStoreWorkspaceId(home), actor);
460
+ const expected = localContextId(home, actor);
444
461
  if (expected !== profile.contextId) {
445
462
  throw new SynomemError(
446
463
  'CONTEXT_FORBIDDEN',
package/src/resolvers.ts CHANGED
@@ -8,7 +8,18 @@
8
8
  * each other's attribution. The resolver narrows (pins a context, restricts to a
9
9
  * preset); the server alone authorizes.
10
10
  */
11
- import { createHash } from 'node:crypto';
11
+ import { randomBytes } from 'node:crypto';
12
+ import {
13
+ closeSync,
14
+ existsSync,
15
+ openSync,
16
+ readFileSync,
17
+ rmSync,
18
+ statSync,
19
+ writeSync,
20
+ } from 'node:fs';
21
+ import { join } from 'node:path';
22
+ import { atomicWriteFile } from './fs-utils.js';
12
23
  import { SynomemClient } from './client.js';
13
24
  import { describeIdentity, discoverContexts } from './discover.js';
14
25
  import { SynomemError } from './errors.js';
@@ -68,15 +79,90 @@ function required(): SynomemError {
68
79
  }
69
80
 
70
81
  /**
71
- * Stable local context id: derived from the store's persistent workspace id and the
72
- * canonical actor — never from a display name — so it survives renames and moving the
73
- * store, and an unrelated store with a same-named agent gets a different id.
82
+ * The stable local context id for one actor in one store, from the store's own
83
+ * context registry (`<home>/contexts.json`).
84
+ *
85
+ * The id is cryptographically random (128 bits) and minted the first time the
86
+ * actor is addressed, never derived from a name or from the canonical tuple:
87
+ * an id reveals nothing about its target and cannot be guessed. It survives
88
+ * renames and moving the store (the registry moves with it), and an unrelated
89
+ * store with a same-named agent has its own registry and so its own id.
90
+ * Minting is serialized across processes so two harnesses opening the store
91
+ * at once agree on one id.
74
92
  */
75
- export function localContextId(storeWorkspaceId: string, actor: ActorIdentity): string {
76
- const digest = createHash('sha256')
77
- .update(`${storeWorkspaceId}\0${actor.kind}\0${actor.id}`)
78
- .digest('hex');
79
- return `lctx_${digest.slice(0, 24)}`;
93
+ export function localContextId(home: string, actor: ActorIdentity): string {
94
+ const path = join(home, 'contexts.json');
95
+ const key = `${actor.kind}:${actor.id}`;
96
+ const existing = readRegistry(path).contexts[key];
97
+ if (existing) return existing;
98
+ return withRegistryLock(home, () => {
99
+ const registry = readRegistry(path);
100
+ const current = registry.contexts[key];
101
+ if (current) return current;
102
+ const id = `lctx_${randomBytes(16).toString('hex')}`;
103
+ registry.contexts[key] = id;
104
+ atomicWriteFile(path, `${JSON.stringify(registry, null, 2)}\n`, 0o600);
105
+ return id;
106
+ });
107
+ }
108
+
109
+ interface ContextRegistry {
110
+ version: 1;
111
+ contexts: Record<string, string>;
112
+ }
113
+
114
+ function readRegistry(path: string): ContextRegistry {
115
+ if (!existsSync(path)) return { version: 1, contexts: {} };
116
+ try {
117
+ const parsed = JSON.parse(readFileSync(path, 'utf8')) as Partial<ContextRegistry>;
118
+ if (parsed.version === 1 && parsed.contexts && typeof parsed.contexts === 'object') {
119
+ return { version: 1, contexts: { ...parsed.contexts } };
120
+ }
121
+ } catch {
122
+ /* fall through */
123
+ }
124
+ throw new SynomemError('CONFIG_INVALID', `${path} is not a valid Synomem context registry.`);
125
+ }
126
+
127
+ /** A short synchronous O_EXCL lock; a dead or old owner is broken. */
128
+ function withRegistryLock<T>(home: string, operation: () => T): T {
129
+ const lock = join(home, 'contexts.json.lock');
130
+ const deadline = Date.now() + 10_000;
131
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
132
+ for (;;) {
133
+ try {
134
+ const descriptor = openSync(lock, 'wx', 0o600);
135
+ writeSync(descriptor, `${process.pid}\n`);
136
+ closeSync(descriptor);
137
+ break;
138
+ } catch (error) {
139
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
140
+ try {
141
+ const pid = Number(readFileSync(lock, 'utf8').trim());
142
+ const old = Date.now() - statSync(lock).mtimeMs > 5_000;
143
+ let alive = Number.isSafeInteger(pid) && pid > 0;
144
+ if (alive) {
145
+ try {
146
+ process.kill(pid, 0);
147
+ } catch (probe) {
148
+ alive = (probe as NodeJS.ErrnoException).code === 'EPERM';
149
+ }
150
+ }
151
+ if (!alive || old) rmSync(lock, { force: true });
152
+ } catch {
153
+ /* released meanwhile */
154
+ }
155
+ if (Date.now() > deadline) {
156
+ throw new SynomemError('DATABASE_BUSY', 'The local context registry is locked. Try again.');
157
+ }
158
+ Atomics.wait(sleeper, 0, 0, 20);
159
+ }
160
+ }
161
+ try {
162
+ return operation();
163
+ } finally {
164
+ rmSync(lock, { force: true });
165
+ }
80
166
  }
81
167
 
82
168
  /**
@@ -240,7 +326,7 @@ export function createLocalResolver(
240
326
  return {
241
327
  service: client,
242
328
  context: {
243
- contextId: localContextId(capabilities.binding.workspaceId, actor),
329
+ contextId: localContextId(options.home, actor),
244
330
  organizationId: null,
245
331
  workspaceId: capabilities.binding.workspaceId,
246
332
  actor,