@agfpd/iapeer-memory 0.1.1 → 0.1.3
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/package.json +3 -3
- package/src/binary.ts +6 -2
- package/src/commands/init.ts +63 -11
- package/src/commands/install-binary.ts +2 -1
- package/src/commands/status.ts +3 -1
- package/src/commands/uninstall.ts +19 -3
- package/src/commands/update.ts +44 -2
- package/src/commands/verify.ts +117 -41
- package/src/paths.ts +3 -0
- package/src/signing.ts +168 -0
- package/src/templates/roles-en.ts +123 -108
- package/src/templates/roles-ru.ts +112 -101
- package/src/watcher.ts +228 -20
package/src/watcher.ts
CHANGED
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
* registration.ts/peerProfileStore.ts + live run):
|
|
5
5
|
*
|
|
6
6
|
* - the REGISTRANT is the IAP envelope's from-personality:
|
|
7
|
-
* `iapeer send watcher --from index` puts the trigger into
|
|
7
|
+
* `iapeer send watcher --from claude-index` puts the trigger into
|
|
8
8
|
* `<index-cwd>/.iapeer/peer-profile.json` → `notifier.triggers[]`,
|
|
9
9
|
* owner=index (writing into a foreign profile is impossible by
|
|
10
10
|
* invariant). We register from INDEX — the consumer of memoryd events.
|
|
11
|
+
* `--from` takes the full IDENTITY `<runtime>-<personality>` (iapeer
|
|
12
|
+
* cli fact, verified by e2e §A: bare "index" → exit 1 «invalid --from
|
|
13
|
+
* identity»); the durable trigger's `owner` stays the parsed
|
|
14
|
+
* PERSONALITY ("index") — readWatcherTrigger matching is unaffected.
|
|
11
15
|
* - replies (✓ registered / teaching errors / list) go to the
|
|
12
16
|
* from-personality SESSION, never to this script — so registration
|
|
13
17
|
* success is verified by READING THE STATE, not the reply:
|
|
@@ -33,6 +37,17 @@ import fs from "node:fs";
|
|
|
33
37
|
import path from "node:path";
|
|
34
38
|
|
|
35
39
|
export const WATCHER_TRIGGER_ID = "iapeer-memory-memoryd";
|
|
40
|
+
/** Fail-open sweep (инверсия, ADR-015): check-gated hourly timer → index. */
|
|
41
|
+
export const SWEEP_TRIGGER_ID = "iapeer-memory-inbox-sweep";
|
|
42
|
+
/** Weekly DreamWeaver tick (директива «всё КРОМЕ DREAM_TICK — копирайтеру»):
|
|
43
|
+
* a notifier TIMER straight to the index — memoryd never emitted this event
|
|
44
|
+
* (И1 fact), so the copywriter never sees it. */
|
|
45
|
+
export const DREAM_TRIGGER_ID = "iapeer-memory-dream-tick";
|
|
46
|
+
/** The inverted pipeline's first receiver of memoryd events (директива
|
|
47
|
+
* Артура 10.06): the copywriter vets BEFORE placement and reports to the
|
|
48
|
+
* index; same-id re-registration REPLACES the trigger (notifier contract),
|
|
49
|
+
* so update/verify --repair re-target idempotently. */
|
|
50
|
+
export const DEFAULT_EVENT_TARGET = "copywriter";
|
|
36
51
|
|
|
37
52
|
/** The watcher runs one executable file — a launcher wrapping the stable binary. */
|
|
38
53
|
export function launcherScriptContent(binaryPath: string): string {
|
|
@@ -46,21 +61,99 @@ export function launcherScriptContent(binaryPath: string): string {
|
|
|
46
61
|
].join("\n");
|
|
47
62
|
}
|
|
48
63
|
|
|
64
|
+
function writeExecutable(filePath: string, content: string): "written" | "identical" {
|
|
65
|
+
try {
|
|
66
|
+
if (fs.readFileSync(filePath, "utf-8") === content) return "identical";
|
|
67
|
+
} catch {
|
|
68
|
+
// absent — write
|
|
69
|
+
}
|
|
70
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
71
|
+
const tmp = `${filePath}.tmp`;
|
|
72
|
+
fs.writeFileSync(tmp, content, "utf-8");
|
|
73
|
+
fs.chmodSync(tmp, 0o755);
|
|
74
|
+
fs.renameSync(tmp, filePath);
|
|
75
|
+
return "written";
|
|
76
|
+
}
|
|
77
|
+
|
|
49
78
|
export function writeLauncherScript(opts: {
|
|
50
79
|
launcherPath: string;
|
|
51
80
|
binaryPath: string;
|
|
52
81
|
}): "written" | "identical" {
|
|
53
|
-
|
|
82
|
+
return writeExecutable(opts.launcherPath, launcherScriptContent(opts.binaryPath));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sweep check-script (fail-open, ADR-015): the notifier runs it before each
|
|
87
|
+
* sweep fire; exit 0 ⇔ stale drafts exist (the index is woken ONLY on a real
|
|
88
|
+
* backlog). Paths and the threshold are BAKED at generation — the notifier's
|
|
89
|
+
* env carries no IAPEER_MEMORY_* context. Covers BOTH inboxes: the human
|
|
90
|
+
* inbox has no memoryd emission yet (И1 fact) — the sweep is its bridge.
|
|
91
|
+
*/
|
|
92
|
+
export function staleCheckScriptContent(opts: {
|
|
93
|
+
vaultPath: string;
|
|
94
|
+
inboxFolders: string[];
|
|
95
|
+
staleSecs: number;
|
|
96
|
+
}): string {
|
|
97
|
+
const dirs = opts.inboxFolders
|
|
98
|
+
.map((f) => `"${path.join(opts.vaultPath, f)}"`)
|
|
99
|
+
.join(" ");
|
|
100
|
+
return [
|
|
101
|
+
"#!/usr/bin/env bash",
|
|
102
|
+
"# iapeer-memory inbox-stale check — generated by init/update (package-owned).",
|
|
103
|
+
"# exit 0 = stale drafts exist (notifier fires the sweep), exit 1 = all clear.",
|
|
104
|
+
`T=${opts.staleSecs}`,
|
|
105
|
+
"now=$(date +%s)",
|
|
106
|
+
`for dir in ${dirs}; do`,
|
|
107
|
+
' [ -d "$dir" ] || continue',
|
|
108
|
+
' for f in "$dir"/*.md; do',
|
|
109
|
+
' [ -e "$f" ] || continue',
|
|
110
|
+
' m=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null) || continue',
|
|
111
|
+
' [ $((now - m)) -ge "$T" ] && exit 0',
|
|
112
|
+
" done",
|
|
113
|
+
"done",
|
|
114
|
+
"exit 1",
|
|
115
|
+
"",
|
|
116
|
+
].join("\n");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function writeStaleCheckScript(opts: {
|
|
120
|
+
checkScriptPath: string;
|
|
121
|
+
vaultPath: string;
|
|
122
|
+
inboxFolders: string[];
|
|
123
|
+
staleSecs?: number;
|
|
124
|
+
}): "written" | "identical" {
|
|
125
|
+
return writeExecutable(
|
|
126
|
+
opts.checkScriptPath,
|
|
127
|
+
staleCheckScriptContent({
|
|
128
|
+
vaultPath: opts.vaultPath,
|
|
129
|
+
inboxFolders: opts.inboxFolders,
|
|
130
|
+
staleSecs: opts.staleSecs ?? 7200,
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Declare the copywriter an ephemeral worker (clean window per delivery,
|
|
137
|
+
* M1/M2/M3 — core canon «wake_policy ephemeral»). The profile is a CORE
|
|
138
|
+
* file: no-clobber merge of exactly one key, atomic write.
|
|
139
|
+
*/
|
|
140
|
+
export function patchWakePolicyEphemeral(
|
|
141
|
+
peerCwd: string,
|
|
142
|
+
): "written" | "identical" | "missing-profile" {
|
|
143
|
+
const profilePath = path.join(peerCwd, ".iapeer", "peer-profile.json");
|
|
144
|
+
let profile: Record<string, unknown>;
|
|
54
145
|
try {
|
|
55
|
-
|
|
146
|
+
const raw = JSON.parse(fs.readFileSync(profilePath, "utf-8")) as unknown;
|
|
147
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "missing-profile";
|
|
148
|
+
profile = raw as Record<string, unknown>;
|
|
56
149
|
} catch {
|
|
57
|
-
|
|
150
|
+
return "missing-profile";
|
|
58
151
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
fs.
|
|
63
|
-
fs.renameSync(tmp,
|
|
152
|
+
if (profile.wake_policy === "ephemeral") return "identical";
|
|
153
|
+
profile.wake_policy = "ephemeral";
|
|
154
|
+
const tmp = `${profilePath}.tmp`;
|
|
155
|
+
fs.writeFileSync(tmp, `${JSON.stringify(profile, null, 2)}\n`, "utf-8");
|
|
156
|
+
fs.renameSync(tmp, profilePath);
|
|
64
157
|
return "written";
|
|
65
158
|
}
|
|
66
159
|
|
|
@@ -71,23 +164,96 @@ export function registrationMessage(opts: {
|
|
|
71
164
|
}): string {
|
|
72
165
|
return JSON.stringify({
|
|
73
166
|
script: opts.script,
|
|
74
|
-
target: opts.target ??
|
|
167
|
+
target: opts.target ?? DEFAULT_EVENT_TARGET,
|
|
75
168
|
id: opts.id ?? WATCHER_TRIGGER_ID,
|
|
76
169
|
});
|
|
77
170
|
}
|
|
78
171
|
|
|
79
|
-
|
|
172
|
+
/** Sweep timer registration body (sent to the `timer` peer). The message is
|
|
173
|
+
* self-contained — it lands in a FRESH index session. */
|
|
174
|
+
export function sweepTimerMessage(opts: {
|
|
175
|
+
checkScriptPath: string;
|
|
176
|
+
every?: string;
|
|
177
|
+
target?: string;
|
|
178
|
+
id?: string;
|
|
179
|
+
}): string {
|
|
180
|
+
return JSON.stringify({
|
|
181
|
+
when: opts.every ?? "@every 1h",
|
|
182
|
+
message:
|
|
183
|
+
"INBOX_SWEEP: stale drafts sit in the inbox folders — the copywriter " +
|
|
184
|
+
"thread did not process them in time. Place them per your doctrine, " +
|
|
185
|
+
"unvetted: needs_review: true on each; the copywriter re-vets via " +
|
|
186
|
+
"PERMANENT_CHANGED when alive.",
|
|
187
|
+
target: opts.target ?? "index",
|
|
188
|
+
check: opts.checkScriptPath,
|
|
189
|
+
id: opts.id ?? SWEEP_TRIGGER_ID,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Weekly DreamWeaver tick (Mondays 04:00 by default — the human sleeps). */
|
|
194
|
+
export function dreamTimerMessage(opts?: {
|
|
195
|
+
cron?: string;
|
|
196
|
+
target?: string;
|
|
197
|
+
id?: string;
|
|
198
|
+
}): string {
|
|
199
|
+
return JSON.stringify({
|
|
200
|
+
when: opts?.cron ?? "0 4 * * 1",
|
|
201
|
+
message:
|
|
202
|
+
"DREAM_TICK: weekly agent-memory consolidation. Fan out DreamWeaver " +
|
|
203
|
+
"over the agent-memory subfolders (including your own), strictly one " +
|
|
204
|
+
"folder per task, sequentially — per your doctrine.",
|
|
205
|
+
target: opts?.target ?? "index",
|
|
206
|
+
id: opts?.id ?? DREAM_TRIGGER_ID,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export type IapSendResult = {
|
|
211
|
+
ok: boolean;
|
|
212
|
+
detail: string;
|
|
213
|
+
/** True when the test-sandbox fuse blocked the send — callers report a
|
|
214
|
+
* SKIP, not a failure (the iapeer `skipped-sandbox` precedent). */
|
|
215
|
+
suppressed?: boolean;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/** Default registrant personality (trigger owner + event target). */
|
|
219
|
+
const DEFAULT_REGISTRANT = "index";
|
|
220
|
+
/** Role peers are claude-runtime sessions (init creates them via `iapeer create`). */
|
|
221
|
+
const DEFAULT_FROM_RUNTIME = "claude";
|
|
222
|
+
|
|
223
|
+
/** `--from` wants the full identity `<runtime>-<personality>`, never a bare name. */
|
|
224
|
+
export function fromIdentity(personality: string, runtime = DEFAULT_FROM_RUNTIME): string {
|
|
225
|
+
return `${runtime}-${personality}`;
|
|
226
|
+
}
|
|
80
227
|
|
|
81
228
|
function iapSend(opts: {
|
|
82
229
|
message: string;
|
|
83
|
-
|
|
230
|
+
fromIdentity: string;
|
|
231
|
+
/** Notifier peer to address: `watcher` (events) or `timer` (cron/sweep). */
|
|
232
|
+
to?: "watcher" | "timer";
|
|
84
233
|
iapeerBin?: string;
|
|
85
234
|
}): IapSendResult {
|
|
235
|
+
// Hard fuse (incident 10.06, prod Index): verify-repair tests reached the
|
|
236
|
+
// LIVE watcher peer and registered crashlooping temp-path triggers — /tmp
|
|
237
|
+
// tmux sockets are host-global, no sandbox env can contain a real send.
|
|
238
|
+
// Test scripts set this env (package.json), mirroring IAPEER_TEST_SANDBOX.
|
|
239
|
+
// BOTH vars honoured (belt and braces): a test helper once stripped all
|
|
240
|
+
// IAPEER_MEMORY_* env before spawning the CLI — the ecosystem-wide
|
|
241
|
+
// IAPEER_TEST_SANDBOX survives generic prefix-stripping.
|
|
242
|
+
if (
|
|
243
|
+
process.env.IAPEER_MEMORY_SUPPRESS_IAP_SEND === "1" ||
|
|
244
|
+
process.env.IAPEER_TEST_SANDBOX === "1"
|
|
245
|
+
) {
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
suppressed: true,
|
|
249
|
+
detail: "iap send suppressed (test sandbox)",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
86
252
|
const bin = opts.iapeerBin ?? "iapeer";
|
|
87
253
|
let proc: ReturnType<typeof Bun.spawnSync>;
|
|
88
254
|
try {
|
|
89
255
|
proc = Bun.spawnSync(
|
|
90
|
-
[bin, "send", "watcher", "--from", opts.
|
|
256
|
+
[bin, "send", opts.to ?? "watcher", "--from", opts.fromIdentity, "--message", opts.message],
|
|
91
257
|
{ stdout: "pipe", stderr: "pipe" },
|
|
92
258
|
);
|
|
93
259
|
} catch (err) {
|
|
@@ -108,55 +274,97 @@ function iapSend(opts: {
|
|
|
108
274
|
|
|
109
275
|
export function registerWatcher(opts: {
|
|
110
276
|
launcherPath: string;
|
|
111
|
-
|
|
277
|
+
/** Registrant PERSONALITY (owner of the durable trigger + default target). */
|
|
278
|
+
registrant?: string;
|
|
279
|
+
/** Runtime prefix of the registrant's identity (claude for role peers). */
|
|
280
|
+
runtime?: string;
|
|
112
281
|
target?: string;
|
|
113
282
|
id?: string;
|
|
114
283
|
iapeerBin?: string;
|
|
115
284
|
}): IapSendResult {
|
|
285
|
+
const registrant = opts.registrant ?? DEFAULT_REGISTRANT;
|
|
116
286
|
return iapSend({
|
|
117
|
-
|
|
287
|
+
fromIdentity: fromIdentity(registrant, opts.runtime),
|
|
118
288
|
iapeerBin: opts.iapeerBin,
|
|
119
289
|
message: registrationMessage({
|
|
120
290
|
script: opts.launcherPath,
|
|
121
|
-
|
|
291
|
+
// No fallback to the registrant here: the EVENT target default
|
|
292
|
+
// (copywriter, inverted pipeline) lives in registrationMessage.
|
|
293
|
+
target: opts.target,
|
|
122
294
|
id: opts.id,
|
|
123
295
|
}),
|
|
124
296
|
});
|
|
125
297
|
}
|
|
126
298
|
|
|
127
299
|
export function unregisterWatcher(opts: {
|
|
128
|
-
|
|
300
|
+
registrant?: string;
|
|
301
|
+
runtime?: string;
|
|
129
302
|
id?: string;
|
|
130
303
|
iapeerBin?: string;
|
|
131
304
|
}): IapSendResult {
|
|
132
305
|
return iapSend({
|
|
133
|
-
|
|
306
|
+
fromIdentity: fromIdentity(opts.registrant ?? DEFAULT_REGISTRANT, opts.runtime),
|
|
134
307
|
iapeerBin: opts.iapeerBin,
|
|
135
308
|
message: JSON.stringify({ cmd: "unregister", id: opts.id ?? WATCHER_TRIGGER_ID }),
|
|
136
309
|
});
|
|
137
310
|
}
|
|
138
311
|
|
|
312
|
+
/** Register a timer trigger (sweep/dream) — body built by *TimerMessage(). */
|
|
313
|
+
export function registerTimer(opts: {
|
|
314
|
+
message: string;
|
|
315
|
+
registrant?: string;
|
|
316
|
+
runtime?: string;
|
|
317
|
+
iapeerBin?: string;
|
|
318
|
+
}): IapSendResult {
|
|
319
|
+
return iapSend({
|
|
320
|
+
to: "timer",
|
|
321
|
+
fromIdentity: fromIdentity(opts.registrant ?? DEFAULT_REGISTRANT, opts.runtime),
|
|
322
|
+
iapeerBin: opts.iapeerBin,
|
|
323
|
+
message: opts.message,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function unregisterTimer(opts: {
|
|
328
|
+
id: string;
|
|
329
|
+
registrant?: string;
|
|
330
|
+
runtime?: string;
|
|
331
|
+
iapeerBin?: string;
|
|
332
|
+
}): IapSendResult {
|
|
333
|
+
return iapSend({
|
|
334
|
+
to: "timer",
|
|
335
|
+
fromIdentity: fromIdentity(opts.registrant ?? DEFAULT_REGISTRANT, opts.runtime),
|
|
336
|
+
iapeerBin: opts.iapeerBin,
|
|
337
|
+
message: JSON.stringify({ cmd: "unregister", id: opts.id }),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
139
341
|
export type WatcherTrigger = {
|
|
342
|
+
/** Durable role token: "event" (watcher) | "time" (timer) — notifier fact. */
|
|
140
343
|
role: string;
|
|
141
344
|
id: string;
|
|
142
345
|
owner: string;
|
|
143
346
|
target?: string;
|
|
144
347
|
script?: string;
|
|
348
|
+
/** Timer-only: the check-gate script path. */
|
|
349
|
+
check?: string;
|
|
145
350
|
heartbeatSec?: number;
|
|
146
351
|
topic?: string;
|
|
147
352
|
};
|
|
148
353
|
|
|
149
354
|
/**
|
|
150
|
-
* Read
|
|
355
|
+
* Read a durable trigger from the registrant's peer profile — the
|
|
151
356
|
* canonical storage contract (sanctioned). Never throws.
|
|
152
357
|
*/
|
|
153
358
|
export function readWatcherTrigger(opts: {
|
|
154
359
|
registrantCwd: string;
|
|
155
360
|
id?: string;
|
|
156
361
|
owner?: string;
|
|
362
|
+
/** "event" (default, watcher triggers) or "time" (timers). */
|
|
363
|
+
role?: "event" | "time";
|
|
157
364
|
}): WatcherTrigger | null {
|
|
158
365
|
const id = opts.id ?? WATCHER_TRIGGER_ID;
|
|
159
366
|
const owner = opts.owner ?? "index";
|
|
367
|
+
const role = opts.role ?? "event";
|
|
160
368
|
try {
|
|
161
369
|
const profile = JSON.parse(
|
|
162
370
|
fs.readFileSync(
|
|
@@ -166,7 +374,7 @@ export function readWatcherTrigger(opts: {
|
|
|
166
374
|
) as { notifier?: { triggers?: WatcherTrigger[] } };
|
|
167
375
|
const triggers = profile.notifier?.triggers ?? [];
|
|
168
376
|
return (
|
|
169
|
-
triggers.find((t) => t.role ===
|
|
377
|
+
triggers.find((t) => t.role === role && t.id === id && t.owner === owner) ??
|
|
170
378
|
null
|
|
171
379
|
);
|
|
172
380
|
} catch {
|