@agfpd/iapeer-memory-core 0.1.12 → 0.2.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/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/memoryd.ts +146 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agfpd/iapeer-memory-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "iapeer-memory core — host-neutral TypeScript memory primitive: vault schema/taxonomy config, search engine, memoryd, context renderer, role contracts. Consumed by the @agfpd/iapeer-memory facade; version kept in lockstep by its release flow (docs/10-distribution.md).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/index.ts
CHANGED
package/src/memoryd.ts
CHANGED
|
@@ -49,6 +49,15 @@ import {
|
|
|
49
49
|
type VaultSnapshot,
|
|
50
50
|
} from "./permanent-detect.js";
|
|
51
51
|
import { makeLogger, type Logger } from "./log.js";
|
|
52
|
+
import {
|
|
53
|
+
atomicWrite,
|
|
54
|
+
buildOutput,
|
|
55
|
+
collectNotes,
|
|
56
|
+
filterAgentNotes,
|
|
57
|
+
fullIndexPathFor,
|
|
58
|
+
type RenderContext,
|
|
59
|
+
} from "./index-render.js";
|
|
60
|
+
import { renderPeerFragment, type FragmentEnv } from "./context-render.js";
|
|
52
61
|
|
|
53
62
|
// ── identity ────────────────────────────────────────────────────────────────
|
|
54
63
|
|
|
@@ -497,8 +506,37 @@ export type MemorydOptions = {
|
|
|
497
506
|
* omit to use `config.mcp.port` (the configured default, ADR-012).
|
|
498
507
|
*/
|
|
499
508
|
mcpPort?: number | null;
|
|
509
|
+
/** Continuous per-peer fragment rendering (docs/05: «триггер регенерации —
|
|
510
|
+
* debounce от FS-изменений vault в memoryd»). Omit → rendering off
|
|
511
|
+
* (host-neutral core: the ecosystem wiring comes from the package). */
|
|
512
|
+
fragments?: MemorydFragmentsWiring;
|
|
500
513
|
};
|
|
501
514
|
|
|
515
|
+
/**
|
|
516
|
+
* Fleet fragment wiring — assembled by the PACKAGE (ADR-009: ecosystem
|
|
517
|
+
* joints live there), consumed by core as plain data. The fleet map is a
|
|
518
|
+
* JSON state file (`{peers: [{personality, cwd}]}`) written by the package
|
|
519
|
+
* from `iapeer list --json` (init/update/verify --repair — the registry cwd
|
|
520
|
+
* is the FACT, iapeer 0.2.14); core only reads it, fail-open: missing or
|
|
521
|
+
* malformed map = rendering quietly off (дыра 10.06: контракт docs/05 был
|
|
522
|
+
* обещан шапкой render.ts и не подключён — пиры после рестарта не знали
|
|
523
|
+
* пути записи vault).
|
|
524
|
+
*/
|
|
525
|
+
export type MemorydFragmentsWiring = {
|
|
526
|
+
/** Fleet map JSON path (package state namespace). */
|
|
527
|
+
fleetMapPath: string;
|
|
528
|
+
/** paths-block facts for every fragment (vault/db/config/state/cache/logs). */
|
|
529
|
+
paths: FragmentEnv["paths"];
|
|
530
|
+
/** Author-index target (capped variant) for an agent; `-full` derived. */
|
|
531
|
+
authorIndexPathFor: (agent: string) => string;
|
|
532
|
+
/** Index curator personality (its branch adds the tags dictionary). */
|
|
533
|
+
indexAgent?: string;
|
|
534
|
+
/** ADR-014 projects root for `dir:` resolution. */
|
|
535
|
+
projectsRoot?: string;
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
export type FleetPeer = { personality: string; cwd: string };
|
|
539
|
+
|
|
502
540
|
export type MemorydHandle = {
|
|
503
541
|
mcpPort: number | null;
|
|
504
542
|
/** Force one detect pass immediately (used by tests and shutdown flush). */
|
|
@@ -571,6 +609,94 @@ export async function startMemoryd(opts: MemorydOptions): Promise<MemorydHandle>
|
|
|
571
609
|
* last_edited_by читается ИЗ ФАЙЛА на момент пачки — правки
|
|
572
610
|
* index/scriber/dreamweaver в пачку не попадают (эхо-брейкер механикой,
|
|
573
611
|
* не доктриной). */
|
|
612
|
+
// ── per-peer fragments (docs/05: свежесть за секунды от FS-изменений) ──
|
|
613
|
+
const fragments = opts.fragments ?? null;
|
|
614
|
+
let fleetCache: { mtimeMs: number; peers: FleetPeer[] } | null = null;
|
|
615
|
+
|
|
616
|
+
/** Fail-open fleet map reader with an mtime cache: missing/malformed file
|
|
617
|
+
* → empty fleet (rendering quietly off), never throws. */
|
|
618
|
+
function readFleetMap(): FleetPeer[] {
|
|
619
|
+
if (!fragments) return [];
|
|
620
|
+
try {
|
|
621
|
+
const st = fs.statSync(fragments.fleetMapPath);
|
|
622
|
+
if (fleetCache && fleetCache.mtimeMs === st.mtimeMs) return fleetCache.peers;
|
|
623
|
+
const raw = JSON.parse(fs.readFileSync(fragments.fleetMapPath, "utf-8")) as {
|
|
624
|
+
peers?: Array<{ personality?: unknown; cwd?: unknown }>;
|
|
625
|
+
};
|
|
626
|
+
const peers: FleetPeer[] = (Array.isArray(raw?.peers) ? raw.peers : [])
|
|
627
|
+
.filter(
|
|
628
|
+
(p): p is { personality: string; cwd: string } =>
|
|
629
|
+
typeof p?.personality === "string" &&
|
|
630
|
+
p.personality.trim() !== "" &&
|
|
631
|
+
typeof p?.cwd === "string" &&
|
|
632
|
+
p.cwd.trim() !== "",
|
|
633
|
+
)
|
|
634
|
+
.map((p) => ({ personality: p.personality.trim(), cwd: p.cwd.trim() }));
|
|
635
|
+
fleetCache = { mtimeMs: st.mtimeMs, peers };
|
|
636
|
+
return peers;
|
|
637
|
+
} catch {
|
|
638
|
+
fleetCache = null;
|
|
639
|
+
return [];
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** One render pass over the whole fleet: collectNotes ONCE (the full
|
|
644
|
+
* vault scan is the expensive part), then filter/build/write per peer.
|
|
645
|
+
* Per-peer failures are isolated; a peer whose cwd is gone is skipped
|
|
646
|
+
* (never scaffold directories for a removed peer). */
|
|
647
|
+
function renderFleetFragments(reason: string): void {
|
|
648
|
+
if (!fragments || !vaultAvailable()) return;
|
|
649
|
+
const peers = readFleetMap();
|
|
650
|
+
if (!peers.length) return;
|
|
651
|
+
const ctx: RenderContext = { taxonomy, ranking: config.ranking };
|
|
652
|
+
let collected: ReturnType<typeof collectNotes>;
|
|
653
|
+
try {
|
|
654
|
+
collected = collectNotes(config.vaultPath, ctx);
|
|
655
|
+
} catch (err) {
|
|
656
|
+
logger.warn(`fragments: vault collect failed (${String(err)}) — pass skipped`);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const indexAgent = fragments.indexAgent ?? "index";
|
|
660
|
+
let rendered = 0;
|
|
661
|
+
for (const peer of peers) {
|
|
662
|
+
try {
|
|
663
|
+
if (!fs.existsSync(peer.cwd)) continue;
|
|
664
|
+
const mine = filterAgentNotes(collected.notes, collected.incomingCount, peer.personality, ctx);
|
|
665
|
+
const outFile = fragments.authorIndexPathFor(peer.personality);
|
|
666
|
+
fs.mkdirSync(path.dirname(outFile), { recursive: true }); // atomicWrite не создаёт родителя
|
|
667
|
+
const fullOut = fullIndexPathFor(outFile);
|
|
668
|
+
const [text] = buildOutput(mine, peer.personality, {
|
|
669
|
+
ctx,
|
|
670
|
+
projectsRoot: fragments.projectsRoot,
|
|
671
|
+
fullIndexPath: fullOut,
|
|
672
|
+
});
|
|
673
|
+
atomicWrite(outFile, text);
|
|
674
|
+
const [fullText] = buildOutput(mine, peer.personality, {
|
|
675
|
+
ctx,
|
|
676
|
+
projectsRoot: fragments.projectsRoot,
|
|
677
|
+
memoryCap: null,
|
|
678
|
+
canonCap: null,
|
|
679
|
+
projectHardCap: null,
|
|
680
|
+
});
|
|
681
|
+
atomicWrite(fullOut, fullText);
|
|
682
|
+
renderPeerFragment({
|
|
683
|
+
peerCwd: peer.cwd,
|
|
684
|
+
env: {
|
|
685
|
+
agent: peer.personality,
|
|
686
|
+
indexAgent,
|
|
687
|
+
paths: fragments.paths,
|
|
688
|
+
authorIndexPath: outFile,
|
|
689
|
+
tagsDictionaryPath: peer.personality === indexAgent ? tagsMirrorPath : undefined,
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
rendered++;
|
|
693
|
+
} catch (err) {
|
|
694
|
+
logger.warn(`fragments: ${peer.personality} render failed (${String(err)})`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (rendered) logger.info(`fragments: rendered ${rendered} peer fragment(s) (${reason})`);
|
|
698
|
+
}
|
|
699
|
+
|
|
574
700
|
function lastEditedByOf(absPath: string): string | null {
|
|
575
701
|
try {
|
|
576
702
|
const head = fs.readFileSync(absPath, "utf-8").slice(0, 4096);
|
|
@@ -709,6 +835,7 @@ export async function startMemoryd(opts: MemorydOptions): Promise<MemorydHandle>
|
|
|
709
835
|
humanEditPass(changed);
|
|
710
836
|
syncTagsMirror();
|
|
711
837
|
await indexAll({ db, config, logger }); // incremental by content hash
|
|
838
|
+
renderFleetFragments("vault-change"); // docs/05: свежесть за секунды
|
|
712
839
|
// PERMANENT правки НЕ эмитятся мгновенно — копятся до batch-прохода
|
|
713
840
|
// (каденция 6h); см. runPermanentBatch.
|
|
714
841
|
}
|
|
@@ -784,9 +911,27 @@ export async function startMemoryd(opts: MemorydOptions): Promise<MemorydHandle>
|
|
|
784
911
|
}
|
|
785
912
|
}
|
|
786
913
|
touchHeartbeat();
|
|
787
|
-
const heartbeatTimer = setInterval(
|
|
914
|
+
const heartbeatTimer = setInterval(() => {
|
|
915
|
+
touchHeartbeat();
|
|
916
|
+
// Fleet-map change without a vault change (a new peer landed in the
|
|
917
|
+
// registry, the package re-wrote the map) → render the newcomers on
|
|
918
|
+
// the next tick; the mtime cache makes the no-change case a stat().
|
|
919
|
+
if (fragments) {
|
|
920
|
+
try {
|
|
921
|
+
const st = fs.statSync(fragments.fleetMapPath);
|
|
922
|
+
if (st.mtimeMs !== fleetCache?.mtimeMs) renderFleetFragments("fleet-map-change");
|
|
923
|
+
} catch {
|
|
924
|
+
// no map — rendering stays off (fail-open)
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}, heartbeatMs);
|
|
788
928
|
heartbeatTimer.unref?.();
|
|
789
929
|
|
|
930
|
+
// Cold-start coverage (дыра 10.06): render the WHOLE fleet at startup —
|
|
931
|
+
// a memoryd restart after install/update populates every peer's fragment
|
|
932
|
+
// before the fleet's own restarts pick them up.
|
|
933
|
+
renderFleetFragments("startup");
|
|
934
|
+
|
|
790
935
|
// Periodic atomic persist of the detect baseline (only when changed —
|
|
791
936
|
// no churn); a non-graceful exit loses at most one interval.
|
|
792
937
|
let lastPersistedJson: string | null = null;
|