@staix/agent-mem 0.22.0 → 0.22.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/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/package.json +5 -2
- package/src/cli-agent-reads.ts +3 -1
- package/src/cli.ts +2 -0
- package/src/memory/generation.ts +6 -3
- package/src/memory/profile-lock-diagnostic.ts +69 -0
- package/src/memory/reader-lease.ts +3 -2
- package/src/memory/shard.ts +4 -0
- package/src/memory/sync-placement-guard.ts +6 -2
- package/src/setup-toml.ts +3 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Curated memory with append-only JSONL logs, a derived SQLite index, short- and long-term tiers, a typed graph, goals with leases, and git sync between machines. Extracted from [agent-hub](https://github.com/STAIxBWLB/agent-hub), with compatible storage and entry IDs.
|
|
4
4
|
|
|
5
|
-
Version: 0.22.
|
|
5
|
+
Version: 0.22.1. Requires Bun 1.4.2 or later; no runtime dependencies.
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@staix/agent-mem",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "Curated, shared memory for coding agents: append-only logs, a derived index, tiers and a typed graph.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
"@staix/agent-mem-v013": "npm:@staix/agent-mem@0.13.0",
|
|
24
24
|
"@staix/agent-mem-v015": "npm:@staix/agent-mem@0.15.0",
|
|
25
25
|
"@staix/agent-mem-v017": "npm:@staix/agent-mem@0.17.0",
|
|
26
|
-
"@staix/agent-mem-v018": "npm:@staix/agent-mem@0.18.0"
|
|
26
|
+
"@staix/agent-mem-v018": "npm:@staix/agent-mem@0.18.0",
|
|
27
|
+
"bun-types-host-1-4-2": "npm:bun-types@1.4.2",
|
|
28
|
+
"@staix/agent-mem-v0220": "npm:@staix/agent-mem@0.22.0",
|
|
29
|
+
"@types/bun-host-1-4-2": "npm:@types/bun@1.4.2"
|
|
27
30
|
},
|
|
28
31
|
"exports": {
|
|
29
32
|
".": "./src/index.ts",
|
package/src/cli-agent-reads.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { noteDisplayActor } from "./memory/cli-attribution.ts";
|
|
2
2
|
import { singleLine } from "./memory/style.ts";
|
|
3
3
|
import { createMemoryMcp } from "./mcp-memory.ts";
|
|
4
|
+
import { safeProfileLockMessage } from "./memory/profile-lock-diagnostic.ts";
|
|
4
5
|
import { memoryHome } from "./memory/home.ts";
|
|
5
6
|
import type { ParsedCommand } from "./commands-table.ts";
|
|
6
7
|
|
|
@@ -111,7 +112,8 @@ export async function agentCliRead(
|
|
|
111
112
|
return shown;
|
|
112
113
|
}
|
|
113
114
|
return output;
|
|
114
|
-
} catch {
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if(safeProfileLockMessage(error))throw error;
|
|
115
117
|
throw new Error(UNAVAILABLE);
|
|
116
118
|
} finally {
|
|
117
119
|
try { memory?.close(); } catch { /* close failures do not expose partial reads */ }
|
package/src/cli.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { createRecallStalenessProbe } from "./memory/recall-staleness.ts";
|
|
|
12
12
|
import { recordMemoryUse } from "./memory/use-telemetry.ts";
|
|
13
13
|
import { DecayApplyError } from "./memory/decay-review.ts";
|
|
14
14
|
import { createMemoryMcp } from "./mcp-memory.ts";
|
|
15
|
+
import { safeProfileLockMessage } from "./memory/profile-lock-diagnostic.ts";
|
|
15
16
|
import { MCP_TOOLS, serveMcp, writeConfirmed } from "./mcp.ts";
|
|
16
17
|
import { createStandalonePolicy } from "./memory/policy.ts";
|
|
17
18
|
import { configuredPolicy, configuredPolicyOptions, loadAmemConfig, repoConfigRoot } from "./memory/config.ts";
|
|
@@ -314,6 +315,7 @@ export async function runCli(argv: readonly string[], context: CliContext = {}):
|
|
|
314
315
|
}
|
|
315
316
|
// Data-free errors (decided before any store or file is opened) keep their own message.
|
|
316
317
|
if (marked && error instanceof AgentUsageError) throw error;
|
|
318
|
+
const recovery=marked?safeProfileLockMessage(error):undefined;if(recovery)throw new Error(recovery);
|
|
317
319
|
if (marked) {
|
|
318
320
|
// parseCommand is pure: when it rejects argv, the error it raised is a usage error, not memory data.
|
|
319
321
|
let usage: Error | undefined, parsed: ReturnType<typeof parseCommand> | undefined;
|
package/src/memory/generation.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { dirname, join } from "node:path";
|
|
|
9
9
|
import { canonicalPath, processLiveness, processSignature, realPath } from "./platform.ts";
|
|
10
10
|
import { shardDir, userShardDir } from "./shard.ts";
|
|
11
11
|
import { readPublicPruneInputSetChain } from "./public-prune-input-chain.ts";
|
|
12
|
+
import { profileLockRecoveryError, safeProfileLockMessage } from "./profile-lock-diagnostic.ts";
|
|
12
13
|
|
|
13
14
|
export interface BoundProfile {
|
|
14
15
|
kind: "shared-v1" | "generation";
|
|
@@ -96,12 +97,14 @@ export function readSelector(memoryDir: string, absoluteDeadline?: number): { se
|
|
|
96
97
|
const current = readProfileJSON(join(memoryDir, "profile.json"));
|
|
97
98
|
if (result.revision === (current === undefined ? "absent" : hashJSON(current))) return result;
|
|
98
99
|
} catch (error) {
|
|
99
|
-
if (error
|
|
100
|
-
if (error instanceof
|
|
100
|
+
if (safeProfileLockMessage(error)) throw error;
|
|
101
|
+
if (error instanceof SyncPlacementRuntimeError) throw profileLockRecoveryError(memoryDir) ?? error;
|
|
102
|
+
if (error instanceof Error && error.message === "public prune view unavailable") throw profileLockRecoveryError(memoryDir) ?? error; // A spent wait budget is never restarted.
|
|
101
103
|
if (!lstatSync(join(memoryDir, PUBLIC_PRUNE_INTENT), { throwIfNoEntry: false }) && !lstatSync(join(memoryDir, "profile.lock"), { throwIfNoEntry: false })) throw error;
|
|
102
|
-
waitForPublicPruneWriter(memoryDir, deadline);
|
|
104
|
+
try { waitForPublicPruneWriter(memoryDir, deadline); } catch (failure) { throw profileLockRecoveryError(memoryDir) ?? failure; }
|
|
103
105
|
}
|
|
104
106
|
}
|
|
107
|
+
const diagnostic=profileLockRecoveryError(memoryDir);if(diagnostic)throw diagnostic;
|
|
105
108
|
return unavailable();
|
|
106
109
|
}
|
|
107
110
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Read-only error hints, never admission, recovery or proof that an owner is dead. */
|
|
2
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, opendirSync, readSync, type BigIntStats } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { realPath } from "./platform.ts";
|
|
5
|
+
|
|
6
|
+
const messages={
|
|
7
|
+
shards:"An interrupted or still-running person shard migration retains profile.lock; have the person finish it or run amem migrate --shards --resume.",
|
|
8
|
+
prune:"An interrupted or still-running person public-prune view migration retains profile.lock; have the person finish it or run amem migrate --public-prune-view --resume.",
|
|
9
|
+
activation:"An interrupted or still-running person sync-placement activation retains profile.lock; have the person run amem migrate --sync-placement --resume, or amem migrate --sync-placement --abandon only while both markers retain their original v2 bytes.",
|
|
10
|
+
renewal:"An interrupted or still-running person writer-inventory renewal retains profile.lock; have the person run amem migrate --sync-placement --resume, or amem migrate --sync-placement --abandon only before the first commit-log byte.",
|
|
11
|
+
staging:"An interrupted or still-running person writer-inventory renewal retains profile.lock and unpublished intent staging; have the person run amem migrate --sync-placement --abandon, then amem migrate --sync-placement --dry-run before another apply.",
|
|
12
|
+
} as const;
|
|
13
|
+
type Family=keyof typeof messages;
|
|
14
|
+
const recognised=new WeakMap<object,string>();
|
|
15
|
+
/** A privacy-safe exception carries only one of our fixed messages, never a caught message or path. */
|
|
16
|
+
export function rememberProfileLockError(error:object,text:string):void {
|
|
17
|
+
if((Object.values(messages) as readonly string[]).includes(text))recognised.set(error,text);
|
|
18
|
+
}
|
|
19
|
+
export function safeProfileLockMessage(error:unknown):string|undefined {
|
|
20
|
+
return error!==null&&(typeof error==="object"||typeof error==="function")?recognised.get(error):undefined;
|
|
21
|
+
}
|
|
22
|
+
const same=(a:BigIntStats,b:BigIntStats)=>a.dev===b.dev&&a.ino===b.ino&&a.uid===b.uid&&a.mode===b.mode&&a.nlink===b.nlink&&a.size===b.size&&a.mtimeNs===b.mtimeNs&&a.ctimeNs===b.ctimeNs;
|
|
23
|
+
function owned(s:BigIntStats,limit:number,privateFile=true):void {
|
|
24
|
+
if(!s.isFile()||s.isSymbolicLink()||s.uid!==BigInt(process.getuid?.()??-1)||s.nlink!==1n||(s.mode&(privateFile?0o077n:0o022n))!==0n||s.size>BigInt(limit))throw Error();
|
|
25
|
+
}
|
|
26
|
+
const object=(v:unknown):v is Record<string,unknown>=>!!v&&typeof v==="object"&&!Array.isArray(v);
|
|
27
|
+
const hex=(v:unknown,width:number)=>typeof v==="string"&&v.length===width&&!/[^a-f0-9]/.test(v);
|
|
28
|
+
/** Only called after an existing refusal. Absent lock means no extra marker/intent read at all.
|
|
29
|
+
* ponytail: fixed metadata limits plus20000 home names/256 staging files; a reviewed bounded descriptor inventory is needed to grow this hint. */
|
|
30
|
+
export function profileLockRefusal(home:string):string|undefined {
|
|
31
|
+
try{
|
|
32
|
+
const checks:(()=>void)[]=[];
|
|
33
|
+
const read=(name:string,limit:number,privateFile=true):Record<string,unknown>|undefined=>{
|
|
34
|
+
const file=join(home,name),before=lstatSync(file,{bigint:true,throwIfNoEntry:false});
|
|
35
|
+
if(!before){checks.push(()=>{if(lstatSync(file,{throwIfNoEntry:false}))throw Error();});return;}
|
|
36
|
+
owned(before,limit,privateFile);const fd=openSync(file,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);
|
|
37
|
+
try{
|
|
38
|
+
const s=fstatSync(fd,{bigint:true});if(!same(before,s))throw Error();
|
|
39
|
+
const bytes=Buffer.alloc(Number(s.size)+1);let count=0;
|
|
40
|
+
while(count<bytes.length){const n=readSync(fd,bytes,count,bytes.length-count,count);if(!n)break;count+=n;}
|
|
41
|
+
if(count!==Number(s.size)||!same(s,fstatSync(fd,{bigint:true}))||!same(s,lstatSync(file,{bigint:true})))throw Error();
|
|
42
|
+
const value:unknown=JSON.parse(new TextDecoder("utf-8",{fatal:true}).decode(bytes.subarray(0,count)));if(!object(value))throw Error();
|
|
43
|
+
checks.push(()=>{if(!same(s,lstatSync(file,{bigint:true})))throw Error();});return value;
|
|
44
|
+
}finally{closeSync(fd);}
|
|
45
|
+
};
|
|
46
|
+
const owner=read("profile.lock",4096);if(!owner)return;
|
|
47
|
+
if(Object.keys(owner).sort().join()!=="pid,signature,token"||!Number.isSafeInteger(owner.pid)||Number(owner.pid)<=0||!hex(owner.signature,64)||!hex(owner.token,32))return;
|
|
48
|
+
const canonical=realPath(home),profile=read("profile.json",262144,false),shards=read("shards.json",262144,false);
|
|
49
|
+
const prune=read("public-prune-view.intent.json",1048576),activation=read("sync-placement-activation.json",1048576),renewal=read("sync-placement-writer-renewal.json",1048576);
|
|
50
|
+
const families=new Set<Family>();
|
|
51
|
+
if(prune){if(prune.schema!=="agent-mem.public-prune-activation/v1"&&prune.schema!=="agent-mem.public-prune-advance/v1"||prune.home!==canonical||!hex(prune.generation,32))return;families.add("prune");}
|
|
52
|
+
if(activation){if(activation.schema!=="agent-mem.sync-placement-activation/v1"||activation.home!==canonical||!object(activation.epoch)||!hex(activation.epoch.generation,32))return;families.add("activation");}
|
|
53
|
+
if(renewal){if(renewal.schema!=="agent-mem.sync-placement-writer-renewal/v1"||renewal.home!==canonical||!object(renewal.revision)||renewal.revision.schema!=="agent-mem.sync-placement-writer-revision/v1"||!hex(renewal.revision.generation,32)||!hex(renewal.revision.epochHash,64))return;families.add("renewal");}
|
|
54
|
+
if(shards?.schema==="agent-mem.shards/v1"&&shards.phase==="captured"&&hex(shards.generation,32))families.add("shards");
|
|
55
|
+
if(!families.size&&profile?.schema==="agent-mem.profile/v3"&&shards?.schema==="agent-mem.shards/v3"&&shards.phase==="active"&&hex(profile.generation,32)&&shards.generation===profile.generation&&hex(profile.placementHead,64)&&profile.placementHead===shards.placementHead){
|
|
56
|
+
const reader=opendirSync(home);let names=0,staging=0,nameBytes=0;
|
|
57
|
+
try{for(;;){const entry=reader.readSync();if(!entry)break;if(++names>20000||(nameBytes+=Buffer.byteLength(entry.name))>8*1024*1024)throw Error();
|
|
58
|
+
const prefix="sync-placement-writer-renewal.json.pending-";if(!entry.name.startsWith(prefix)||!hex(entry.name.slice(prefix.length),64))continue;
|
|
59
|
+
if(++staging>256)throw Error();const file=join(home,entry.name),s=lstatSync(file,{bigint:true});owned(s,1048576);checks.push(()=>{if(!same(s,lstatSync(file,{bigint:true})))throw Error();});
|
|
60
|
+
}}finally{reader.closeSync();}
|
|
61
|
+
if(staging)families.add("staging");
|
|
62
|
+
}
|
|
63
|
+
for(const check of checks)check();if(families.size!==1)return;
|
|
64
|
+
return messages[[...families][0]!];
|
|
65
|
+
}catch{return undefined;}
|
|
66
|
+
}
|
|
67
|
+
export function profileLockRecoveryError(home:string):Error|undefined {
|
|
68
|
+
const text=profileLockRefusal(home);if(!text)return;const error=new Error(text);error.name="ProfileLockMigrationError";rememberProfileLockError(error,text);return error;
|
|
69
|
+
}
|
|
@@ -4,6 +4,7 @@ import { readProfileJSON, writeProfileJSON } from "./generation.ts";
|
|
|
4
4
|
import { readerAdmissionDatabase } from "./mutation.ts";
|
|
5
5
|
import { processLiveness, processSignature } from "./platform.ts";
|
|
6
6
|
import { waitForPublicPruneWriter } from "./public-prune-view.ts";
|
|
7
|
+
import { profileLockRecoveryError, safeProfileLockMessage } from "./profile-lock-diagnostic.ts";
|
|
7
8
|
|
|
8
9
|
const READER_LEASE_SCHEMA = "agent-mem.reader-lease/v1";
|
|
9
10
|
const RENEW_MS = 25_000;
|
|
@@ -52,8 +53,8 @@ function admitReader(memoryDir: string): void {
|
|
|
52
53
|
// Even a malformed or stale marker refuses admission; only the profile-lock owner may reclaim it.
|
|
53
54
|
try {
|
|
54
55
|
waitForPublicPruneWriter(memoryDir);
|
|
55
|
-
if (lstatSync(join(memoryDir, "profile.lock"), { throwIfNoEntry: false })) throw new Error("profile lock prevents reader admission; finish or resume the migration with amem migrate --resume");
|
|
56
|
-
} catch { throw new Error("profile lock prevents reader admission; finish or resume the migration with amem migrate --resume"); }
|
|
56
|
+
if (lstatSync(join(memoryDir, "profile.lock"), { throwIfNoEntry: false })) throw profileLockRecoveryError(memoryDir) ?? new Error("profile lock prevents reader admission; finish or resume the migration with amem migrate --resume");
|
|
57
|
+
} catch (error) { if(safeProfileLockMessage(error))throw error;throw profileLockRecoveryError(memoryDir) ?? new Error("profile lock prevents reader admission; finish or resume the migration with amem migrate --resume"); }
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/** One lease per process/home/namespace, reference-counted across handles.
|
package/src/memory/shard.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { existsSync, linkSync, lstatSync, mkdirSync, readFileSync, rmSync, write
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { canonicalJSON, generationDescriptor, hashJSON, readProfileJSON, readSelector, validGeneration, PROFILE_LOCK_TIMEOUT_MS, type BoundProfile, type GenerationSeal } from "./generation.ts";
|
|
10
10
|
import { SHARD_SCHEMA, resolveAlias } from "./aliases.ts";
|
|
11
|
+
import { profileLockRecoveryError, safeProfileLockMessage } from "./profile-lock-diagnostic.ts";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* #15 T1: the per-project shard layout under the user home. One shard per project key at `projects/<key-hash>/`;
|
|
@@ -126,6 +127,7 @@ function shardSnapshot(memoryDir: string, deadline: number) {
|
|
|
126
127
|
let selector: ReturnType<typeof readSelector>;
|
|
127
128
|
try { selector = readSelector(memoryDir, deadline); }
|
|
128
129
|
catch (error) {
|
|
130
|
+
if(safeProfileLockMessage(error))throw error;
|
|
129
131
|
if (error instanceof SyncPlacementRuntimeError) throw error;
|
|
130
132
|
// The writer's spent wait budget or unavailable proof is never restarted here.
|
|
131
133
|
if (error instanceof Error && error.message === "public prune view unavailable") throw error;
|
|
@@ -145,8 +147,10 @@ function shardSnapshot(memoryDir: string, deadline: number) {
|
|
|
145
147
|
throw new Error("curated shard selector changed");
|
|
146
148
|
}
|
|
147
149
|
export function readShardedSelection(memoryDir: string): ShardedSelection {
|
|
150
|
+
try {
|
|
148
151
|
const { journal } = shardSnapshot(memoryDir, Date.now() + PROFILE_LOCK_TIMEOUT_MS);
|
|
149
152
|
return { schema: journal.schema, generation: journal.generation, phase: "active", selectorRevision: journal.selectorRevision };
|
|
153
|
+
}catch(error){if(safeProfileLockMessage(error))throw error;throw profileLockRecoveryError(memoryDir)??error;}
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
/** Fixed curated paths cannot redirect writes into legacy inputs or another shard.
|
|
@@ -7,13 +7,14 @@ import { LOCAL_METADATA_MAX_BYTES, legacyProfileMarkerShape, type BoundProfile }
|
|
|
7
7
|
import { legacyShardMarkerShape } from "./shard.ts";
|
|
8
8
|
import { PLACEMENT_PROFILE_SCHEMA, PLACEMENT_SHARDS_SCHEMA, validatePlacementProfile, validatePlacementShards } from "./sync-placement-format.ts";
|
|
9
9
|
import { PLACEMENT_ACTIVATION_INTENT } from "./sync-placement-activation-path.ts";
|
|
10
|
+
import { profileLockRefusal, rememberProfileLockError } from "./profile-lock-diagnostic.ts";
|
|
10
11
|
|
|
11
12
|
export const PLACEMENT_STAGE1_REFUSAL = "sync placement v3 requires matching durable activation metadata; keep the current compatible profile active until person-authorized activation is complete";
|
|
12
13
|
export const PLACEMENT_MARKER_REFUSAL = "sync placement metadata is invalid or unsupported; person inspection and a compatible reader are required before continuing";
|
|
13
14
|
export const PLACEMENT_ACTIVATION_PENDING = "sync placement activation is pending or incomplete; have the person inspect retained evidence and run amem migrate --sync-placement --resume, or --abandon only while both markers retain their original v2 bytes";
|
|
14
15
|
export class SyncPlacementRuntimeError extends Error {
|
|
15
16
|
readonly code: "sync_placement_not_enabled" | "sync_placement_unavailable";
|
|
16
|
-
constructor(reason: string) { super(reason); this.name = "SyncPlacementRuntimeError"; this.code = reason === PLACEMENT_STAGE1_REFUSAL ? "sync_placement_not_enabled" : "sync_placement_unavailable"; }
|
|
17
|
+
constructor(reason: string) { super(reason); this.name = "SyncPlacementRuntimeError"; this.code = reason === PLACEMENT_STAGE1_REFUSAL ? "sync_placement_not_enabled" : "sync_placement_unavailable";rememberProfileLockError(this,reason); }
|
|
17
18
|
}
|
|
18
19
|
class MarkerChanged extends Error {}
|
|
19
20
|
const same = (left: BigIntStats, right: BigIntStats) => left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
@@ -65,6 +66,9 @@ export function placementMarkerRefusal(profile: unknown, shards: unknown): strin
|
|
|
65
66
|
/** Selector preflight only recognizes v3. Legacy malformed/split metadata must retain its existing reader/recovery path. */
|
|
66
67
|
// This narrow probe is not a data/transport gate; effects must still use the full runtime guard.
|
|
67
68
|
export function placementSelectorRefusal(home: string): string | undefined {
|
|
69
|
+
const refusal=selectorRefusal(home);return refusal?profileLockRefusal(home)??refusal:undefined;
|
|
70
|
+
}
|
|
71
|
+
function selectorRefusal(home:string):string|undefined {
|
|
68
72
|
// A durable placement fence forbids falling back to raw legacy sync after marker rollback.
|
|
69
73
|
const fenced = placementFencePresent(home);
|
|
70
74
|
const pending = placementActivationPending(home);
|
|
@@ -100,5 +104,5 @@ export function placementRuntimeRefusal(home: string, bound?: Pick<BoundProfile,
|
|
|
100
104
|
catch { return PLACEMENT_MARKER_REFUSAL; }
|
|
101
105
|
}
|
|
102
106
|
export function assertPlacementRuntime(home: string): void {
|
|
103
|
-
const refusal = placementRuntimeRefusal(home); if (refusal) throw new SyncPlacementRuntimeError(refusal);
|
|
107
|
+
const refusal = placementRuntimeRefusal(home); if (refusal) throw new SyncPlacementRuntimeError(profileLockRefusal(home)??refusal);
|
|
104
108
|
}
|
package/src/setup-toml.ts
CHANGED
|
@@ -37,7 +37,7 @@ function inline(value: unknown): string {
|
|
|
37
37
|
// Bun emits scalars and many arrays as one assignment. Reuse that literal when it is safe in this scope.
|
|
38
38
|
// Objects/AOTs may become headers; those need the inline form below to avoid changing subsequent table scope.
|
|
39
39
|
let rendered = "";
|
|
40
|
-
try { rendered = Bun.TOML.stringify({ setup_value: value }); } catch { /* The validated inline encoder covers values the built-in writer cannot emit. */ }
|
|
40
|
+
try { rendered = Bun.TOML.stringify({ setup_value: value }) ?? ""; } catch { /* The validated inline encoder covers values the built-in writer cannot emit. */ }
|
|
41
41
|
const literal = /^setup_value\s*=\s*([^\r\n]*)\r?\n?$/.exec(rendered)?.[1];
|
|
42
42
|
if (literal !== undefined && isDeepStrictEqual(shape(parse(rendered).setup_value), shape(value))) return literal;
|
|
43
43
|
const encode = (part: unknown): string => {
|
|
@@ -255,7 +255,8 @@ function arrayEdit(before: readonly unknown[], after: readonly unknown[]): { kee
|
|
|
255
255
|
function appendedTables(path: readonly string[], values: readonly unknown[], eol: string): string {
|
|
256
256
|
if (!values.every(plain)) return error();
|
|
257
257
|
const document: Record<string, unknown> = {}; edit(document, path, [...values]);
|
|
258
|
-
let source: string; try { source = Bun.TOML.stringify(document); } catch { return error(); }
|
|
258
|
+
let source: string | undefined; try { source = Bun.TOML.stringify(document); } catch { return error(); }
|
|
259
|
+
if (source === undefined) return error();
|
|
259
260
|
const sourceUnits = statements(source), rows = sourceUnits.filter(unit => prefix(path, unit.path));
|
|
260
261
|
if (rows.filter(unit => unit.header && unit.array && unit.path.length === path.length).length !== values.length) return error();
|
|
261
262
|
return rows.map(unit => source.slice(unit.start, unit.end)).join("").replace(/\r?\n/g, eol);
|