@sema-agent/core 5.25.0 → 5.26.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 +66 -0
- package/dist/agents/agent-definition.js +5 -0
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +5 -0
- package/dist/core/hooks.js +3 -2
- package/dist/core/memory-engine/dual-root.js +3 -1
- package/dist/core/memory-engine/engine.d.ts +45 -1
- package/dist/core/memory-engine/engine.js +23 -5
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/permission-rule-consent.js +8 -1
- package/dist/core/runner/compaction-call-options.d.ts +4 -4
- package/dist/core/runner/compaction-call-options.js +3 -4
- package/dist/core/runner/prepare-memory.d.ts +34 -15
- package/dist/core/runner/prepare-memory.js +85 -17
- package/dist/core/runner/prepare-task.js +31 -7
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +6 -0
- package/dist/core/store-contracts/tool-result-store-contract.js +24 -0
- package/dist/core/task-registry-agent.js +3 -3
- package/dist/core/task-registry-monitor.js +6 -5
- package/dist/core/tool-result-budget.d.ts +1 -1
- package/dist/core/tool-result-budget.js +3 -3
- package/dist/core/tool-result-store.d.ts +164 -9
- package/dist/core/tool-result-store.js +82 -23
- package/dist/core/types.d.ts +68 -0
- package/dist/core/untrusted-text.d.ts +6 -2
- package/dist/core/untrusted-text.js +1 -1
- package/dist/engine/session/import-validate.js +2 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/orchestration/workflow.js +2 -0
- package/dist/prompts/default.d.ts +11 -0
- package/dist/prompts/default.js +3 -0
- package/dist/stores/file/fs-atomic.d.ts +1 -1
- package/dist/stores/file/tool-result-store.d.ts +45 -9
- package/dist/stores/file/tool-result-store.js +76 -9
- package/dist/tools/fs/fs-shared.js +5 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.26.0 — 2026-08-11
|
|
4
|
+
|
|
5
|
+
No API-BREAKING changes (exports grow only; new members optional/additive). Several
|
|
6
|
+
behavior-surface narrowings — every one tighten-direction — called out below.
|
|
7
|
+
|
|
8
|
+
### Narrowed (behavior, ruled)
|
|
9
|
+
|
|
10
|
+
- **A remote execution env no longer teaches the memory write path.** With `executionEnv` remote
|
|
11
|
+
and a memory store mounted, the `# Memory` write instruction is withheld (the sandboxed hands
|
|
12
|
+
cannot reach the host store; teaching the path walked the model into receipting saves that never
|
|
13
|
+
land). A deployment persisting through its own closure restores it by declaring
|
|
14
|
+
`TaskSpec.memoryPersistenceCapable: true`. **Consumer note**: probes pinning "remote run carries
|
|
15
|
+
the write instruction" go red; re-pin on the declaration.
|
|
16
|
+
- **`memoryPersistenceCapable: false` is enforced, not just disclosed.** Over a writable scope the
|
|
17
|
+
session mounts the read-only notice AND the write gate refuses file-tool writes into the memory
|
|
18
|
+
root (`read_only_layering`, loud), and the terminal/checkpoint harvest admits nothing — through
|
|
19
|
+
the engine's zero-admission arm, so the report half still runs: materialize-time inbound findings
|
|
20
|
+
and quarantine escalations are drained into a real `HarvestReport` (with a warning naming the
|
|
21
|
+
declaration) instead of a fabricated empty one.
|
|
22
|
+
- **The read-only notice retraction is DECLARED-only.** Only an explicit
|
|
23
|
+
`memoryPersistenceCapable: true` retracts the engine's writeScope-null notice; a write-capable
|
|
24
|
+
roster alone (inferred capability) no longer strips it — the gate refuses those very writes, so
|
|
25
|
+
inference proved nothing about the store.
|
|
26
|
+
- **Dual-root overlap is a prepare-time configuration error.** Two memory planes whose data roots
|
|
27
|
+
or control dirs overlap (either direction, either tier) are refused loudly at prepare instead of
|
|
28
|
+
running two engines over shared physical state.
|
|
29
|
+
- **A shell-doctrine mark is positive-only and monotonic.** A negative `irreversibility` tier is
|
|
30
|
+
not a provenance mark; `shellGate:"classify"` never downgrades an explicitly marked `"always"`.
|
|
31
|
+
Deployments with marked tools will see asks that classify used to swallow.
|
|
32
|
+
- **Tool-result refs are injective and conflict-typed (#119).** The mint hashes an injective
|
|
33
|
+
representation (four `~`-separated segments incl. a content digest); writing a different payload
|
|
34
|
+
under an existing ref is a typed `ref_conflict` error, never a silent overwrite; a damaged owner
|
|
35
|
+
record refuses whole.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **`TaskSpec.memoryPersistenceCapable`** (tri-state, additive): `true` = the deployment vouches
|
|
40
|
+
for a persistence channel the engine cannot see; `false` = mandatory floor (disclosure + write
|
|
41
|
+
gate + zero-admission harvest); absent = inferred from the final roster. Crosses the delegation
|
|
42
|
+
boundary tighten-only (a parent's `false` binds the subtree; a child definition cannot loosen
|
|
43
|
+
it). Non-boolean values are refused loudly at both doors (`config.memory_persistence_invalid` /
|
|
44
|
+
`config.agent.invalid`).
|
|
45
|
+
- **`MEMORY_READONLY_NOTICE` / `NO_PERSISTENT_MEMORY_NOTICE` exported**, and
|
|
46
|
+
**`MemoryInjection.readOnlyNotice`** (additive member): a session with no write channel is told
|
|
47
|
+
so instead of silently receipting saves; the standalone notice serves rosters the engine never
|
|
48
|
+
sees.
|
|
49
|
+
- **`MemoryEngine.harvest` accepts `admitNothing`** (additive option): a real harvest that commits
|
|
50
|
+
nothing while still draining and announcing inbound findings.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- Delegation seats forward the capability floor (subagent arbitration, retained-resume folding,
|
|
55
|
+
workflow agent-type folding fill absence only).
|
|
56
|
+
- The offloaded-detail notice allowance derives from the ref-mint ceiling (a literal sized against
|
|
57
|
+
the retired two-segment shape under-bounded the four-segment notice).
|
|
58
|
+
- Ten stale-or-loose doc/comment spots from the merged-code rescan (classify doctrine conditionals
|
|
59
|
+
stated in public docs; JSDoc reattachments; TiDB sizing note states the four-segment mint).
|
|
60
|
+
|
|
61
|
+
### Known residual (registered, not a regression)
|
|
62
|
+
|
|
63
|
+
- The zero-copy File backend's read-side inbound sync adopts disk divergence independent of session
|
|
64
|
+
intent: bytes a shell lands under the memory root during a read-only or declared-false session
|
|
65
|
+
can be adopted at a later materialize (checkpoint-resume included). Registered with three
|
|
66
|
+
candidate fixes pending a direction ruling; the disclosure, gate, and harvest boundaries above
|
|
67
|
+
all hold — this is the remaining channel, stated here so the boundary's edges are explicit.
|
|
68
|
+
|
|
3
69
|
## 5.25.0 — 2026-08-10
|
|
4
70
|
|
|
5
71
|
No API-BREAKING changes (exports grow only; every new member is optional/additive). One
|
|
@@ -4,5 +4,10 @@ export function defineAgent(def) {
|
|
|
4
4
|
e.code = "config.agent.invalid";
|
|
5
5
|
throw e;
|
|
6
6
|
}
|
|
7
|
+
if (def.memoryPersistenceCapable !== undefined && typeof def.memoryPersistenceCapable !== "boolean") {
|
|
8
|
+
const e = new Error(`defineAgent("${def.name}"): memoryPersistenceCapable must be a boolean when present — got ${JSON.stringify(def.memoryPersistenceCapable)} (a string "false" would silently read as capable).`);
|
|
9
|
+
e.code = "config.agent.invalid";
|
|
10
|
+
throw e;
|
|
11
|
+
}
|
|
7
12
|
return Object.freeze({ ...def });
|
|
8
13
|
}
|
|
@@ -682,6 +682,7 @@ export function createSendMessageTool(opts) {
|
|
|
682
682
|
...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {}),
|
|
683
683
|
...(ctx.interactiveTools === false ? { interactiveTools: false } : {}),
|
|
684
684
|
...(ctx.oneShot === true ? { oneShot: true } : {}),
|
|
685
|
+
...(ctx.memoryPersistenceCapable === false ? { memoryPersistenceCapable: false } : {}),
|
|
685
686
|
},
|
|
686
687
|
...(ctx.autoModeReview !== undefined ? { currentAutoModeReview: ctx.autoModeReview } : {}),
|
|
687
688
|
});
|
|
@@ -400,6 +400,7 @@ export declare function createSubagentResume(deps: {
|
|
|
400
400
|
handsReadOnly?: true;
|
|
401
401
|
interactiveTools?: false;
|
|
402
402
|
oneShot?: true;
|
|
403
|
+
memoryPersistenceCapable?: false;
|
|
403
404
|
};
|
|
404
405
|
/** The RESUMING caller's own handback-review seat (its trusted `ToolExecuteContext.autoModeReview`),
|
|
405
406
|
* same turn-bound reasoning as `currentOnQuestion` above: a resumed cycle is a completion like any
|
package/dist/agents/subagent.js
CHANGED
|
@@ -522,6 +522,9 @@ export function createSubagentResume(deps) {
|
|
|
522
522
|
...(entry.specSnapshot.handsReadOnly === true || deps.currentClamps?.handsReadOnly === true ? { handsReadOnly: true } : {}),
|
|
523
523
|
...(entry.specSnapshot.interactiveTools === false || deps.currentClamps?.interactiveTools === false ? { interactiveTools: false } : {}),
|
|
524
524
|
...(entry.specSnapshot.oneShot === true || deps.currentClamps?.oneShot === true ? { oneShot: true } : {}),
|
|
525
|
+
...(entry.specSnapshot.memoryPersistenceCapable === false || deps.currentClamps?.memoryPersistenceCapable === false
|
|
526
|
+
? { memoryPersistenceCapable: false }
|
|
527
|
+
: {}),
|
|
525
528
|
signal: abort.signal,
|
|
526
529
|
};
|
|
527
530
|
if (deps.registry !== undefined && deps.taskId !== undefined && deps.taskAccess !== undefined) {
|
|
@@ -1668,6 +1671,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1668
1671
|
};
|
|
1669
1672
|
const childThinking = def?.thinking ?? ctx.thinkingLevel;
|
|
1670
1673
|
const provenanceAgentName = agentName ?? def?.name;
|
|
1674
|
+
const childMemoryPersistenceCapable = ctx.memoryPersistenceCapable === false ? false : (def?.memoryPersistenceCapable ?? ctx.memoryPersistenceCapable);
|
|
1671
1675
|
const childOnAsk = ctx.onAsk !== undefined
|
|
1672
1676
|
? withDelegationProvenance(ctx.onAsk, {
|
|
1673
1677
|
parentToolCallId: ctx.toolCallId,
|
|
@@ -1689,6 +1693,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1689
1693
|
...(ctx.onQuestion !== undefined ? { onQuestion: ctx.onQuestion } : {}),
|
|
1690
1694
|
...(ctx.interactiveTools === false ? { interactiveTools: false } : {}),
|
|
1691
1695
|
...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {}),
|
|
1696
|
+
...(childMemoryPersistenceCapable !== undefined ? { memoryPersistenceCapable: childMemoryPersistenceCapable } : {}),
|
|
1692
1697
|
...(ctx.oneShot === true ? { oneShot: true } : {}),
|
|
1693
1698
|
...(ctx.clientContext !== undefined ? { clientContext: ctx.clientContext } : {}),
|
|
1694
1699
|
...(ctx.excludeTools !== undefined ? { excludeTools: [...ctx.excludeTools] } : {}),
|
package/dist/core/hooks.js
CHANGED
|
@@ -342,11 +342,12 @@ export async function runToolGate(input) {
|
|
|
342
342
|
await notifier.notifyAsync(() => input.persistedRules?.onResolved?.({ toolName: req.toolName, toolCallId, rule: hit }), "toolGate.persistedRuleResolved");
|
|
343
343
|
}
|
|
344
344
|
else if (hit !== undefined) {
|
|
345
|
+
const shownRule = inlineUntrusted(hit, 200);
|
|
345
346
|
const mandateNoun = persistedRuleMandate === "operator_always" ? "this deployment mandates per-call confirmation for shell commands (shellGate: always)" : "this tool carries egress/irreversibility marks (a mandated confirmation a rule cannot clear)";
|
|
346
347
|
decision = {
|
|
347
348
|
...decision,
|
|
348
|
-
persistedRuleShadowed:
|
|
349
|
-
message: `${decision.message !== undefined ? `${decision.message} ` : ""}(a persisted allow rule (${
|
|
349
|
+
persistedRuleShadowed: shownRule,
|
|
350
|
+
message: `${decision.message !== undefined ? `${decision.message} ` : ""}(a persisted allow rule (${shownRule}) matches this call but does not clear the ask — ${mandateNoun})`,
|
|
350
351
|
};
|
|
351
352
|
}
|
|
352
353
|
}
|
|
@@ -31,13 +31,15 @@ export function mergeInjections(project, personal) {
|
|
|
31
31
|
if (!personal)
|
|
32
32
|
return project;
|
|
33
33
|
const instruction = project.instruction || personal.instruction;
|
|
34
|
+
const readOnlyNotice = instruction === "" && project.readOnlyNotice !== undefined && personal.readOnlyNotice !== undefined ? project.readOnlyNotice : undefined;
|
|
34
35
|
const indexParts = [project.index, personal.index].filter((s) => Boolean(s && s.trim()));
|
|
35
36
|
const announcements = [...(project.announcements ?? []), ...(personal.announcements ?? [])];
|
|
36
37
|
const announceParts = [project.announceBlock, personal.announceBlock].filter((s) => Boolean(s && s.trim()));
|
|
37
|
-
const blockParts = [instruction, ...indexParts, ...announceParts];
|
|
38
|
+
const blockParts = [instruction || readOnlyNotice, ...indexParts, ...announceParts];
|
|
38
39
|
const indexSeed = project.indexSeed ?? personal.indexSeed;
|
|
39
40
|
return {
|
|
40
41
|
instruction,
|
|
42
|
+
...(readOnlyNotice !== undefined ? { readOnlyNotice } : {}),
|
|
41
43
|
...(indexParts.length > 0 ? { index: indexParts.join("\n\n") } : {}),
|
|
42
44
|
...(announcements.length > 0 ? { announcements } : {}),
|
|
43
45
|
...(announceParts.length > 0 ? { announceBlock: announceParts.join("\n\n") } : {}),
|
|
@@ -40,6 +40,37 @@ export declare const MEMORY_RECALL_DISCIPLINE = "Before answering questions abou
|
|
|
40
40
|
* would forge the very account the three-tier discipline forbids forging).
|
|
41
41
|
*/
|
|
42
42
|
export declare const MEMORY_PREFERENCE_DISCIPLINE = "When the user confirms a stored preference or fact still holds, refresh that entry's `last-confirmed: <YYYY-MM-DD>` frontmatter line (add it when absent). When you save a preference, add an `applies-when: <context>` frontmatter line naming when it applies. Both are plain frontmatter lines \u2014 write them yourself; nothing fills them in for you.";
|
|
43
|
+
/**
|
|
44
|
+
* Corrections for announcement segments that carry store-mutation guidance ("record a fresh entry …
|
|
45
|
+
* tombstone the old one", minted in an earlier writable session) a session cannot act on. The queue
|
|
46
|
+
* items are opaque strings (rewriting them would be text surgery over minted-at-enqueue wording),
|
|
47
|
+
* so the correction is a trailing coda, not a rewrite — in two scopes, because the two mounting
|
|
48
|
+
* seats speak about different things (an unqualified "the memory store is not writable" beside
|
|
49
|
+
* another, WRITABLE plane's instruction would negate that plane's guidance):
|
|
50
|
+
* - PLANE scope, attached by `inject()` right after a read-only layering's own announcement block:
|
|
51
|
+
* explicitly local to the immediately preceding notices, so a mixed dual-root merge keeps the
|
|
52
|
+
* writable plane's instruction and announcements fully actionable.
|
|
53
|
+
* - SESSION scope, attached by the runner at the block tail when the whole session cannot persist:
|
|
54
|
+
* there is no writable instruction left standing there (it is replaced or absent), so the global
|
|
55
|
+
* wording is accurate. Name-free by construction (#181 class).
|
|
56
|
+
*/
|
|
57
|
+
export declare const MEMORY_ANNOUNCEMENT_READONLY_PLANE_CODA = "The notices immediately above concern a READ-ONLY memory store: any guidance in them to record, update, or tombstone an entry cannot be applied to that store this session \u2014 surface it to the user instead of claiming it done.";
|
|
58
|
+
export declare const MEMORY_ANNOUNCEMENT_READONLY_CODA = "The memory store itself is not writable this session, so any guidance above to record, update, or tombstone a memory entry cannot be applied here \u2014 surface it to the user instead of claiming it done.";
|
|
59
|
+
/**
|
|
60
|
+
* The read-only counterpart of the `# Memory` write instruction. A run with memory mounted but no
|
|
61
|
+
* way to write it used to get an EMPTY instruction — correct in what it doesn't teach, but silent
|
|
62
|
+
* about the state itself, and a model asked "remember X" under that silence answers with a success
|
|
63
|
+
* receipt for a save that never happens (the confabulated-receipt shape). This section states the
|
|
64
|
+
* state instead. Mounted only where "you cannot save" is provably TRUE: by `inject()` for a
|
|
65
|
+
* read-only layering (writeScope null ⇒ chmod'd tree), and by the runner for handsReadOnly (the
|
|
66
|
+
* shell rides the read-only band), for a declared-unavailable session (`memoryPersistenceCapable:
|
|
67
|
+
* false` — where the runner's write gate also refuses the file channel, keeping the engine-refusal
|
|
68
|
+
* sentence true), and for a write-less roster the persistence inference cannot vouch for. A merely
|
|
69
|
+
* Write-less roster does NOT qualify — other tools can still write the root. Name-free by
|
|
70
|
+
* construction (#181 class — it names no tool), and NOT part of the CC-verbatim capture: CC has no
|
|
71
|
+
* read-only memory layering, so there is nothing to capture; the section is sema-authored.
|
|
72
|
+
*/
|
|
73
|
+
export declare const MEMORY_READONLY_NOTICE = "# Memory\n\nYou have READ-ONLY access to persistent memory in this session: stored notes are available below, but this session has no memory write channel \u2014 the engine will not accept writes into the memory store. If the user asks you to remember something for later, say plainly that you cannot save it in this session \u2014 never claim to have noted or remembered it.";
|
|
43
74
|
/** CC index-injection parameters: MEMORY.md's first 200 lines / 25KB enter the prompt. */
|
|
44
75
|
export declare const MEMORY_INDEX_MAX_LINES = 200;
|
|
45
76
|
export declare const MEMORY_INDEX_MAX_BYTES: number;
|
|
@@ -105,6 +136,16 @@ export interface MemoryInjection {
|
|
|
105
136
|
/** CC `# Memory` section (system-authority instruction — §0.3 逐字 surface). Empty for a read-only
|
|
106
137
|
* layering (no write channel to instruct). */
|
|
107
138
|
instruction: string;
|
|
139
|
+
/** Present exactly when this is a read-only LAYERING (writeScope null — the tree is chmod'd
|
|
140
|
+
* read-only, so no tool writes into it whatever the roster): the {@link MEMORY_READONLY_NOTICE}
|
|
141
|
+
* section stating that memory cannot be saved to, so the model declines "remember X" instead of
|
|
142
|
+
* issuing a confabulated success receipt. NOT set for `writeToolMounted:false` over a writable
|
|
143
|
+
* scope — there only the instruction-named tool is absent, other roster tools can still write the
|
|
144
|
+
* root, and the claim would be false (the runner mounts the notice for its own provably
|
|
145
|
+
* write-less shape, handsReadOnly). Kept as its OWN member (not folded into `instruction`) so the
|
|
146
|
+
* dual-root merge's "the write plane's instruction wins" falsy-OR keeps working: a read-only
|
|
147
|
+
* plane must never outrank a write plane's instruction. */
|
|
148
|
+
readOnlyNotice?: string;
|
|
108
149
|
/** The derived MEMORY.md index, truncated (200 lines / 25KB) and FENCED untrusted. Undefined when empty. */
|
|
109
150
|
index?: string;
|
|
110
151
|
/** design/138 S2-B (时机①) — the announcements DRAINED by this inject (queued by the previous
|
|
@@ -301,7 +342,7 @@ export declare class MemoryEngine {
|
|
|
301
342
|
ok: true;
|
|
302
343
|
} | {
|
|
303
344
|
ok: false;
|
|
304
|
-
code: ScanFinding["code"];
|
|
345
|
+
code: ScanFinding["code"] | "read_only_layering";
|
|
305
346
|
reason: string;
|
|
306
347
|
muted: boolean;
|
|
307
348
|
};
|
|
@@ -315,6 +356,9 @@ export declare class MemoryEngine {
|
|
|
315
356
|
reason: string;
|
|
316
357
|
};
|
|
317
358
|
sessionId?: string;
|
|
359
|
+
admitNothing?: {
|
|
360
|
+
reason: string;
|
|
361
|
+
};
|
|
318
362
|
}): Promise<HarvestReport>;
|
|
319
363
|
private harvestCore;
|
|
320
364
|
/**
|
|
@@ -34,6 +34,11 @@ export function buildMemoryInstruction(memoryDir, instructionFileName) {
|
|
|
34
34
|
}
|
|
35
35
|
export const MEMORY_RECALL_DISCIPLINE = "Before answering questions about earlier work, decisions, dates, people, or the user's preferences, look them up: `memory_search` finds entries by keyword and `memory_get` reads a full entry — the injected memory index only lists what exists. When a lookup comes up empty, say that you checked memory and found nothing instead of guessing.";
|
|
36
36
|
export const MEMORY_PREFERENCE_DISCIPLINE = "When the user confirms a stored preference or fact still holds, refresh that entry's `last-confirmed: <YYYY-MM-DD>` frontmatter line (add it when absent). When you save a preference, add an `applies-when: <context>` frontmatter line naming when it applies. Both are plain frontmatter lines — write them yourself; nothing fills them in for you.";
|
|
37
|
+
export const MEMORY_ANNOUNCEMENT_READONLY_PLANE_CODA = "The notices immediately above concern a READ-ONLY memory store: any guidance in them to record, update, or tombstone an entry cannot be applied to that store this session — surface it to the user instead of claiming it done.";
|
|
38
|
+
export const MEMORY_ANNOUNCEMENT_READONLY_CODA = "The memory store itself is not writable this session, so any guidance above to record, update, or tombstone a memory entry cannot be applied here — surface it to the user instead of claiming it done.";
|
|
39
|
+
export const MEMORY_READONLY_NOTICE = `# Memory
|
|
40
|
+
|
|
41
|
+
You have READ-ONLY access to persistent memory in this session: stored notes are available below, but this session has no memory write channel — the engine will not accept writes into the memory store. If the user asks you to remember something for later, say plainly that you cannot save it in this session — never claim to have noted or remembered it.`;
|
|
37
42
|
export const MEMORY_INDEX_MAX_LINES = 200;
|
|
38
43
|
export const MEMORY_INDEX_MAX_BYTES = 25 * 1024;
|
|
39
44
|
export const STUB_ARCHIVED_LINE = "[body archived — request hydration by listing the slug in memory/.hydrate]";
|
|
@@ -41,6 +46,7 @@ export const DEFAULT_MAX_MEMORY_FILES = 500;
|
|
|
41
46
|
export const DEFAULT_HARVEST_DEADLINE_MS = 5_000;
|
|
42
47
|
export const DEFAULT_HARVEST_FILE_BUDGET = 2_000;
|
|
43
48
|
export const MASS_DELETION_FUSE_RATIO = 0.5;
|
|
49
|
+
let indexCaptureSeq = 0;
|
|
44
50
|
export class MemoryEngine {
|
|
45
51
|
backend;
|
|
46
52
|
memoryDir;
|
|
@@ -425,6 +431,7 @@ export class MemoryEngine {
|
|
|
425
431
|
inject(handle, opts) {
|
|
426
432
|
const writeChannel = handle.writeScope !== null && opts?.writeToolMounted !== false;
|
|
427
433
|
const instruction = writeChannel ? buildMemoryInstruction(handle.writableRoot) : "";
|
|
434
|
+
const readOnlyNotice = handle.writeScope === null ? MEMORY_READONLY_NOTICE : undefined;
|
|
428
435
|
const indexPath = join(handle.writableRoot, MEMORY_INDEX_FILENAME);
|
|
429
436
|
const onDisk = handle.indexOnDiskUntrusted === true ? undefined : readSafe(indexPath);
|
|
430
437
|
const indexText = onDisk !== undefined && onDisk.trim() !== "" ? onDisk : handle.indexText;
|
|
@@ -440,14 +447,17 @@ export class MemoryEngine {
|
|
|
440
447
|
if (drained.queue.length > 0 || drained.folded > 0) {
|
|
441
448
|
announcements = drained.queue;
|
|
442
449
|
announceBlock = renderAnnouncements(drained.queue, drained.folded);
|
|
450
|
+
if (handle.writeScope === null)
|
|
451
|
+
announceBlock = `${announceBlock}\n\n${MEMORY_ANNOUNCEMENT_READONLY_PLANE_CODA}`;
|
|
443
452
|
}
|
|
444
453
|
}
|
|
445
454
|
catch (err) {
|
|
446
455
|
this.discloseAnnounceFailure("inject drain", err);
|
|
447
456
|
}
|
|
448
|
-
const block = [instruction, index, announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
|
|
457
|
+
const block = [instruction || readOnlyNotice, index, announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
|
|
449
458
|
return {
|
|
450
459
|
instruction,
|
|
460
|
+
...(readOnlyNotice !== undefined ? { readOnlyNotice } : {}),
|
|
451
461
|
...(index !== undefined ? { index } : {}),
|
|
452
462
|
...(announcements !== undefined ? { announcements } : {}),
|
|
453
463
|
...(announceBlock !== undefined ? { announceBlock } : {}),
|
|
@@ -459,8 +469,14 @@ export class MemoryEngine {
|
|
|
459
469
|
const root = handle.writableRoot;
|
|
460
470
|
if (canonicalPath !== root && !canonicalPath.startsWith(`${root}${sep}`))
|
|
461
471
|
return { ok: true };
|
|
462
|
-
if (handle.writeScope === null)
|
|
463
|
-
return {
|
|
472
|
+
if (handle.writeScope === null) {
|
|
473
|
+
return {
|
|
474
|
+
ok: false,
|
|
475
|
+
code: "read_only_layering",
|
|
476
|
+
reason: "this session's memory is read-only (no write scope) — the engine does not accept writes into the memory domain. Nothing was written.",
|
|
477
|
+
muted: false,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
464
480
|
const findings = [];
|
|
465
481
|
const nameFinding = scanMemoryFileName(relative(root, canonicalPath));
|
|
466
482
|
if (nameFinding !== undefined)
|
|
@@ -512,10 +528,12 @@ export class MemoryEngine {
|
|
|
512
528
|
warnings: [],
|
|
513
529
|
};
|
|
514
530
|
const writeScope = handle.writeScope;
|
|
515
|
-
if (writeScope === null) {
|
|
531
|
+
if (writeScope === null || opts?.admitNothing !== undefined) {
|
|
516
532
|
const roFindings = this.backend.drainInboundFindings?.();
|
|
517
533
|
if (roFindings !== undefined && roFindings.length > 0)
|
|
518
534
|
report.inboundFindings = roFindings;
|
|
535
|
+
if (opts?.admitNothing !== undefined)
|
|
536
|
+
report.warnings.push(opts.admitNothing.reason);
|
|
519
537
|
return report;
|
|
520
538
|
}
|
|
521
539
|
try {
|
|
@@ -1240,7 +1258,7 @@ export class MemoryEngine {
|
|
|
1240
1258
|
}
|
|
1241
1259
|
}
|
|
1242
1260
|
if (!landed)
|
|
1243
|
-
writeFileSync(`${dest}.${process.pid}`, text, { encoding: "utf8", flag: "wx" });
|
|
1261
|
+
writeFileSync(`${dest}.${process.pid}.${indexCaptureSeq++}`, text, { encoding: "utf8", flag: "wx" });
|
|
1244
1262
|
captured = true;
|
|
1245
1263
|
}
|
|
1246
1264
|
catch (err) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, } from "./engine.js";
|
|
1
|
+
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, } from "./engine.js";
|
|
2
2
|
export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type MemoryGetDetails, } from "./tools.js";
|
|
3
3
|
export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
|
|
4
4
|
export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, type ScannedEntryFile } from "./file-backend.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, } from "./engine.js";
|
|
1
|
+
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, } from "./engine.js";
|
|
2
2
|
export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, } from "./tools.js";
|
|
3
3
|
export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
|
|
4
4
|
export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH } from "./file-backend.js";
|
|
@@ -281,8 +281,15 @@ export async function prepareCcImport(opts) {
|
|
|
281
281
|
skipped.push({ rule: String(entry), reason: "settings entry is not a string" });
|
|
282
282
|
continue;
|
|
283
283
|
}
|
|
284
|
-
if (!entry.startsWith("Bash("))
|
|
284
|
+
if (!entry.startsWith("Bash(")) {
|
|
285
|
+
const reason = /^[A-Za-z][A-Za-z0-9_]*\(.*\)$/.test(entry)
|
|
286
|
+
? "unsupported.tool: only Bash(...) command rules import in v1 — this entry stays in the settings file, unimported"
|
|
287
|
+
: /^[A-Za-z][A-Za-z0-9_-]*$/.test(entry)
|
|
288
|
+
? "unsupported.form: a bare tool-name entry is a name-set item, not a command rule — it stays in the settings file, unimported"
|
|
289
|
+
: "unsupported.form: not a Bash(...) command rule — it stays in the settings file, unimported";
|
|
290
|
+
skipped.push({ rule: entry, reason });
|
|
285
291
|
continue;
|
|
292
|
+
}
|
|
286
293
|
const parsed = parseAllowRuleText(entry);
|
|
287
294
|
if ("reject" in parsed) {
|
|
288
295
|
skipped.push({ rule: entry, reason: `${parsed.reject.code}: ${parsed.reject.message}` });
|
|
@@ -95,10 +95,10 @@ export declare function buildStaleOffloadPointer(toolName: string, ref: string,
|
|
|
95
95
|
* The session transcript is NEVER touched — this runs on the outgoing {@link Context} only.
|
|
96
96
|
* Error results, image/document-bearing blocks' non-text parts, already-offloaded previews, and
|
|
97
97
|
* replacements that would save < `minSavingsChars` are left verbatim. The full text is persisted
|
|
98
|
-
* under a deterministic content-digested ref (
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* readable back via `read_tool_result`.
|
|
98
|
+
* under a deterministic content-digested ref (`buildToolResultRef(sessionId, toolCallId,
|
|
99
|
+
* toolResultContentSegment(text))` — a `~`-separated three-segment mint whose digest is its OWN
|
|
100
|
+
* segment; write-once, so re-projection on every turn re-puts a no-op; the digest exists because
|
|
101
|
+
* tool-call ids carry no cross-turn uniqueness contract), readable back via `read_tool_result`.
|
|
102
102
|
*/
|
|
103
103
|
export declare function projectStaleToolResults(context: Context, cfg: ResolvedStaleToolResultOffload, store: ToolResultStore, sessionId: string,
|
|
104
104
|
/** Run-scoped cache of refs already persisted by THIS run (独立复审 MED,已修): without it the
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { emitTrace } from "../trace.js";
|
|
3
|
-
import { buildToolResultRef, OFFLOAD_TOOL_NAME, PERSISTED_OUTPUT_PREFIX } from "../tool-result-store.js";
|
|
2
|
+
import { buildToolResultRef, OFFLOAD_TOOL_NAME, PERSISTED_OUTPUT_PREFIX, toolResultContentSegment, toolResultProvenanceOf, } from "../tool-result-store.js";
|
|
4
3
|
export function buildWorkingFileAttachments(spec, prepared) {
|
|
5
4
|
if (spec.compaction?.attachWorkingFiles === false || !prepared.readTaskFile)
|
|
6
5
|
return undefined;
|
|
@@ -73,13 +72,13 @@ export async function projectStaleToolResults(context, cfg, store, sessionId, wr
|
|
|
73
72
|
const text = toolResultText(msg);
|
|
74
73
|
if (text.startsWith(PERSISTED_OUTPUT_PREFIX))
|
|
75
74
|
continue;
|
|
76
|
-
const ref = buildToolResultRef(sessionId,
|
|
75
|
+
const ref = buildToolResultRef(sessionId, msg.toolCallId, toolResultContentSegment(text));
|
|
77
76
|
const pointer = buildStaleOffloadPointer(toolName, ref, text.length);
|
|
78
77
|
if (text.length - pointer.length < cfg.minSavingsChars)
|
|
79
78
|
continue;
|
|
80
79
|
if (!writtenRefs.has(ref)) {
|
|
81
80
|
try {
|
|
82
|
-
await store.put(ref, text);
|
|
81
|
+
await store.put(ref, text, toolResultProvenanceOf(sessionId));
|
|
83
82
|
writtenRefs.add(ref);
|
|
84
83
|
}
|
|
85
84
|
catch {
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* design/157 B15 一期 (P6 相位抽取) — prepareTask's long-term-memory phase, verbatim. The ONLY inputs
|
|
3
|
-
* are the five fields below (measured, not assumed: spec.memory + deps.* + the three ambient values);
|
|
4
|
-
* the ONLY outputs are the memory-engine session and the composed injection block. `deps.onError`
|
|
5
|
-
* call order and payloads are part of the contract (event-sequence snapshot pin recorded across the
|
|
6
|
-
* move). Throws pass through unchanged: a `config.memory_*`-coded violation is a DELIBERATE refusal
|
|
7
|
-
* (design/142 S1 硬门) and must keep failing prepare loudly.
|
|
8
|
-
*/
|
|
9
1
|
import type { BeforeWriteHook, RunnerDeps, TaskSpec, ToolSpec } from "../types.js";
|
|
10
2
|
import type { Prepared } from "./prepare-task.js";
|
|
11
3
|
export interface PrepareMemoryInput {
|
|
@@ -18,15 +10,42 @@ export interface PrepareMemoryInput {
|
|
|
18
10
|
};
|
|
19
11
|
/**
|
|
20
12
|
* #181-F5 — whether a tool NAMED `Write` (the tool the CC `# Memory` instruction names) is on the
|
|
21
|
-
* ASSEMBLED roster this run
|
|
22
|
-
* `tools.some(t => t.name === "Write") && !exclude.includes("Write")
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
13
|
+
* ASSEMBLED roster this run, not excluded, AND can actually REACH the host-side memory root:
|
|
14
|
+
* prepare-task's `tools.some(t => t.name === "Write") && !exclude.includes("Write")` plus the
|
|
15
|
+
* remote-env conjunct (a hand-band Write on a remote ExecutionEnv writes the sandbox filesystem,
|
|
16
|
+
* not the host memory root — it does not count unless the deployment declares the mount shared
|
|
17
|
+
* via `memoryPersistenceCapable: true`). Name occupancy IS the channel declaration for the local
|
|
18
|
+
* arms — a hands-less run that mounts its own `Write` counts, and an `excludeTools: ["Write"]`
|
|
19
|
+
* run reads as unmounted whatever the hands band did. Threaded into `engine.inject` so a run with
|
|
20
|
+
* a writable memory scope but no write channel is not instructed to call a tool that is not on
|
|
21
|
+
* its roster (or cannot reach the store); the RB-276 index seed follows the same gate.
|
|
28
22
|
*/
|
|
29
23
|
writeToolsMounted: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The session-wide persistence verdict: `TaskSpec.memoryPersistenceCapable` (the deployment's own
|
|
26
|
+
* statement — the only honest channel for a custom memory writer persisting through its closure,
|
|
27
|
+
* which no inference can see) when set, else the KNOWN-store-path inference: a mounted,
|
|
28
|
+
* non-excluded file-write tool (Write/Edit/NotebookEdit) or a write-capable shell. A generic
|
|
29
|
+
* write-effect tool does NOT count — a mail sender's side effect is not a memory store, and
|
|
30
|
+
* counting it re-opened the silent-confabulation hole. `false` ⇒ this phase mounts
|
|
31
|
+
* {@link MEMORY_READONLY_NOTICE} where the engine is silent.
|
|
32
|
+
*/
|
|
33
|
+
rosterCanPersist: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The raw `TaskSpec.memoryPersistenceCapable` DECLARATION, kept separate from the composed
|
|
36
|
+
* {@link rosterCanPersist} verdict because the two drive different arms:
|
|
37
|
+
* - `true` (declared) RETRACTS the engine's own read-only notice — the deployment vouches for a
|
|
38
|
+
* persistence channel the engine cannot see (a custom writer persisting through its closure),
|
|
39
|
+
* so "you cannot save" beside it would be a false claim. An INFERRED-true roster must NOT
|
|
40
|
+
* retract: over a writeScope-null layering the engine will refuse those very file writes
|
|
41
|
+
* (`read_only_layering`), so the roster's write tools prove nothing about THIS store and
|
|
42
|
+
* stripping the notice re-opens the silent-confabulation hole for exactly the state the
|
|
43
|
+
* notice was built for.
|
|
44
|
+
* - `false` (declared) also CLOSES the file-tool write channel into the writable memory root
|
|
45
|
+
* (the write gate refuses), so the mounted notice's "the engine will not accept writes into
|
|
46
|
+
* the memory store" stays a true statement instead of a disclosure the store then contradicts.
|
|
47
|
+
*/
|
|
48
|
+
memoryPersistenceDeclared?: boolean;
|
|
30
49
|
/**
|
|
31
50
|
* design/178 ②-1 — whether the `memory_search`/`memory_get` pair PASSED its early mount conjuncts
|
|
32
51
|
* (exclusion + name occupancy, decided in prepare-task BEFORE this phase). True ⇒ this phase builds
|