@sema-agent/core 5.0.0 → 5.1.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/CHANGELOG.md +50 -0
- package/dist/agents/repair-loop.js +9 -1
- package/dist/agents/roster-store.d.ts +9 -2
- package/dist/agents/roster-store.js +26 -5
- package/dist/agents/send-message-tool.js +1 -1
- package/dist/agents/subagent.js +15 -5
- package/dist/bin/sema-tb.d.ts +1 -2
- package/dist/bin/sema-tb.js +13 -25
- package/dist/brain/anthropic.js +2 -3
- package/dist/brain/degrading.d.ts +2 -8
- package/dist/brain/degrading.js +3 -3
- package/dist/brain/terminal-cause.js +2 -8
- package/dist/core/hooks.js +4 -4
- package/dist/core/lsp.js +2 -3
- package/dist/core/mcp.d.ts +1 -1
- package/dist/core/mcp.js +2 -1
- package/dist/core/memory-engine/engine.js +2 -2
- package/dist/core/memory-engine/file-backend.js +3 -1
- package/dist/core/memory-engine/layout.d.ts +3 -0
- package/dist/core/memory-engine/layout.js +51 -0
- package/dist/core/memory.d.ts +0 -1
- package/dist/core/memory.js +1 -1
- package/dist/core/permission-rules.d.ts +2 -2
- package/dist/core/permission-rules.js +18 -7
- package/dist/core/protocol-table.d.ts +14 -0
- package/dist/core/protocol-table.js +23 -0
- package/dist/core/runner/active-skill-scope.js +7 -7
- package/dist/core/runner/prepare-memory.js +4 -1
- package/dist/core/runner/prepare-task.js +38 -25
- package/dist/core/runner/runtask.js +9 -3
- package/dist/core/runner/session-rule-policy.d.ts +2 -2
- package/dist/core/runner/session-rule-policy.js +2 -1
- package/dist/core/runner/synthetic-tools.js +2 -2
- package/dist/core/runner/tool-disclosure.d.ts +0 -1
- package/dist/core/runner/tool-disclosure.js +0 -2
- package/dist/core/runner/turn-attachments.js +2 -1
- package/dist/core/sensitive-path-policy.js +2 -2
- package/dist/core/tool-policy.d.ts +0 -3
- package/dist/core/tool-policy.js +29 -15
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/orchestration/run-spec.js +1 -1
- package/dist/orchestration/run-workflow-tool.js +2 -2
- package/dist/orchestration/workflow-script-store.d.ts +1 -1
- package/dist/orchestration/workflow-script-store.js +1 -1
- package/dist/prompt-assembly/assemble.js +0 -1
- package/dist/prompt-assembly/tool-catalog.d.ts +2 -1
- package/dist/prompts/default.d.ts +0 -2
- package/dist/prompts/default.js +1 -5
- package/dist/stores/cc/mailbox-store.d.ts +4 -0
- package/dist/stores/cc/mailbox-store.js +16 -2
- package/dist/stores/file/session-policy-store.d.ts +10 -1
- package/dist/stores/file/session-policy-store.js +20 -4
- package/dist/tools/fs/fs-search-tools.js +1 -2
- package/dist/tools/fs/fs-shared.d.ts +0 -1
- package/dist/tools/fs/fs-shared.js +0 -1
- package/dist/tools/todo.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.0 (2026-08-03)
|
|
4
|
+
|
|
5
|
+
_The second (and final planned) baggage sweep: every remaining compatibility arm, plus a durable memory-key fix with an automatic adoption leg and the protocol table that clears the road for a second tool protocol. Version-line policy (project ruling): the major stays at 5 — breaking changes below are declared loudly here and coordinated with all in-org consumers, but do not mint a new major._
|
|
6
|
+
|
|
7
|
+
**BREAKING (declared, rides the 5.x line by project ruling)**
|
|
8
|
+
|
|
9
|
+
0. **The registry closes — every remaining compatibility arm is retired in this release:**
|
|
10
|
+
- `createDegradingBrain`: ONE form — the ordered `fallbacks` chain (`fallback`+`fallbackModel` retired; a chain-of-one is `fallbacks: [{brain, model}]`). The pair's two-trigger default retires with it: every construction gets the four-class gate; narrow explicitly via `downgradeOn`.
|
|
11
|
+
- `ToolSearch`: the pre-design/116 `select` ARRAY argument resolves nothing (the CC-canonical `select:A,B,C` string prefix in `query` is the live selection form).
|
|
12
|
+
- `Skill`: `skill` is REQUIRED (the `{name}` arm retired) — a stale replay bounces as loud, model-correctable invalid-arguments.
|
|
13
|
+
- `LSP`: the deprecated `symbol` alias for `query` leaves the tool schema (the `LspRequest.symbol` SEAM field is the wire contract, untouched).
|
|
14
|
+
- `TodoWrite`: `activeForm` is REQUIRED (exactly CC's TodoItem).
|
|
15
|
+
- `WorkflowScriptStore.resolveName`: ONE return shape — `{script, defaultArgs?}` (the bare-string arm retired).
|
|
16
|
+
- `sema-tb`: the `--message` argv leg is a usage error (process-list exposure once made a model's pgrep cleanup kill its own agent); use `--objective-file` or stdin.
|
|
17
|
+
- Anthropic brain: thinking enables ONLY on a valid non-`"off"` `ThinkingLevel` — the legacy-boolean silent 0.5-share enable is gone (out-of-contract value = absence).
|
|
18
|
+
- Terminal-cause predicates read `errorKind` ONLY — the `errorMessage`-sentinel fallback is deleted; stamping `errorKind` on every cut turn is now a documented Brain obligation.
|
|
19
|
+
- Read/Edit/Write schemas declare `file_path` only (the durable `path` alias retired; the security-gate readers keep their `path` over-read — that side is fail-closed coverage, not compat).
|
|
20
|
+
- The legacy prompt composer loses `memoryEnabled`/`consolidationEnabled` (dead in production since design/138 S4; `MEMORY_SAFETY`/`MEMORY_HYGIENE`/`MEMORY_GUIDANCE` stay exported as standalone assets).
|
|
21
|
+
1. **`PermissionResult` loses the legacy `reason` field.** All three arms carry `message` only, and `decisionText` reads `message` only. A TS policy returning `{reason}` compiles red; a JS policy keeps its verdict but its custom text degrades to the per-site generic default. Migrate: `reason:` → `message:`.
|
|
22
|
+
2. **`MemorySpecInput` is ONE form.** The singular `scope` field is removed; `scopes: string[]` is the only spelling. Migrate: `{scope: "x"}` → `{scopes: ["x"]}`. An untyped caller still passing `{scope}` gets "no usable scopes" = memory off for that task.
|
|
23
|
+
3. **The approval folds are STRICT.** `createApprovalPolicy.approve` and the `onAsk` boolean arm approve ONLY on the literal `true`; an out-of-contract truthy (`"yes"`, `1`, …) from an untyped bridge is a fail-closed deny naming the defect. The historical truthy leniency was a fail-open on the security face with no live producer (server bridge verified).
|
|
24
|
+
4. **Grep drops the retired keys (CC alignment).** `max_results` leaves the schema and the wiring — a stale caller's value applies NOTHING (`head_limit` is the one spelling). The `regex` key has no declared arm anywhere.
|
|
25
|
+
|
|
26
|
+
**Fixed**
|
|
27
|
+
|
|
28
|
+
- **One repo, one memory (RB-472).** `deriveRepoKey` canonicalizes (realpath) before minting, so a repo reached through a symlink alias and through its physical path no longer splits into two memory partitions. A pre-6.x deployment's legacy-keyed dirs are ADOPTED automatically at mount (one-shot whole-dir rename, concurrent-open race-tolerant). A store that already split is a loud `memory.partition_split` incident with both coordinates — never silently merged. Rename failure = `memory.partition_adopt_failed` disclosure; the mount never fails over adoption.
|
|
29
|
+
|
|
30
|
+
**Added**
|
|
31
|
+
|
|
32
|
+
- **Protocol namespace table**: `PROTOCOL_TABLE` / `MCP_NAMESPACE` / `protocolOf` / `ProtocolNamespace` / `ProtocolId` — "which protocol owns this tool name" as a lookup (`MCP_PREFIX` is now the table entry's prefix; a second protocol lands by appending one entry). `ToolOrigin`'s protocol slice derives from `ProtocolId`.
|
|
33
|
+
- **Corrupt-read disclosure seats** (the tolerances stay; the silence goes): `FileSessionPolicyStore`/`FileRosterStore` constructor `onCorruptRead`, `CcFileMailboxStoreOptions.onCorruptRead`, `recordRosterSpawn` optional `onError` (threaded to `onObserverError`, site `roster.recordSpawn`), and the durable-approval continuity read disclosing via `onObserverError` (site `resume.durableApprovalContinuity`). Plain ENOENT never fires any of them.
|
|
34
|
+
|
|
35
|
+
**Migration one-line table**: `reason:` → `message:` / `{scope}` → `{scopes: [..]}` / approver returns literal `true`·`false`·`"unavailable"`·`{allow}` only / `max_results` → `head_limit`.
|
|
36
|
+
|
|
37
|
+
## 5.0.1 (2026-08-02)
|
|
38
|
+
|
|
39
|
+
_Post-release review patch: the 5.0.0 review wave (five domain reviewers + two independent codex legs) surfaced silent-widening gaps in the new audit surfaces plus wiring/anchoring stragglers. All fixes are loud-direction; no new API._
|
|
40
|
+
|
|
41
|
+
**Fixed**
|
|
42
|
+
|
|
43
|
+
- **Persisted session/ancestor rules join the retired-name audit (review G-2).** Session-rule policies were composed after the only policy-name audit pass and exposed no name metadata — a stored `{toolDeny: ["bash"]}` compiled clean and silently widened. The audit is now a reusable pass run over every narrowing layer, and `createSessionRulePolicy` returns a `NamedToolPolicy`.
|
|
44
|
+
- **Pre-prefix MCP names reach the hard arm (review G-3).** The `config.legacy_tool_name` check only consulted the static `RETIRED_TOOL_NAMES` map, which cannot enumerate dynamic MCP names — a v4-era policy entry like `github__delete_repository` (auto-prefixed by the retired canonicalizer) degraded to an advisory and the destructive call ran. An unmatched `__`-shaped name without the `mcp__` prefix now hard-fails prepare with prefix guidance; an `mcp__*` miss stays advisory (the server may not be mounted this run).
|
|
45
|
+
- **Repair-loop accounts stay live across an in-flight attempt (review G-4, time half).** A checkpoint minted mid-attempt clones the threaded bundle at that instant, and only a pre-dispatch stamp existed — an attempt that ran 900ms and parked persisted ~zero elapsed time, so the resume re-granted the washed budget. An unref'd 1s interval keeps the stamp current. (The money half — mid-flight partial spend — is tracked separately: it has a double-count design question against the resumed attempt's own stats.)
|
|
46
|
+
- **Permission-rule policies join the retired-name audit (review F-6).** `createPermissionRulePolicy` now returns a `NamedToolPolicy` exposing its compiled tool names as `nameSets`, so the prepare-time policy-name audit covers rule policies: a rule written with a RETIRED name (e.g. `{rule: "bash", behavior: "deny"}`) that matches nothing in the run's roster is now the same HARD `config.legacy_tool_name` prepare failure as a list policy — previously it compiled with zero issues and the deny silently guarded nothing. A deployment mounting its own tool under a retired name is unaffected (roster hit). Behavior note: configs carrying such stale rules now fail loudly at prepare instead of running silently widened.
|
|
47
|
+
- **Name-addressed revives anchor correctly (review F-1).** The SendMessage by-name resolution hand-copy dropped `createdAt`, so a `{to: NAME}` revive's spawn frame fell back to the wake instant for `startedAt` — the exact-id rung was fixed in 5.0.0, the name rung is now too.
|
|
48
|
+
- **Two more revive legs chain `root ?? parent` (review F-8).** The revived child's own spec internals and the re-minted registry row now derive the same root anchor as the roster/observer legs — a pre-floor row no longer yields an internally inconsistent agent (fleet view anchored, `canAccess` root arm blind).
|
|
49
|
+
- **The disclosure channel reaches both remaining composition roots (review F-2/F-3).** The runner's SendMessage mount wires `onNotifyError` to the `observer.notify_failed` trace lane, and the delegation tool's steer-lane resume threads `opts.onObserverError` — a throwing deployment sink on either revive path is now disclosed instead of counted on an unread notifier.
|
|
50
|
+
- **Checkpoint mints read the clock once.** All three checkpoint mint points stamped `createdAt` and `deadline` from separate `Date.now()` reads; a mint straddling a ms tick violated `deadline = createdAt + ttl` by 1ms.
|
|
51
|
+
- Comment-truth sweep: 14 sites still narrating the deleted OLD_TO_NEW/tombstone machinery rewritten to post-5.0.0 behavior; `markStopSourceByShellId`'s orphaned docstring relocated and corrected (env is REQUIRED for effect — an env-less call is a no-op).
|
|
52
|
+
|
|
3
53
|
## 5.0.0 (2026-08-02)
|
|
4
54
|
|
|
5
55
|
_The baggage-unload major: the compatibility faces RB-476 cleared for tool NAMES are now cleared for id arguments, adapters and indexes too (RB-479), plus the revive-frame anchoring fix a downstream consumer was blocked on (RB-478). Everything that used to resolve silently under an old spelling now refuses loudly with the current spelling named._
|
|
@@ -194,7 +194,15 @@ export async function runRepairLoop(runner, implSpec, config) {
|
|
|
194
194
|
}
|
|
195
195
|
: implSpec;
|
|
196
196
|
restartedOnce = false;
|
|
197
|
-
const
|
|
197
|
+
const liveStamp = setInterval(stampAccount, 1_000);
|
|
198
|
+
liveStamp.unref?.();
|
|
199
|
+
let result;
|
|
200
|
+
try {
|
|
201
|
+
result = await runner.runTask(spec, { repairBundle: bundle });
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
clearInterval(liveStamp);
|
|
205
|
+
}
|
|
198
206
|
spend += (result.stats.costMicroUsd ?? 0) + (result.stats.nested?.costMicroUsd ?? 0);
|
|
199
207
|
stampAccount();
|
|
200
208
|
accumulate(result.stats);
|
|
@@ -42,7 +42,14 @@ export declare class FileRosterStore implements RosterStore {
|
|
|
42
42
|
private readonly path;
|
|
43
43
|
private readonly maxAgeMs;
|
|
44
44
|
private readonly gc;
|
|
45
|
-
|
|
45
|
+
private readonly onCorruptRead;
|
|
46
|
+
constructor(path: string, opts?: RosterGcOptions & {
|
|
47
|
+
onCorruptRead?: (info: {
|
|
48
|
+
path: string;
|
|
49
|
+
reason: string;
|
|
50
|
+
}) => void;
|
|
51
|
+
});
|
|
52
|
+
private discloseCorrupt;
|
|
46
53
|
private read;
|
|
47
54
|
private write;
|
|
48
55
|
record(entry: RosterEntry): void;
|
|
@@ -50,4 +57,4 @@ export declare class FileRosterStore implements RosterStore {
|
|
|
50
57
|
list(): RosterEntry[];
|
|
51
58
|
releaseAgent(agentId: string): void;
|
|
52
59
|
}
|
|
53
|
-
export declare function recordRosterSpawn(roster: RosterStore | undefined, entry: RosterEntry): void;
|
|
60
|
+
export declare function recordRosterSpawn(roster: RosterStore | undefined, entry: RosterEntry, onError?: (err: unknown) => void): void;
|
|
@@ -76,10 +76,19 @@ export class FileRosterStore {
|
|
|
76
76
|
path;
|
|
77
77
|
maxAgeMs;
|
|
78
78
|
gc;
|
|
79
|
+
onCorruptRead;
|
|
79
80
|
constructor(path, opts) {
|
|
80
81
|
this.path = path;
|
|
81
82
|
this.maxAgeMs = opts?.maxAgeMs;
|
|
82
83
|
this.gc = opts;
|
|
84
|
+
this.onCorruptRead = opts?.onCorruptRead;
|
|
85
|
+
}
|
|
86
|
+
discloseCorrupt(reason) {
|
|
87
|
+
try {
|
|
88
|
+
this.onCorruptRead?.({ path: this.path, reason });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
}
|
|
83
92
|
}
|
|
84
93
|
read(mode) {
|
|
85
94
|
let text;
|
|
@@ -89,8 +98,10 @@ export class FileRosterStore {
|
|
|
89
98
|
catch (e) {
|
|
90
99
|
if (e.code === "ENOENT")
|
|
91
100
|
return [];
|
|
92
|
-
if (mode === "lenient")
|
|
101
|
+
if (mode === "lenient") {
|
|
102
|
+
this.discloseCorrupt(`read failed (non-ENOENT): ${e.message}`);
|
|
93
103
|
return [];
|
|
104
|
+
}
|
|
94
105
|
throw e;
|
|
95
106
|
}
|
|
96
107
|
if (text.trim() === "")
|
|
@@ -102,8 +113,10 @@ export class FileRosterStore {
|
|
|
102
113
|
return raw.entries.filter((e) => typeof e === "object" && e !== null && typeof e.name === "string" && typeof e.agentId === "string" && typeof e.createdAt === "number");
|
|
103
114
|
}
|
|
104
115
|
catch (e) {
|
|
105
|
-
if (mode === "lenient")
|
|
116
|
+
if (mode === "lenient") {
|
|
117
|
+
this.discloseCorrupt(`corrupt roster document: ${e.message}`);
|
|
106
118
|
return [];
|
|
119
|
+
}
|
|
107
120
|
throw e;
|
|
108
121
|
}
|
|
109
122
|
}
|
|
@@ -127,12 +140,20 @@ export class FileRosterStore {
|
|
|
127
140
|
this.write(this.read("strict").filter((e) => e.agentId !== agentId));
|
|
128
141
|
}
|
|
129
142
|
}
|
|
130
|
-
export function recordRosterSpawn(roster, entry) {
|
|
143
|
+
export function recordRosterSpawn(roster, entry, onError) {
|
|
131
144
|
if (!roster)
|
|
132
145
|
return;
|
|
146
|
+
const disclose = (err) => {
|
|
147
|
+
try {
|
|
148
|
+
onError?.(err);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
}
|
|
152
|
+
};
|
|
133
153
|
try {
|
|
134
|
-
void Promise.resolve(roster.record(entry)).catch(
|
|
154
|
+
void Promise.resolve(roster.record(entry)).catch(disclose);
|
|
135
155
|
}
|
|
136
|
-
catch {
|
|
156
|
+
catch (err) {
|
|
157
|
+
disclose(err);
|
|
137
158
|
}
|
|
138
159
|
}
|
|
@@ -369,7 +369,7 @@ export function createSendMessageTool(opts) {
|
|
|
369
369
|
}
|
|
370
370
|
if (byName.status === "found") {
|
|
371
371
|
const h = byName.handle;
|
|
372
|
-
target = { task_id: h.id, type: h.type, status: h.status, ...(h.toolUseId !== undefined ? { toolUseId: h.toolUseId } : {}), ...(h.sessionScoped ? { sessionScoped: true } : {}), ...(h.description !== undefined ? { description: h.description } : {}), ...(h.name !== undefined ? { name: h.name } : {}), ...(h.agentType !== undefined ? { agentType: h.agentType } : {}), ...(h.owner !== undefined ? { owner: h.owner } : {}), ...(h.scope !== undefined ? { scope: h.scope } : {}), ...(h.parentTaskId !== undefined ? { parentTaskId: h.parentTaskId } : {}), ...(h.parentSessionId !== undefined ? { parentSessionId: h.parentSessionId } : {}), ...(h.rootSessionId !== undefined ? { rootSessionId: h.rootSessionId } : {}) };
|
|
372
|
+
target = { task_id: h.id, type: h.type, status: h.status, createdAt: h.createdAt, ...(h.toolUseId !== undefined ? { toolUseId: h.toolUseId } : {}), ...(h.sessionScoped ? { sessionScoped: true } : {}), ...(h.description !== undefined ? { description: h.description } : {}), ...(h.name !== undefined ? { name: h.name } : {}), ...(h.agentType !== undefined ? { agentType: h.agentType } : {}), ...(h.owner !== undefined ? { owner: h.owner } : {}), ...(h.scope !== undefined ? { scope: h.scope } : {}), ...(h.parentTaskId !== undefined ? { parentTaskId: h.parentTaskId } : {}), ...(h.parentSessionId !== undefined ? { parentSessionId: h.parentSessionId } : {}), ...(h.rootSessionId !== undefined ? { rootSessionId: h.rootSessionId } : {}) };
|
|
373
373
|
}
|
|
374
374
|
else {
|
|
375
375
|
let rosterHit;
|
package/dist/agents/subagent.js
CHANGED
|
@@ -1381,7 +1381,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1381
1381
|
? {
|
|
1382
1382
|
...(reviveClaim.row.parentTaskId !== undefined ? { parentTaskId: reviveClaim.row.parentTaskId } : {}),
|
|
1383
1383
|
...(reviveClaim.row.parentSessionId !== undefined ? { parentSessionId: reviveClaim.row.parentSessionId } : {}),
|
|
1384
|
-
...(reviveClaim.row.rootSessionId
|
|
1384
|
+
...((reviveClaim.row.rootSessionId ?? reviveClaim.row.parentSessionId) !== undefined
|
|
1385
|
+
? { rootSessionId: reviveClaim.row.rootSessionId ?? reviveClaim.row.parentSessionId }
|
|
1386
|
+
: {}),
|
|
1385
1387
|
}
|
|
1386
1388
|
: {
|
|
1387
1389
|
...(ctx.taskId !== undefined ? { parentTaskId: ctx.taskId } : {}),
|
|
@@ -1691,7 +1693,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1691
1693
|
}, { priority: "later" }), "subagent.reapTerminalNotify");
|
|
1692
1694
|
});
|
|
1693
1695
|
if (agentName !== undefined) {
|
|
1694
|
-
recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, sessionId: forkedId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: Date.now() });
|
|
1696
|
+
recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, sessionId: forkedId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
|
|
1695
1697
|
}
|
|
1696
1698
|
const bgSink = ctx.onBackgroundChildEvent;
|
|
1697
1699
|
const sinkEmit = (event) => {
|
|
@@ -2045,7 +2047,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2045
2047
|
...(reviveRow.name !== undefined ? { name: reviveRow.name } : {}),
|
|
2046
2048
|
...(reviveRow.parentTaskId !== undefined ? { parentTaskId: reviveRow.parentTaskId } : {}),
|
|
2047
2049
|
...(reviveRow.parentSessionId !== undefined ? { parentSessionId: reviveRow.parentSessionId } : {}),
|
|
2048
|
-
...(reviveRow.rootSessionId
|
|
2050
|
+
...((reviveRow.rootSessionId ?? reviveRow.parentSessionId) !== undefined
|
|
2051
|
+
? { rootSessionId: reviveRow.rootSessionId ?? reviveRow.parentSessionId }
|
|
2052
|
+
: {}),
|
|
2049
2053
|
...(reviveRow.model !== undefined ? { model: reviveRow.model } : {}),
|
|
2050
2054
|
...(reviveRow.teamName !== undefined ? { teamName: reviveRow.teamName } : {}),
|
|
2051
2055
|
deliveryChannel: "attaching",
|
|
@@ -2092,7 +2096,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2092
2096
|
return { isError: true, content: `Sub-agent not started in background: ${e instanceof Error ? e.message : String(e)}${wt ? `\n${wt}` : ""}`, details: { error: "register_failed" } };
|
|
2093
2097
|
}
|
|
2094
2098
|
if (agentName !== undefined) {
|
|
2095
|
-
recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...(reviveRow !== undefined ? ((reviveRow.rootSessionId ?? reviveRow.parentSessionId) !== undefined ? { rootSessionId: reviveRow.rootSessionId ?? reviveRow.parentSessionId } : {}) : (ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: reviveRow?.spawnedAt ?? Date.now() });
|
|
2099
|
+
recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...(reviveRow !== undefined ? ((reviveRow.rootSessionId ?? reviveRow.parentSessionId) !== undefined ? { rootSessionId: reviveRow.rootSessionId ?? reviveRow.parentSessionId } : {}) : (ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: reviveRow?.spawnedAt ?? Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
|
|
2096
2100
|
}
|
|
2097
2101
|
const bgSink = ctx.onBackgroundChildEvent;
|
|
2098
2102
|
const sinkEmit = (event) => {
|
|
@@ -2329,7 +2333,12 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2329
2333
|
resumeConfig.durableApproval = { scope: cp0.scope };
|
|
2330
2334
|
}
|
|
2331
2335
|
}
|
|
2332
|
-
catch {
|
|
2336
|
+
catch (err) {
|
|
2337
|
+
try {
|
|
2338
|
+
opts.onObserverError?.(err, { site: "resume.durableApprovalContinuity" });
|
|
2339
|
+
}
|
|
2340
|
+
catch {
|
|
2341
|
+
}
|
|
2333
2342
|
}
|
|
2334
2343
|
}
|
|
2335
2344
|
const bgStream = await opts.runner.resumeStream(pr.ticket.token, pr.outcome, resumeConfig, {
|
|
@@ -2673,6 +2682,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2673
2682
|
...(opts.background?.notify ? { notify: opts.background.notify } : {}),
|
|
2674
2683
|
sink: ctx.onSubagentSpawn,
|
|
2675
2684
|
...(opts.background ? { registry: opts.background.registry } : {}),
|
|
2685
|
+
...(opts.onObserverError !== undefined ? { onNotifyError: (f) => opts.onObserverError?.(f.error, { site: f.site }) } : {}),
|
|
2676
2686
|
});
|
|
2677
2687
|
notifier.notify(() => ctx.onSubagentSpawn?.(createSteerHandle(stream, ctx.toolCallId, childAgentName, settled, {
|
|
2678
2688
|
resume,
|
package/dist/bin/sema-tb.d.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { type Model } from "../index.js";
|
|
3
3
|
interface Args {
|
|
4
4
|
model: string;
|
|
5
|
-
message: string | undefined;
|
|
6
5
|
objectiveFile: string | undefined;
|
|
7
6
|
jsonl: string | undefined;
|
|
8
7
|
}
|
|
@@ -19,7 +18,7 @@ export type InstructionResolution = {
|
|
|
19
18
|
} | {
|
|
20
19
|
error: string;
|
|
21
20
|
};
|
|
22
|
-
export declare function resolveInstruction(args: Pick<Args, "
|
|
21
|
+
export declare function resolveInstruction(args: Pick<Args, "objectiveFile">, readInput: (source: string | 0) => string): InstructionResolution;
|
|
23
22
|
type TbCost = Model["cost"];
|
|
24
23
|
export type TbCostResolution = {
|
|
25
24
|
cost: TbCost;
|
package/dist/bin/sema-tb.js
CHANGED
|
@@ -6,11 +6,11 @@ import { positiveIntFromEnv, tbModelInput } from "./tb-env.js";
|
|
|
6
6
|
import { inferMaxTokensField } from "../brain/openai.js";
|
|
7
7
|
function usage(code = 2) {
|
|
8
8
|
const out = code === 0 ? process.stdout : process.stderr;
|
|
9
|
-
out.write("Usage: sema-tb --model <provider/model> [--
|
|
10
|
-
" Instruction legs: --objective-file <path> (preferred for real task text) |
|
|
9
|
+
out.write("Usage: sema-tb --model <provider/model> [--objective-file <path>] [--jsonl <path>]\n" +
|
|
10
|
+
" Instruction legs: --objective-file <path> (preferred for real task text) | stdin\n" +
|
|
11
11
|
" | stdin (default when neither flag is given). Both flags together = usage error.\n" +
|
|
12
12
|
" WARNING: argv is visible to every process in the sandbox (ps / pgrep -f / /proc/*/cmdline) — long\n" +
|
|
13
|
-
"
|
|
13
|
+
" Instructions ride --objective-file or stdin; the retired --message argv leg is refused (process-list exposure).\n" +
|
|
14
14
|
"Env: <PROVIDER>_API_KEY / <PROVIDER>_BASE_URL, TB_MAX_TURNS, TB_TIMEOUT_SEC, TB_FULLBODY, TB_TODOREMIND, TB_BGTASKS, TB_SUMMARIZE,\n" +
|
|
15
15
|
" TB_FINALVERIFY, TB_CALLCAP, TB_FINALIZE, TB_REASONING, TB_JSONL, TB_CONTEXT_WINDOW, TB_MAX_OUTPUT_TOKENS, TB_TASKLIST, TB_VISION\n" +
|
|
16
16
|
' TB_COST="input,output[,cacheRead[,cacheWrite]]" — USD per MTok; explicit pricing override (omitted\n' +
|
|
@@ -20,19 +20,19 @@ function usage(code = 2) {
|
|
|
20
20
|
}
|
|
21
21
|
export function parseArgs(argv, env = process.env) {
|
|
22
22
|
let model;
|
|
23
|
-
let message;
|
|
24
23
|
let objectiveFile;
|
|
25
24
|
let jsonl;
|
|
26
25
|
for (let i = 0; i < argv.length; i++) {
|
|
27
26
|
const a = argv[i];
|
|
28
|
-
if (a === "--
|
|
27
|
+
if (a === "--message") {
|
|
28
|
+
return { usageError: "--message is retired (argv is visible to every sandbox process — the exposure once made a model's pgrep cleanup kill its own agent). Use --objective-file <path> or pipe the instruction on stdin." };
|
|
29
|
+
}
|
|
30
|
+
if (a === "--model" || a === "--objective-file" || a === "--jsonl") {
|
|
29
31
|
const v = argv[++i];
|
|
30
32
|
if (v === undefined)
|
|
31
33
|
return { usageError: `missing value for ${a}` };
|
|
32
34
|
if (a === "--model")
|
|
33
35
|
model = v;
|
|
34
|
-
else if (a === "--message")
|
|
35
|
-
message = v;
|
|
36
36
|
else if (a === "--objective-file")
|
|
37
37
|
objectiveFile = v;
|
|
38
38
|
else
|
|
@@ -46,28 +46,16 @@ export function parseArgs(argv, env = process.env) {
|
|
|
46
46
|
if (!model || !model.includes("/")) {
|
|
47
47
|
return { usageError: "--model must be <provider/model> (e.g. deepseek/deepseek-v4-flash)" };
|
|
48
48
|
}
|
|
49
|
-
return { args: { model,
|
|
49
|
+
return { args: { model, objectiveFile, jsonl: jsonl ?? env.TB_JSONL } };
|
|
50
50
|
}
|
|
51
51
|
export function resolveInstruction(args, readInput) {
|
|
52
|
-
|
|
53
|
-
return { error: "--message and --objective-file are mutually exclusive (pick one instruction leg)" };
|
|
54
|
-
}
|
|
55
|
-
const leg = args.objectiveFile !== undefined
|
|
56
|
-
? `--objective-file ${args.objectiveFile}`
|
|
57
|
-
: args.message !== undefined
|
|
58
|
-
? "--message"
|
|
59
|
-
: "stdin (no --message/--objective-file)";
|
|
52
|
+
const leg = args.objectiveFile !== undefined ? `--objective-file ${args.objectiveFile}` : "stdin (no --objective-file)";
|
|
60
53
|
let raw;
|
|
61
|
-
|
|
62
|
-
raw = args.
|
|
54
|
+
try {
|
|
55
|
+
raw = readInput(args.objectiveFile ?? 0);
|
|
63
56
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
raw = readInput(args.objectiveFile ?? 0);
|
|
67
|
-
}
|
|
68
|
-
catch (e) {
|
|
69
|
-
return { error: `cannot read instruction from ${leg}: ${e instanceof Error ? e.message : String(e)}` };
|
|
70
|
-
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
return { error: `cannot read instruction from ${leg}: ${e instanceof Error ? e.message : String(e)}` };
|
|
71
59
|
}
|
|
72
60
|
const instruction = raw.trim();
|
|
73
61
|
if (!instruction)
|
package/dist/brain/anthropic.js
CHANGED
|
@@ -253,7 +253,7 @@ export function createAnthropicBrain(config = {}) {
|
|
|
253
253
|
if (options?.temperature !== undefined && anthCompat.supportsTemperature !== false) {
|
|
254
254
|
body.temperature = options.temperature;
|
|
255
255
|
}
|
|
256
|
-
if (model.reasoning && options?.reasoning && options.reasoning !== "off") {
|
|
256
|
+
if (model.reasoning && isThinkingLevel(options?.reasoning) && options.reasoning !== "off") {
|
|
257
257
|
if (anthCompat.thinkingMode === "adaptive") {
|
|
258
258
|
body.thinking = { type: "adaptive" };
|
|
259
259
|
}
|
|
@@ -264,8 +264,7 @@ export function createAnthropicBrain(config = {}) {
|
|
|
264
264
|
delete body.thinking;
|
|
265
265
|
}
|
|
266
266
|
else {
|
|
267
|
-
const
|
|
268
|
-
const share = config.thinkingBudgetShare ?? tierShare ?? 0.5;
|
|
267
|
+
const share = config.thinkingBudgetShare ?? reasoningBudgetShare(options.reasoning);
|
|
269
268
|
const { max, budget } = thinkingBudget(body.max_tokens, share, config.thinkingBudgetTokens, hardCap);
|
|
270
269
|
body.max_tokens = max;
|
|
271
270
|
body.thinking = { type: "enabled", budget_tokens: budget };
|
|
@@ -12,18 +12,12 @@ interface DegradingBrainBaseOptions {
|
|
|
12
12
|
primary: Brain;
|
|
13
13
|
downgradeOn?: DegradeReason[];
|
|
14
14
|
}
|
|
15
|
-
export type DegradingBrainOptions =
|
|
15
|
+
export type DegradingBrainOptions = DegradingBrainBaseOptions & {
|
|
16
16
|
fallbacks: Array<{
|
|
17
17
|
brain: Brain;
|
|
18
18
|
model: Model;
|
|
19
19
|
}>;
|
|
20
|
-
|
|
21
|
-
fallbackModel?: never;
|
|
22
|
-
}) | (DegradingBrainBaseOptions & {
|
|
23
|
-
fallbacks?: never;
|
|
24
|
-
fallback: Brain;
|
|
25
|
-
fallbackModel: Model;
|
|
26
|
-
});
|
|
20
|
+
};
|
|
27
21
|
export declare function readDegradation(msg: {
|
|
28
22
|
diagnostics?: AssistantMessageDiagnostic[];
|
|
29
23
|
} | undefined): DegradationInfo | undefined;
|
package/dist/brain/degrading.js
CHANGED
|
@@ -115,12 +115,12 @@ export function passthroughStream(brain, model, context, options) {
|
|
|
115
115
|
return out;
|
|
116
116
|
}
|
|
117
117
|
export function createDegradingBrain(opts) {
|
|
118
|
-
const triggers = new Set(opts.downgradeOn ??
|
|
119
|
-
const rawHops = opts.fallbacks ??
|
|
118
|
+
const triggers = new Set(opts.downgradeOn ?? ["rate_limit", "breaker_open", "server_error", "last_resort"]);
|
|
119
|
+
const rawHops = opts.fallbacks ?? [];
|
|
120
120
|
const seenIds = new Set();
|
|
121
121
|
const hops = rawHops.filter((h) => (seenIds.has(h.model.id) ? false : (seenIds.add(h.model.id), true))).slice(0, MAX_CHAIN);
|
|
122
122
|
if (hops.length === 0) {
|
|
123
|
-
throw new Error("createDegradingBrain:
|
|
123
|
+
throw new Error("createDegradingBrain: `fallbacks` needs at least one {brain, model} hop (RB-482 #3: the legacy fallback+fallbackModel pair is retired — a chain-of-one is `fallbacks: [{brain, model}]`)");
|
|
124
124
|
}
|
|
125
125
|
const stream = (model, context, options) => {
|
|
126
126
|
if (options?.resilience?.allowDegrade === false) {
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import { DEGENERATE_MESSAGE } from "./repetition.js";
|
|
2
|
-
import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
|
|
3
1
|
export function isDegenerateCutMessage(message) {
|
|
4
|
-
|
|
5
|
-
return true;
|
|
6
|
-
return message.errorMessage === DEGENERATE_MESSAGE;
|
|
2
|
+
return message.errorKind === "degenerate";
|
|
7
3
|
}
|
|
8
4
|
export function isWalltimeCutMessage(message) {
|
|
9
|
-
|
|
10
|
-
return true;
|
|
11
|
-
return message.errorMessage === WALLTIME_CUTOFF_MESSAGE;
|
|
5
|
+
return message.errorKind === "walltime_cutoff";
|
|
12
6
|
}
|
package/dist/core/hooks.js
CHANGED
|
@@ -177,7 +177,7 @@ export async function runToolGate(input) {
|
|
|
177
177
|
if (decision.action === "ask" && req.toolName === ASK_USER_QUESTION_TOOL_NAME) {
|
|
178
178
|
decision = {
|
|
179
179
|
action: "deny",
|
|
180
|
-
|
|
180
|
+
message: "AskUserQuestion could not be parked for a durable human answer on this path (no checkpoint " +
|
|
181
181
|
"facility, or the suspend failed) and a synchronous permission approver cannot carry an answer — " +
|
|
182
182
|
"the question was NOT shown to anyone. Proceed with your best judgment and state the assumption " +
|
|
183
183
|
"you made.",
|
|
@@ -211,7 +211,7 @@ export async function runToolGate(input) {
|
|
|
211
211
|
}
|
|
212
212
|
catch (err) {
|
|
213
213
|
traceHookCrash(input, err, notifier);
|
|
214
|
-
editDenied = { action: "deny",
|
|
214
|
+
editDenied = { action: "deny", message: preToolUseCrashReason(`the approved edit for "${toolName}"`, err) };
|
|
215
215
|
denySource = "hook";
|
|
216
216
|
break;
|
|
217
217
|
}
|
|
@@ -219,7 +219,7 @@ export async function runToolGate(input) {
|
|
|
219
219
|
if (hr.additionalContext)
|
|
220
220
|
preToolContext.push(hr.additionalContext);
|
|
221
221
|
if (hr.action === "deny") {
|
|
222
|
-
editDenied = { action: "deny",
|
|
222
|
+
editDenied = { action: "deny", message: decisionText(hr) ?? `the approved edit for "${toolName}" was blocked by a PreToolUse hook` };
|
|
223
223
|
denySource = "hook";
|
|
224
224
|
break;
|
|
225
225
|
}
|
|
@@ -244,7 +244,7 @@ export async function runToolGate(input) {
|
|
|
244
244
|
catch (err) {
|
|
245
245
|
recheck = {
|
|
246
246
|
action: "deny",
|
|
247
|
-
|
|
247
|
+
message: `policy re-check of the approved edit for "${toolName}" errored: ${err instanceof Error ? err.message : String(err)}`,
|
|
248
248
|
};
|
|
249
249
|
}
|
|
250
250
|
if (recheck.action === "deny") {
|
package/dist/core/lsp.js
CHANGED
|
@@ -171,18 +171,17 @@ export function createLspTool(manager, opts = {}) {
|
|
|
171
171
|
line: Type.Optional(Type.Number({ description: "The line number (1-based, as shown in editors)." })),
|
|
172
172
|
character: Type.Optional(Type.Number({ description: "The character offset (1-based, as shown in editors)." })),
|
|
173
173
|
query: Type.Optional(Type.String({ description: "Symbol query for workspaceSymbol (may be empty to browse broadly)." })),
|
|
174
|
-
symbol: Type.Optional(Type.String({ description: "Deprecated alias for query." })),
|
|
175
174
|
}),
|
|
176
175
|
effect: "read",
|
|
177
176
|
execute: async (args, ctx) => {
|
|
178
|
-
const { operation, filePath, line, character, query
|
|
177
|
+
const { operation, filePath, line, character, query } = args;
|
|
179
178
|
if (POSITION_OPS.has(operation) && (line === undefined || character === undefined)) {
|
|
180
179
|
return errorResult(`Error (LSP): the "${operation}" operation needs both line and character.`);
|
|
181
180
|
}
|
|
182
181
|
if (POSITION_OPS.has(operation) && (!Number.isInteger(line) || line < 1 || !Number.isInteger(character) || character < 1)) {
|
|
183
182
|
return errorResult(`Error (LSP): line and character are 1-based integers (as shown in editors); got line=${line}, character=${character}.`);
|
|
184
183
|
}
|
|
185
|
-
const wsQuery = query
|
|
184
|
+
const wsQuery = query;
|
|
186
185
|
if (operation === "workspaceSymbol" && wsQuery === undefined) {
|
|
187
186
|
return errorResult(`Error (LSP): the "workspaceSymbol" operation needs a query.`);
|
|
188
187
|
}
|
package/dist/core/mcp.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export interface McpServerStatus {
|
|
|
56
56
|
error?: string;
|
|
57
57
|
transportClosed?: boolean;
|
|
58
58
|
}
|
|
59
|
-
export declare const MCP_PREFIX
|
|
59
|
+
export declare const MCP_PREFIX: string;
|
|
60
60
|
export declare function resolveMcpDeclaredResultSize(meta: Record<string, unknown> | undefined): number | undefined;
|
|
61
61
|
export declare function gateMcpOutput(content: Array<TextContent | ImageContent>, limitTokens?: number): Array<TextContent | ImageContent>;
|
|
62
62
|
export declare function truncateMcpErrorText(s: string): string;
|
package/dist/core/mcp.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { MCP_NAMESPACE } from "./protocol-table.js";
|
|
2
3
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
4
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4
5
|
import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
@@ -12,7 +13,7 @@ import { truncateError } from "./tool-errors.js";
|
|
|
12
13
|
import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
13
14
|
import { validateJsonSchemaShape } from "./runner/strict-output-schema.js";
|
|
14
15
|
const NAME_SEP = "__";
|
|
15
|
-
export const MCP_PREFIX =
|
|
16
|
+
export const MCP_PREFIX = MCP_NAMESPACE.prefix;
|
|
16
17
|
const MCP_OUTPUT_TOKENS_DEFAULT = 25_000;
|
|
17
18
|
const MCP_CHARS_PER_TOKEN = 4;
|
|
18
19
|
const MCP_META_RESULT_SIZE_CAP = 500_000;
|
|
@@ -6,7 +6,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
|
|
|
6
6
|
import { formatMemoryAge } from "../memory-recall.js";
|
|
7
7
|
import { computeEntryRev, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
|
|
8
8
|
import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles } from "./file-backend.js";
|
|
9
|
-
import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, } from "./layout.js";
|
|
9
|
+
import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, adoptLegacyRepoDirs, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, } from "./layout.js";
|
|
10
10
|
import { scanMemoryFileName, scanMemoryWrite, scanRemediation } from "./scan.js";
|
|
11
11
|
export const MEMORY_INSTRUCTION_TEMPLATE = `# Memory
|
|
12
12
|
|
|
@@ -61,7 +61,7 @@ export class MemoryEngine {
|
|
|
61
61
|
this.controlDir =
|
|
62
62
|
typeof backendControl === "string" && backendControl
|
|
63
63
|
? backendControl
|
|
64
|
-
: (opts.controlDir ?? deriveControlPlaneDir(resolveMemoryEngineRoot(), opts.memoryDir));
|
|
64
|
+
: (opts.controlDir ?? (adoptLegacyRepoDirs(resolveMemoryEngineRoot(), opts.memoryDir), deriveControlPlaneDir(resolveMemoryEngineRoot(), opts.memoryDir)));
|
|
65
65
|
this.now = opts.now ?? Date.now;
|
|
66
66
|
this.perScopeBudgetBytes = opts.perScopeBudgetBytes ?? Number.MAX_SAFE_INTEGER;
|
|
67
67
|
this.perFileBytes = opts.perFileBytes ?? MAX_MEMORY_BYTES;
|
|
@@ -5,7 +5,7 @@ import { jaccardDistance, termSet } from "../memory-vector.js";
|
|
|
5
5
|
import { MAX_MEMORY_BYTES } from "../memory.js";
|
|
6
6
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
7
7
|
import { computeEntryRev, entryFromFile, isValidEntryId, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
|
|
8
|
-
import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, atomicWriteFileSync, quarantineAndTombstone, claimRootScope, deriveControlPlaneDir, ensureDirExists, enqueueMemoryAnnouncement, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirName, writeAllSync, } from "./layout.js";
|
|
8
|
+
import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, atomicWriteFileSync, quarantineAndTombstone, claimRootScope, adoptLegacyRepoDirs, deriveControlPlaneDir, ensureDirExists, enqueueMemoryAnnouncement, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirName, writeAllSync, } from "./layout.js";
|
|
9
9
|
import { scanMemoryFileName, scanMemoryWrite } from "./scan.js";
|
|
10
10
|
export const MEMORY_INDEX_FILENAME = "MEMORY.md";
|
|
11
11
|
export const DEFAULT_MAX_ENTRY_DEPTH = 3;
|
|
@@ -78,6 +78,8 @@ export class FileMemoryEngineBackend {
|
|
|
78
78
|
constructor(dir, opts = {}) {
|
|
79
79
|
this.now = opts.now ?? Date.now;
|
|
80
80
|
this.directoryRoot = dir;
|
|
81
|
+
if (opts.controlDir === undefined)
|
|
82
|
+
adoptLegacyRepoDirs(resolveMemoryEngineRoot(), dir);
|
|
81
83
|
this.controlPlaneRoot = opts.controlDir ?? deriveControlPlaneDir(resolveMemoryEngineRoot(), dir);
|
|
82
84
|
ensureDirExists(dir);
|
|
83
85
|
ensureDirExists(this.controlPlaneRoot);
|
|
@@ -6,6 +6,9 @@ export declare class ControlPlaneCorruptError extends Error {
|
|
|
6
6
|
});
|
|
7
7
|
}
|
|
8
8
|
export declare function deriveRepoKey(repoRoot: string): string;
|
|
9
|
+
export declare function adoptLegacyRepoDirs(configRoot: string, repoRoot: string, onIncident?: (err: Error & {
|
|
10
|
+
code?: string;
|
|
11
|
+
}) => void): void;
|
|
9
12
|
export declare function deriveRepoMemoryDir(configRoot: string, repoRoot: string): string;
|
|
10
13
|
export declare function deriveProjectMemoryDir(configRoot: string, projectId: string): string;
|
|
11
14
|
export declare function deriveProjectControlDir(configRoot: string, projectId: string): string;
|
|
@@ -11,8 +11,59 @@ export class ControlPlaneCorruptError extends Error {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
export function deriveRepoKey(repoRoot) {
|
|
14
|
+
return canonicalize(resolve(repoRoot)).normalize("NFC").replace(/[^A-Za-z0-9]/g, "-");
|
|
15
|
+
}
|
|
16
|
+
function deriveRepoKeyLegacy(repoRoot) {
|
|
14
17
|
return resolve(repoRoot).normalize("NFC").replace(/[^A-Za-z0-9]/g, "-");
|
|
15
18
|
}
|
|
19
|
+
export function adoptLegacyRepoDirs(configRoot, repoRoot, onIncident) {
|
|
20
|
+
const disclose = (code, message) => {
|
|
21
|
+
try {
|
|
22
|
+
const e = new Error(message);
|
|
23
|
+
e.code = code;
|
|
24
|
+
onIncident?.(e);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
const newKey = deriveRepoKey(repoRoot);
|
|
31
|
+
const oldKey = deriveRepoKeyLegacy(repoRoot);
|
|
32
|
+
if (newKey === oldKey)
|
|
33
|
+
return;
|
|
34
|
+
const oldDir = join(configRoot, oldKey);
|
|
35
|
+
const newDir = join(configRoot, newKey);
|
|
36
|
+
const exists = (p) => {
|
|
37
|
+
try {
|
|
38
|
+
statSync(p);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
if (!exists(oldDir))
|
|
46
|
+
return;
|
|
47
|
+
if (exists(newDir)) {
|
|
48
|
+
disclose("memory.partition_split", `memory partition split detected for ${repoRoot}: BOTH the legacy alias-keyed dir (${oldDir}) and the canonical dir (${newDir}) exist. ` +
|
|
49
|
+
`This session uses the canonical dir; the legacy dir is left untouched (never merged — CAS baselines would collide). ` +
|
|
50
|
+
`If the legacy side holds newer memory, move its rows by hand and remove it.`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
renameSync(oldDir, newDir);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
const code = err.code;
|
|
58
|
+
if ((code === "ENOENT" || code === "EEXIST" || code === "ENOTEMPTY") && exists(newDir))
|
|
59
|
+
return;
|
|
60
|
+
disclose("memory.partition_adopt_failed", `could not adopt the legacy memory partition ${oldDir} → ${newDir} (${code ?? String(err)}): this session proceeds on the canonical dir; ` +
|
|
61
|
+
`the legacy dir still holds the prior memory — move it by hand.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
}
|
|
66
|
+
}
|
|
16
67
|
export function deriveRepoMemoryDir(configRoot, repoRoot) {
|
|
17
68
|
return join(configRoot, deriveRepoKey(repoRoot), "memory");
|
|
18
69
|
}
|
package/dist/core/memory.d.ts
CHANGED
package/dist/core/memory.js
CHANGED
|
@@ -354,7 +354,7 @@ function mtimeMsOf(ts) {
|
|
|
354
354
|
export function normalizeMemorySpec(input) {
|
|
355
355
|
if (!input)
|
|
356
356
|
return undefined;
|
|
357
|
-
const raw = input.scopes
|
|
357
|
+
const raw = input.scopes ?? [];
|
|
358
358
|
const scopes = [];
|
|
359
359
|
const seen = new Set();
|
|
360
360
|
for (const s of raw) {
|