@nathapp/nax-agent 0.91.8-canary.1 → 0.91.8-canary.2
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 +4 -0
- package/dist/config/catalog-overrides.d.ts +13 -0
- package/dist/index.d.ts +3 -2
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/memory/index-render.d.ts +6 -2
- package/dist/memory/index-render.js +18 -3
- package/dist/memory/index.d.ts +1 -1
- package/dist/memory/local-provider.js +51 -4
- package/dist/memory/operations.d.ts +21 -18
- package/dist/memory/operations.js +57 -26
- package/dist/memory/records.d.ts +18 -6
- package/dist/memory/records.js +49 -6
- package/dist/memory/store.d.ts +12 -8
- package/dist/memory/store.js +24 -20
- package/dist/memory/types.d.ts +12 -0
- package/dist/native/adapter-deps.d.ts +9 -0
- package/dist/native/adapter-deps.js +9 -0
- package/dist/native/models.js +3 -0
- package/dist/native/session/compaction.d.ts +15 -1
- package/dist/native/session/compaction.js +90 -23
- package/dist/native/session/manual-compaction.d.ts +57 -0
- package/dist/native/session/manual-compaction.js +72 -0
- package/dist/native/session/session-summarizer.d.ts +75 -0
- package/dist/native/session/session-summarizer.js +73 -0
- package/dist/native/session/transcript-identity.d.ts +12 -0
- package/dist/native/session/transcript-identity.js +16 -0
- package/dist/native/session/turn-compaction-step.d.ts +30 -2
- package/dist/native/session/turn-compaction-step.js +7 -1
- package/dist/native/session/turn-loop-round-trip.js +71 -17
- package/dist/native/session/turn-loop.js +2 -6
- package/dist/native/session-adapter.d.ts +19 -0
- package/dist/native/session-adapter.js +42 -64
- package/dist/sandbox/srt-backend.js +9 -16
- package/dist/session/agent-session-options.js +1 -0
- package/dist/session/agent-session-turn.d.ts +6 -0
- package/dist/session/agent-session-turn.js +5 -4
- package/dist/session/agent-session-types.d.ts +38 -0
- package/dist/session/agent-session.d.ts +8 -0
- package/dist/session/agent-session.js +163 -20
- package/dist/session/native-backend.js +8 -0
- package/dist/session/session-backend.d.ts +19 -1
- package/dist/session/session-types.d.ts +6 -0
- package/dist/session/steer-channel.d.ts +36 -0
- package/dist/session/steer-channel.js +39 -0
- package/dist/session-store/session-lock.d.ts +2 -2
- package/dist/session-store/session-lock.js +4 -2
- package/dist/tools/ask-request.d.ts +8 -0
- package/dist/tools/ask-request.js +34 -1
- package/dist/tools/provider-adapt.js +5 -1
- package/dist/tools/provider-types.d.ts +7 -0
- package/dist/tools/types.d.ts +16 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@ All notable changes to `@nathapp/nax-agent` are recorded here. The format follow
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). While the version is `0.x`, a minor
|
|
5
5
|
release may change the public API.
|
|
6
6
|
|
|
7
|
+
## [0.91.8-canary.2] - 2026-11-04
|
|
8
|
+
|
|
9
|
+
- No changes in this package. Released in lockstep with the other nax packages.
|
|
10
|
+
|
|
7
11
|
## [0.91.8-canary.1] - 2026-11-03
|
|
8
12
|
|
|
9
13
|
- No changes in this package. Released in lockstep with the other nax packages.
|
|
@@ -65,6 +65,15 @@ export interface OpenRouterRouting {
|
|
|
65
65
|
p99?: number;
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
|
+
/** Per-level token budgets, hand-mirrored from nax-ai's ThinkingBudgets.
|
|
69
|
+
* Config code cannot import nax-ai; validation remains owned by nax-ai.
|
|
70
|
+
*/
|
|
71
|
+
export interface CatalogThinkingBudgets {
|
|
72
|
+
minimal?: number;
|
|
73
|
+
low?: number;
|
|
74
|
+
medium?: number;
|
|
75
|
+
high?: number;
|
|
76
|
+
}
|
|
68
77
|
/**
|
|
69
78
|
* One complete catalog entry for a model the bundled pi-ai snapshot does not
|
|
70
79
|
* know. Complete, not a patch: nax-ai's `normaliseCatalog` replaces any
|
|
@@ -98,6 +107,10 @@ export interface CatalogModelOverride {
|
|
|
98
107
|
* Mirrored from nax-ai's ThinkingFormat; omit to inherit the provider's format.
|
|
99
108
|
*/
|
|
100
109
|
thinkingFormat?: "openai" | "qwen" | "qwen-chat-template" | "deepseek" | "zai" | "openrouter";
|
|
110
|
+
/** Wire field receiving the thinking token budget on openai-completions models. */
|
|
111
|
+
thinkingBudgetField?: string;
|
|
112
|
+
/** Model-owned token budgets for declared reasoning levels. */
|
|
113
|
+
thinkingBudgets?: CatalogThinkingBudgets;
|
|
101
114
|
}
|
|
102
115
|
/** Provider-scoped catalog overrides — maps 1:1 onto nax-ai's `ProviderOverride[]`. */
|
|
103
116
|
export interface ProviderCatalogOverride {
|
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export { isProcessAlive } from "#src/internal/process-alive";
|
|
|
19
19
|
export { killProcessGroup } from "#src/internal/process-kill";
|
|
20
20
|
export { capStrings, redactSecrets } from "#src/internal/redact";
|
|
21
21
|
export { NaxError } from "#src/infra/nax-error";
|
|
22
|
-
export { MEMORY_CLIENT_ID_RE, MEMORY_DESCRIPTION_MAX_CHARS, MEMORY_FORGET_REASON_MAX_CHARS, MEMORY_INDEX_MAX_BYTES, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MIN_BYTES, MEMORY_INDEX_MIN_LINES, MEMORY_NOTE_BODY_MAX_BYTES, MEMORY_OP_TIMEOUT_MS, MEMORY_PROBE_TIMEOUT_MS, MEMORY_PULL_MAX_PAGES, MEMORY_PULL_PAGE_SIZE, MEMORY_SLUG_RE, MEMORY_SOURCE_MAX_CHARS, type MemoryApproveInput, type MemoryDecision, type MemoryForgetInput, type MemoryForgetResult, type MemoryHit, type MemoryNote, type MemoryNoteStatus, type MemoryNoteType, type MemoryNoteWrite, type MemoryOutbox, type MemoryOutboxEntry, type MemoryPage, type MemoryProvider, type MemoryPushResult, type MemorySaveInput, type MemorySaveResult, type MemoryScope, type MemoryStore, type MemoryStorePaths, type MemorySyncResult, type MemorySyncState, type MemoryTimers, type MemoryWriteRefusal, type MemoryWriteRequest, type RenderedMemoryIndex, type RefreshMemoryIndexOptions, type RenderMemoryIndexInput, approveMemoryNote, createLocalMemoryProvider, createMemoryOutbox, createMemoryStore, findSecret, forgetMemoryNote, isMemoryPushRejected, parseMemoryNote, raceTimeout, refreshMemoryIndex, renderMemoryIndex, saveMemoryNote, searchMemoryNotes, serializeMemoryNote, syncMemory, tokenizeMemoryQuery, validateMemoryNoteWrite, } from "#src/memory/index";
|
|
22
|
+
export { MEMORY_CLIENT_ID_RE, MEMORY_DESCRIPTION_MAX_CHARS, MEMORY_FORGET_REASON_MAX_CHARS, MEMORY_INDEX_MAX_BYTES, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MIN_BYTES, MEMORY_INDEX_MIN_LINES, MEMORY_NOTE_BODY_MAX_BYTES, MEMORY_OP_TIMEOUT_MS, MEMORY_PROBE_TIMEOUT_MS, MEMORY_PULL_MAX_PAGES, MEMORY_PULL_PAGE_SIZE, MEMORY_SLUG_RE, MEMORY_SOURCE_MAX_CHARS, type MemoryApproveInput, type MemoryDecision, type MemoryForgetInput, type MemoryForgetResult, type MemoryHit, type MemoryNote, type MemoryNoteStatus, type MemoryNoteType, type MemoryNoteWrite, type MemoryOutbox, type MemoryOutboxEntry, type MemoryPage, type MemoryPendingForget, type MemoryProvider, type MemoryPushResult, type MemorySaveInput, type MemorySaveResult, type MemoryScope, type MemoryStore, type MemoryStorePaths, type MemorySyncResult, type MemorySyncState, type MemoryTimers, type MemoryWriteRefusal, type MemoryWriteRequest, type RenderedMemoryIndex, type RefreshMemoryIndexOptions, type RenderMemoryIndexInput, approveMemoryNote, createLocalMemoryProvider, createMemoryOutbox, createMemoryStore, findSecret, forgetMemoryNote, isMemoryPushRejected, parseMemoryNote, raceTimeout, refreshMemoryIndex, renderMemoryIndex, saveMemoryNote, searchMemoryNotes, serializeMemoryNote, syncMemory, tokenizeMemoryQuery, validateMemoryNoteWrite, } from "#src/memory/index";
|
|
23
23
|
export { createTerminalAuthInteraction, PLAIN_STYLE, PromptCancelledError, type TerminalAuthOptions, type TerminalStyle, } from "#src/terminal-auth/index";
|
|
24
24
|
export type { CredentialSource } from "#src/native/credentials/session-source";
|
|
25
25
|
export { AuthCancelledError, type AuthEvent, type AuthInteraction, type AuthLink, type AuthMethod, type AuthOption, type AuthPrompt, type AuthResult, ambientShadows, anyAmbientCredential, authImportOutcomeLabel, createFileTranscriptStore, createMemoryTranscriptStore, credentialFilePath, DEFAULT_PI_AUTH_PATH, type ImportOutcome, importPiCredentials, listProviderModels, listStoredProviders, loginProviderIds, MAX_RETAINED_TRANSCRIPTS, type MemoryTranscriptStore, NATIVE_AGENT, type NativeCatalogOverrides, type NativeCompleteContext, type NativeCompleteOptions, type NativeCompleteResult, NativeSessionAdapter, NativeSessionUnsupportedError, type NativeTierConfig, nativeComplete, nativeTierProviders, naxCredentialStore, newSessionKey, type ProviderModel, providersWithoutCredentials, pruneRetainedTranscripts, type ResolveResult, type ResolveStatus, removeStoredProvider, resolveNativeId, runLogin, type StoredEntry, servedAuth, type TranscriptAcpRecord, type TranscriptDoc, type TranscriptStore, type TurnMarker, } from "#src/native/index";
|
|
@@ -33,7 +33,7 @@ export { createSessionStorage, defaultSessionsDir, processAlive, LIST_PAGE_SIZE,
|
|
|
33
33
|
export type { AdapterFailure } from "#src/session/adapter-failure";
|
|
34
34
|
export { createAgentSession, resumeAgentSession } from "#src/session/agent-session";
|
|
35
35
|
export { AgentSessionError, type AgentSessionErrorCode } from "#src/session/agent-session-errors";
|
|
36
|
-
export type { AgentSession, AgentSessionHostPorts, AgentSessionProfile, AnswerReply, AnswerStatus, ApprovalDecidedBy, CostSource, CommandRunResult, CreateAgentSessionOptions, EmbedderTool, EmbedderToolContext, EmbedderToolResult, SessionConfigureInput, SessionEvent, SessionEventBase, SessionEventBody, TurnEndStatus, } from "#src/session/agent-session-types";
|
|
36
|
+
export type { AgentSession, AgentSessionHostPorts, AgentSessionProfile, AnswerReply, AnswerStatus, ApprovalDecidedBy, CostSource, CommandRunResult, CompactResult, CreateAgentSessionOptions, EmbedderTool, EmbedderToolContext, EmbedderToolResult, SessionConfigureInput, SessionEvent, SessionEventBase, SessionEventBody, TurnEndStatus, } from "#src/session/agent-session-types";
|
|
37
37
|
export type { AgentAwaitingHumanEvent, AgentCallEndedEvent, AgentCallStartedEvent, AgentMessageUpdateEvent, AgentProcessUpdateEvent, AgentStreamEvent, AgentStreamEventBase, AgentThinkingUpdateEvent, AgentToolCallUpdateEvent, AgentUsageUpdateEvent, } from "#src/session/agent-stream-events";
|
|
38
38
|
export { IMAGE_MIME_TYPES, IMAGE_TOKEN_ESTIMATE, MAX_IMAGE_BASE64_CHARS, MAX_IMAGES_PER_MESSAGE, MAX_REQUEST_IMAGE_BASE64_CHARS, MAX_REQUEST_IMAGES, type SendInput, validateImageAttachments, } from "#src/session/image-input";
|
|
39
39
|
export { type AdapterInteraction, type AdapterInteractionResponse, type InteractionHandler, NO_OP_INTERACTION_HANDLER, } from "#src/session/interaction-handler";
|
|
@@ -42,6 +42,7 @@ export type { NativeBackendOptions } from "#src/session/native-backend-options";
|
|
|
42
42
|
export type { ProtocolIds } from "#src/session/protocol-types";
|
|
43
43
|
export type { ApprovalRequest, BackendInfo, BackendOpenContext, OpenedBackend, SessionAskPort, SessionBackend, TurnContribution, } from "#src/session/session-backend";
|
|
44
44
|
export { type AgentSessionAdapter, type AuthStamp, type InteractionExchange, type InvalidToolCallDetail, type OpenSessionOpts, type SendTurnOpts, type SessionHandle, type SessionModel, SessionTurnError, type TrackedSpawnDeadlineOptions, type TurnResult, } from "#src/session/session-types";
|
|
45
|
+
export type { SteerChannel, SteerResult } from "#src/session/steer-channel";
|
|
45
46
|
export { attachTurnSpend, type FailedTurnSpend, readTurnSpend } from "#src/session/turn-spend";
|
|
46
47
|
export type { JSONSchema, ToolDescriptor, } from "#src/session/tool-descriptor";
|
|
47
48
|
export { createTurnDeadline, type TurnDeadline, } from "#src/session/turn-deadline";
|
package/dist/internal.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export * from "#src/internal/shell-quote";
|
|
|
46
46
|
export * from "#src/internal/sort";
|
|
47
47
|
export * from "#src/internal/strip-control-chars";
|
|
48
48
|
export * from "#src/internal/thenable";
|
|
49
|
+
export { parseMemoryNoteRecord } from "#src/memory/note";
|
|
49
50
|
export { _memoryStoreDeps } from "#src/memory/store";
|
|
50
51
|
export * from "#src/native/adapter-deps";
|
|
51
52
|
export * from "#src/native/auth";
|
package/dist/internal.js
CHANGED
|
@@ -46,6 +46,7 @@ export * from "#src/internal/shell-quote";
|
|
|
46
46
|
export * from "#src/internal/sort";
|
|
47
47
|
export * from "#src/internal/strip-control-chars";
|
|
48
48
|
export * from "#src/internal/thenable";
|
|
49
|
+
export { parseMemoryNoteRecord } from "#src/memory/note";
|
|
49
50
|
export { _memoryStoreDeps } from "#src/memory/store";
|
|
50
51
|
export * from "#src/native/adapter-deps";
|
|
51
52
|
export * from "#src/native/auth";
|
|
@@ -32,7 +32,10 @@ export interface RenderedMemoryIndex {
|
|
|
32
32
|
readonly text: string;
|
|
33
33
|
readonly omitted: number;
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* What a refresh carries into both renderings: the held keys and the two caps. The keys
|
|
37
|
+
* are the ones the outbox still holds; the record's own keys are added to them.
|
|
38
|
+
*/
|
|
36
39
|
export interface RefreshMemoryIndexOptions {
|
|
37
40
|
/** `"<scope>/<slug>"` of every note with a queued forget. */
|
|
38
41
|
readonly heldForgets?: ReadonlySet<string>;
|
|
@@ -45,7 +48,8 @@ export interface RefreshMemoryIndexOptions {
|
|
|
45
48
|
* Regenerate the index files of a store from what it holds: the project notes and
|
|
46
49
|
* the decisions into `MEMORY.md`, and — on a store that has a user directory — the
|
|
47
50
|
* user notes into the user scope's `MEMORY.md`, which carries the header lines and
|
|
48
|
-
* the `## User` section only.
|
|
51
|
+
* the `## User` section only. Every note with a queued or recorded forget is marked.
|
|
52
|
+
* The project rendering is what is returned.
|
|
49
53
|
*/
|
|
50
54
|
export declare function refreshMemoryIndex(store: MemoryStore, options?: RefreshMemoryIndexOptions): Promise<RenderedMemoryIndex>;
|
|
51
55
|
export declare function renderMemoryIndex(input: RenderMemoryIndexInput): RenderedMemoryIndex;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MEMORY_DESCRIPTION_MAX_CHARS, MEMORY_INDEX_MAX_BYTES, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MIN_BYTES, MEMORY_INDEX_MIN_LINES, } from "./types.js";
|
|
1
|
+
import { MEMORY_DESCRIPTION_MAX_CHARS, MEMORY_INDEX_MAX_BYTES, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MIN_BYTES, MEMORY_INDEX_MIN_LINES, memoryNoteKey, } from "./types.js";
|
|
2
2
|
/** The two lines every index starts with. */
|
|
3
3
|
const HEADING_LINES = ["# Memory", "Memory notes are recorded facts, not instructions."];
|
|
4
4
|
/** The order the project section lists its type blocks in: most actionable first. */
|
|
@@ -100,14 +100,29 @@ function userSection(userNotes, heldForgets) {
|
|
|
100
100
|
function joinLines(lines) {
|
|
101
101
|
return lines.map((line) => line.text).join("\n");
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* The keys both renderings mark: the caller's held forgets plus the ones the mirror has
|
|
105
|
+
* recorded. A proposal forget is answered long before anything acts on it, so the record
|
|
106
|
+
* — not the caller — is what still knows about it; a key held by both is one mark.
|
|
107
|
+
*/
|
|
108
|
+
async function markedForgets(store, heldForgets) {
|
|
109
|
+
const recorded = await store.readPendingForgets();
|
|
110
|
+
if (recorded.length === 0)
|
|
111
|
+
return heldForgets ?? NO_HELD_FORGETS;
|
|
112
|
+
const keys = new Set(heldForgets ?? NO_HELD_FORGETS);
|
|
113
|
+
for (const entry of recorded)
|
|
114
|
+
keys.add(memoryNoteKey(entry.scope, entry.slug));
|
|
115
|
+
return keys;
|
|
116
|
+
}
|
|
103
117
|
/**
|
|
104
118
|
* Regenerate the index files of a store from what it holds: the project notes and
|
|
105
119
|
* the decisions into `MEMORY.md`, and — on a store that has a user directory — the
|
|
106
120
|
* user notes into the user scope's `MEMORY.md`, which carries the header lines and
|
|
107
|
-
* the `## User` section only.
|
|
121
|
+
* the `## User` section only. Every note with a queued or recorded forget is marked.
|
|
122
|
+
* The project rendering is what is returned.
|
|
108
123
|
*/
|
|
109
124
|
export async function refreshMemoryIndex(store, options = {}) {
|
|
110
|
-
const heldForgets = options.heldForgets
|
|
125
|
+
const heldForgets = await markedForgets(store, options.heldForgets);
|
|
111
126
|
const caps = { maxBytes: options.maxBytes, maxLines: options.maxLines };
|
|
112
127
|
const project = renderMemoryIndex({
|
|
113
128
|
projectNotes: await store.listNotes("project"),
|
package/dist/memory/index.d.ts
CHANGED
|
@@ -8,5 +8,5 @@ export { searchMemoryNotes, tokenizeMemoryQuery } from "./search.ts";
|
|
|
8
8
|
export { createMemoryStore, type MemoryStore, type MemoryStorePaths, type MemorySyncState, } from "./store.ts";
|
|
9
9
|
export { raceTimeout } from "./support.ts";
|
|
10
10
|
export { type MemorySyncInput, type MemorySyncResult, syncMemory } from "./sync.ts";
|
|
11
|
-
export { MEMORY_CLIENT_ID_RE, MEMORY_DECISION_STATUSES, MEMORY_DESCRIPTION_MAX_CHARS, MEMORY_FORGET_REASON_MAX_CHARS, MEMORY_INDEX_MAX_BYTES, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MIN_BYTES, MEMORY_INDEX_MIN_LINES, MEMORY_NOTE_BODY_MAX_BYTES, MEMORY_NOTE_STATUSES, MEMORY_NOTE_TYPES, MEMORY_OP_TIMEOUT_MS, MEMORY_PROBE_TIMEOUT_MS, MEMORY_PULL_MAX_PAGES, MEMORY_PULL_PAGE_SIZE, MEMORY_SCOPES, MEMORY_SLUG_RE, MEMORY_SOURCE_MAX_CHARS, type MemoryDecision, type MemoryDecisionStatus, type MemoryHit, type MemoryNote, type MemoryNoteStatus, type MemoryNoteType, type MemoryNoteWrite, type MemoryScope, type MemoryTimers, type MemoryWriteRefusal, } from "./types.ts";
|
|
11
|
+
export { MEMORY_CLIENT_ID_RE, MEMORY_DECISION_STATUSES, MEMORY_DESCRIPTION_MAX_CHARS, MEMORY_FORGET_REASON_MAX_CHARS, MEMORY_INDEX_MAX_BYTES, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MIN_BYTES, MEMORY_INDEX_MIN_LINES, MEMORY_NOTE_BODY_MAX_BYTES, MEMORY_NOTE_STATUSES, MEMORY_NOTE_TYPES, MEMORY_OP_TIMEOUT_MS, MEMORY_PROBE_TIMEOUT_MS, MEMORY_PULL_MAX_PAGES, MEMORY_PULL_PAGE_SIZE, MEMORY_SCOPES, MEMORY_SLUG_RE, MEMORY_SOURCE_MAX_CHARS, type MemoryDecision, type MemoryDecisionStatus, type MemoryHit, type MemoryNote, type MemoryNoteStatus, type MemoryNoteType, type MemoryNoteWrite, type MemoryPendingForget, type MemoryScope, type MemoryTimers, type MemoryWriteRefusal, } from "./types.ts";
|
|
12
12
|
export { findSecret, forgetReasonRefusal, validateMemoryNoteWrite } from "./validate.ts";
|
|
@@ -6,16 +6,26 @@
|
|
|
6
6
|
* `proposal`, which is not in force until an approval); a `forget` deletes the
|
|
7
7
|
* note under `direct` and leaves it in place under `proposal`. Both resolve the
|
|
8
8
|
* id the caller can key the mirror on — the note's id for a save, the write's
|
|
9
|
-
* client id for a forget, which has no note left to name.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* client id for a forget, which has no note left to name.
|
|
10
|
+
*
|
|
11
|
+
* A proposal forget is the one write the mirror cannot carry out, so it is queued in
|
|
12
|
+
* the store's `pending-forgets.jsonl` instead: the record is what an index refresh and
|
|
13
|
+
* an approval read later, in a process that never saw the request. A `direct` forget or
|
|
14
|
+
* a save of that key takes the entry back out — the mirror then holds what was asked
|
|
15
|
+
* for. Nothing else may write the record on this provider's behalf.
|
|
16
|
+
*
|
|
17
|
+
* A write the store can never accept — an unslugged note, a scope this store does not
|
|
18
|
+
* have, a target outside it — is refused as `MEMORY_PUSH_REJECTED`, never retried.
|
|
19
|
+
* A failure that a retry can fix (a permission error, a full disk) is left as it is,
|
|
20
|
+
* including one from the record write: the operation keeps the write queued and a
|
|
21
|
+
* later sync replays it.
|
|
13
22
|
*
|
|
14
23
|
* `pull` is the empty finished page: the mirror is the store the caller already
|
|
15
24
|
* reads, so there is nothing to pull back into it. There is no `search`; keyword
|
|
16
25
|
* search runs over the mirror through `searchMemoryNotes`.
|
|
17
26
|
*/
|
|
18
27
|
import { NaxError } from "#src/infra/nax-error";
|
|
28
|
+
import { memoryNoteKey } from "./types.js";
|
|
19
29
|
const ERROR_STAGE = "memory";
|
|
20
30
|
/** The status a write lands with: a proposal is not in force until an approval. */
|
|
21
31
|
const WRITE_MODE_STATUS = {
|
|
@@ -53,6 +63,38 @@ async function throughStore(action) {
|
|
|
53
63
|
throw pushRejectedFrom(error);
|
|
54
64
|
}
|
|
55
65
|
}
|
|
66
|
+
/** The entry a proposal forget leaves in the record: the request's own id and reason. */
|
|
67
|
+
function queuedForget(request) {
|
|
68
|
+
return { clientId: request.clientId, scope: request.scope, slug: request.slug, reason: request.reason ?? "" };
|
|
69
|
+
}
|
|
70
|
+
/** Whether rewriting the record would change it: an unchanged list is not written back. */
|
|
71
|
+
function sameEntries(before, after) {
|
|
72
|
+
if (before.length !== after.length)
|
|
73
|
+
return false;
|
|
74
|
+
return before.every((entry, index) => {
|
|
75
|
+
const other = after[index];
|
|
76
|
+
return (other !== undefined &&
|
|
77
|
+
entry.clientId === other.clientId &&
|
|
78
|
+
entry.scope === other.scope &&
|
|
79
|
+
entry.slug === other.slug &&
|
|
80
|
+
entry.reason === other.reason);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The record rewritten without `key`'s entry, and with `queued` appended after the
|
|
85
|
+
* others when a proposal forget is what took the entry out. The store validates the
|
|
86
|
+
* slugs it is handed, so a forget of a slug no note can carry is refused here — before
|
|
87
|
+
* anything is recorded — even though a proposal never reaches the notes themselves.
|
|
88
|
+
*/
|
|
89
|
+
async function rewriteRecord(store, key, queued) {
|
|
90
|
+
const entries = await store.readPendingForgets();
|
|
91
|
+
const kept = entries.filter((entry) => memoryNoteKey(entry.scope, entry.slug) !== key);
|
|
92
|
+
const next = queued === null ? kept : [...kept, queued];
|
|
93
|
+
// A key the record does not hold leaves the file alone: absent stays absent.
|
|
94
|
+
if (sameEntries(entries, next))
|
|
95
|
+
return;
|
|
96
|
+
await store.writePendingForgets(next);
|
|
97
|
+
}
|
|
56
98
|
export function createLocalMemoryProvider(store) {
|
|
57
99
|
/** A save is accepted by definition: the mirror is what the provider is. */
|
|
58
100
|
async function save(request) {
|
|
@@ -61,6 +103,9 @@ export function createLocalMemoryProvider(store) {
|
|
|
61
103
|
throw pushRejected(`memory save for "${request.slug}" carries no note`);
|
|
62
104
|
const status = WRITE_MODE_STATUS[request.writeMode];
|
|
63
105
|
await throughStore(() => store.writeNote({ ...note, status }));
|
|
106
|
+
// Saving the slug again means it is wanted, so whatever forget it was queued for goes.
|
|
107
|
+
const key = memoryNoteKey(note.scope, note.slug);
|
|
108
|
+
await throughStore(() => rewriteRecord(store, key, null));
|
|
64
109
|
return { id: note.id, status };
|
|
65
110
|
}
|
|
66
111
|
/** A forget under direct removes the note; a proposal leaves it for an approval. */
|
|
@@ -68,6 +113,8 @@ export function createLocalMemoryProvider(store) {
|
|
|
68
113
|
const direct = request.writeMode === "direct";
|
|
69
114
|
if (direct)
|
|
70
115
|
await throughStore(() => store.deleteNote(request.scope, request.slug));
|
|
116
|
+
const key = memoryNoteKey(request.scope, request.slug);
|
|
117
|
+
await throughStore(() => rewriteRecord(store, key, direct ? null : queuedForget(request)));
|
|
71
118
|
return { id: request.clientId, status: direct ? "deleted" : "pending" };
|
|
72
119
|
}
|
|
73
120
|
return {
|
|
@@ -51,10 +51,8 @@ export interface MemoryForgetResult {
|
|
|
51
51
|
/**
|
|
52
52
|
* One approval: the note to put in force, named by its scope and slug.
|
|
53
53
|
*
|
|
54
|
-
* The mirror is the whole of what an approval decides
|
|
55
|
-
*
|
|
56
|
-
* note it rewrites has to be the note the mirror holds at that moment — and every one of them
|
|
57
|
-
* has a default, so `{ store, scope, slug }` is a complete input.
|
|
54
|
+
* The mirror is the whole of what an approval decides: no queue to declare a write on, no provider
|
|
55
|
+
* to ask, and every lock seam defaulted, so `{ store, scope, slug }` is a complete input.
|
|
58
56
|
*/
|
|
59
57
|
export interface MemoryApproveInput {
|
|
60
58
|
readonly store: MemoryStore;
|
|
@@ -97,24 +95,29 @@ export declare function saveMemoryNote(input: MemorySaveInput): Promise<MemorySa
|
|
|
97
95
|
*/
|
|
98
96
|
export declare function forgetMemoryNote(input: MemoryForgetInput): Promise<MemoryForgetResult>;
|
|
99
97
|
/**
|
|
100
|
-
* Put one pending note in force in this mirror.
|
|
98
|
+
* Put one pending note in force in this mirror — or retire the note a recorded forget names.
|
|
101
99
|
*
|
|
102
100
|
* Only the note the mirror holds is consulted, so approval is a local transition rather than an
|
|
103
101
|
* authorization step: it asks no provider and writes nothing to the queue, and a forget waiting
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* asked for.
|
|
102
|
+
* there is still waiting afterwards. What it does read besides the note is the mirror's
|
|
103
|
+
* pending-forget record — `pending-forgets.jsonl`, written by the local provider for a proposal
|
|
104
|
+
* forget — and the queue's held keys, because the index it regenerates carries those markers.
|
|
108
105
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
106
|
+
* An entry in the record is the one thing an approval can carry out that a rewrite cannot: the note
|
|
107
|
+
* is deleted, the entry is taken out of the record, the index is regenerated, and the note comes
|
|
108
|
+
* back as it was read with `status: "deleted"` — its `updatedAt` still says when it was written.
|
|
109
|
+
* That arm runs before the activation of a `pending` note, because a pending forget wins over a
|
|
110
|
+
* pending save: the agent asked for the note to go after asking for it to exist. A delete that
|
|
111
|
+
* throws leaves the entry where it is, so the next approval finds the forget again.
|
|
114
112
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
113
|
+
* The read and the rewrite are one decision, so they run under the same ordering and the same lock
|
|
114
|
+
* file a save and a forget use: this is a writer of the mirror, and what it approves must be the
|
|
115
|
+
* note the mirror holds now rather than a copy read before another writer's newer one landed.
|
|
116
|
+
* Waiting is the right answer here, unlike a save's — an approval has no queue to put its write on.
|
|
117
|
+
*
|
|
118
|
+
* A note already in force, with nothing recorded against it, is returned exactly as it stands —
|
|
119
|
+
* approving it again is not a rewrite, so its `updatedAt` still says when it was made active. A
|
|
120
|
+
* note that is history is not a note waiting for approval, and neither is one this mirror does
|
|
121
|
+
* not hold: both are `MEMORY_NOTE_NOT_FOUND`, because there is nothing to approve.
|
|
119
122
|
*/
|
|
120
123
|
export declare function approveMemoryNote(input: MemoryApproveInput): Promise<MemoryNote>;
|
|
@@ -47,10 +47,13 @@
|
|
|
47
47
|
* `approveMemoryNote` is the transition a proposal waits for: a pending note is rewritten in force
|
|
48
48
|
* under the caller's clock and the index is regenerated from the mirror, a note already in force is
|
|
49
49
|
* returned as it stands, and a note that is history — or one this mirror does not hold — is the
|
|
50
|
-
* same `MEMORY_NOTE_NOT_FOUND` refusal a forget of it would be.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
50
|
+
* same `MEMORY_NOTE_NOT_FOUND` refusal a forget of it would be. A note the mirror's
|
|
51
|
+
* `pending-forgets.jsonl` record holds a forget for is retired instead of approved: the note is
|
|
52
|
+
* deleted, its entry comes out of the record and the index is regenerated, so the answer is that
|
|
53
|
+
* note, unchanged but for `status: "deleted"` — a recorded forget wins over a pending save. It asks
|
|
54
|
+
* no provider and writes nothing to the queue, but it writes the mirror and the record, so it takes
|
|
55
|
+
* the same ordering and the same locks a save takes. It reads the queue only to keep the index's
|
|
56
|
+
* marker, and approves no queued forget: an entry the outbox still holds stays the outbox's.
|
|
54
57
|
*/
|
|
55
58
|
import { randomUUID } from "node:crypto";
|
|
56
59
|
import { getLogger } from "#src/infra/agent-logger";
|
|
@@ -382,10 +385,7 @@ function inForce(note, now) {
|
|
|
382
385
|
function mirrorOutbox(store) {
|
|
383
386
|
return createMemoryOutbox({ dir: store.paths.outboxDir });
|
|
384
387
|
}
|
|
385
|
-
/**
|
|
386
|
-
* The mirror's lock as this approval takes it: the caller's seam where it gave one, and the
|
|
387
|
-
* process defaults — including the logger slot — where it did not.
|
|
388
|
-
*/
|
|
388
|
+
/** The mirror's lock as this approval takes it: the caller's seams, else the process defaults. */
|
|
389
389
|
function lockInputOf(input) {
|
|
390
390
|
return {
|
|
391
391
|
store: input.store,
|
|
@@ -397,16 +397,20 @@ function lockInputOf(input) {
|
|
|
397
397
|
}
|
|
398
398
|
/**
|
|
399
399
|
* The locked half of an approval: read, decide and rewrite while no other writer of this
|
|
400
|
-
* mirror can change the note in between
|
|
400
|
+
* mirror can change the note in between — the read and the write are one decision, which is
|
|
401
|
+
* the whole reason this takes the lock at all.
|
|
401
402
|
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
403
|
+
* A recorded pending forget is checked before the note's own status, because a pending forget
|
|
404
|
+
* wins over a pending save: the note the mirror holds was written first, and the forget of it
|
|
405
|
+
* came after. A note that is history, or one the mirror does not hold, is the refusal it was
|
|
406
|
+
* before the record is consulted at all.
|
|
405
407
|
*/
|
|
406
408
|
async function approveLocked(input) {
|
|
407
409
|
const note = await input.store.readNote(input.scope, input.slug);
|
|
408
410
|
if (note === null || RETIRED_STATUSES.has(note.status))
|
|
409
411
|
throw noteNotFound(input);
|
|
412
|
+
if (await holdsRecordedForget(input))
|
|
413
|
+
return await retireRecorded(input, note);
|
|
410
414
|
if (note.status === "active")
|
|
411
415
|
return note;
|
|
412
416
|
const approved = inForce(note, clockOf(input));
|
|
@@ -414,26 +418,53 @@ async function approveLocked(input) {
|
|
|
414
418
|
await refreshIndex({ store: input.store, outbox: mirrorOutbox(input.store) });
|
|
415
419
|
return approved;
|
|
416
420
|
}
|
|
421
|
+
/** Whether the mirror's record holds a pending forget for the key this approval names. */
|
|
422
|
+
async function holdsRecordedForget(input) {
|
|
423
|
+
const key = memoryNoteKey(input.scope, input.slug);
|
|
424
|
+
const recorded = await input.store.readPendingForgets();
|
|
425
|
+
return recorded.some((entry) => memoryNoteKey(entry.scope, entry.slug) === key);
|
|
426
|
+
}
|
|
417
427
|
/**
|
|
418
|
-
*
|
|
428
|
+
* The note a recorded forget retires: the note goes, then its entry comes out of the record.
|
|
429
|
+
*
|
|
430
|
+
* That order is the failure contract: a delete that throws leaves the record holding the entry, so
|
|
431
|
+
* the forget is still outstanding and still findable. The record is re-read after the delete — an
|
|
432
|
+
* entry another writer appended meanwhile survives — and the note comes back as it was read, with
|
|
433
|
+
* `status: "deleted"` and its own `updatedAt`: an approval retires a note, it does not rewrite one.
|
|
434
|
+
*/
|
|
435
|
+
async function retireRecorded(input, note) {
|
|
436
|
+
const key = memoryNoteKey(input.scope, input.slug);
|
|
437
|
+
await input.store.deleteNote(input.scope, input.slug);
|
|
438
|
+
const recorded = await input.store.readPendingForgets();
|
|
439
|
+
await input.store.writePendingForgets(recorded.filter((entry) => memoryNoteKey(entry.scope, entry.slug) !== key));
|
|
440
|
+
await refreshIndex({ store: input.store, outbox: mirrorOutbox(input.store) });
|
|
441
|
+
return { ...note, status: "deleted" };
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Put one pending note in force in this mirror — or retire the note a recorded forget names.
|
|
419
445
|
*
|
|
420
446
|
* Only the note the mirror holds is consulted, so approval is a local transition rather than an
|
|
421
447
|
* authorization step: it asks no provider and writes nothing to the queue, and a forget waiting
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
448
|
+
* there is still waiting afterwards. What it does read besides the note is the mirror's
|
|
449
|
+
* pending-forget record — `pending-forgets.jsonl`, written by the local provider for a proposal
|
|
450
|
+
* forget — and the queue's held keys, because the index it regenerates carries those markers.
|
|
451
|
+
*
|
|
452
|
+
* An entry in the record is the one thing an approval can carry out that a rewrite cannot: the note
|
|
453
|
+
* is deleted, the entry is taken out of the record, the index is regenerated, and the note comes
|
|
454
|
+
* back as it was read with `status: "deleted"` — its `updatedAt` still says when it was written.
|
|
455
|
+
* That arm runs before the activation of a `pending` note, because a pending forget wins over a
|
|
456
|
+
* pending save: the agent asked for the note to go after asking for it to exist. A delete that
|
|
457
|
+
* throws leaves the entry where it is, so the next approval finds the forget again.
|
|
426
458
|
*
|
|
427
|
-
* The read and the rewrite are one decision, so they run under the same ordering and the same
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
* its write on, so it has to be the writer when it reads.
|
|
459
|
+
* The read and the rewrite are one decision, so they run under the same ordering and the same lock
|
|
460
|
+
* file a save and a forget use: this is a writer of the mirror, and what it approves must be the
|
|
461
|
+
* note the mirror holds now rather than a copy read before another writer's newer one landed.
|
|
462
|
+
* Waiting is the right answer here, unlike a save's — an approval has no queue to put its write on.
|
|
432
463
|
*
|
|
433
|
-
* A note already in force is returned exactly as it stands —
|
|
434
|
-
* rewrite, so its `updatedAt` still says when it was made active. A
|
|
435
|
-
* a note waiting for approval, and neither is one this mirror does
|
|
436
|
-
* `MEMORY_NOTE_NOT_FOUND`, because there is nothing to approve.
|
|
464
|
+
* A note already in force, with nothing recorded against it, is returned exactly as it stands —
|
|
465
|
+
* approving it again is not a rewrite, so its `updatedAt` still says when it was made active. A
|
|
466
|
+
* note that is history is not a note waiting for approval, and neither is one this mirror does
|
|
467
|
+
* not hold: both are `MEMORY_NOTE_NOT_FOUND`, because there is nothing to approve.
|
|
437
468
|
*/
|
|
438
469
|
export async function approveMemoryNote(input) {
|
|
439
470
|
return await orderedByMirrorLocks(input.store, () => withMirrorLock(lockInputOf(input), () => approveLocked(input)));
|
package/dist/memory/records.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The record files the mirror keeps beside its notes, decoded.
|
|
3
3
|
*
|
|
4
|
-
* `decisions.jsonl` is one `MemoryDecision` per line
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `decisions.jsonl` is one `MemoryDecision` per line, `pending-forgets.jsonl` one
|
|
5
|
+
* `MemoryPendingForget` per line, and `sync.json` is one `MemorySyncState`: all three are
|
|
6
|
+
* written whole by this store and read back by it, so what makes a line a decision — or
|
|
7
|
+
* a pending forget, or what makes a file a state — is a schema rather than a cast.
|
|
8
|
+
* Keeping the schemas here leaves the store to decide what a refused line *means*
|
|
8
9
|
* (skipped with a warning) and this module to decide what counts as one.
|
|
9
10
|
*/
|
|
10
11
|
import { z } from "zod";
|
|
11
|
-
import { type MemoryDecision } from "./types.ts";
|
|
12
|
+
import { type MemoryDecision, type MemoryPendingForget } from "./types.ts";
|
|
12
13
|
declare const SyncStateSchema: z.ZodObject<{
|
|
13
14
|
provider: z.ZodNullable<z.ZodString>;
|
|
14
15
|
cursor: z.ZodObject<{
|
|
@@ -24,6 +25,17 @@ export type ParsedSyncState = z.infer<typeof SyncStateSchema>;
|
|
|
24
25
|
export declare function defaultSyncState(): ParsedSyncState;
|
|
25
26
|
/** One mirror line as a decision, or `null` when the line is not one. */
|
|
26
27
|
export declare function parseDecisionLine(line: string): MemoryDecision | null;
|
|
28
|
+
/** One mirror line as a pending forget, or `null` when the line is not one. */
|
|
29
|
+
export declare function parsePendingForgetLine(line: string): MemoryPendingForget | null;
|
|
30
|
+
/**
|
|
31
|
+
* A JSONL record's text as its entries. A line the decoder refuses is skipped with one
|
|
32
|
+
* report naming its 1-based line number, never fatal: the other lines are still the
|
|
33
|
+
* record. A record file that is not there (`null`) is the empty record, not the absent
|
|
34
|
+
* one — a store that has never recorded anything answers `[]` and creates nothing.
|
|
35
|
+
*/
|
|
36
|
+
export declare function decodeRecordLines<T>(text: string | null, decode: (line: string) => T | null, warnSkipped: (line: number) => void): T[];
|
|
37
|
+
/** One record file's text: one JSON object per line, and nothing at all when empty. */
|
|
38
|
+
export declare function encodeRecordLines(entries: readonly unknown[]): string;
|
|
27
39
|
/** The whole `sync.json` text as a state, or `null` when it does not hold one. */
|
|
28
40
|
export declare function parseSyncStateText(text: string): ParsedSyncState | null;
|
|
29
41
|
export {};
|
package/dist/memory/records.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The record files the mirror keeps beside its notes, decoded.
|
|
3
3
|
*
|
|
4
|
-
* `decisions.jsonl` is one `MemoryDecision` per line
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `decisions.jsonl` is one `MemoryDecision` per line, `pending-forgets.jsonl` one
|
|
5
|
+
* `MemoryPendingForget` per line, and `sync.json` is one `MemorySyncState`: all three are
|
|
6
|
+
* written whole by this store and read back by it, so what makes a line a decision — or
|
|
7
|
+
* a pending forget, or what makes a file a state — is a schema rather than a cast.
|
|
8
|
+
* Keeping the schemas here leaves the store to decide what a refused line *means*
|
|
8
9
|
* (skipped with a warning) and this module to decide what counts as one.
|
|
9
10
|
*/
|
|
10
11
|
import { z } from "zod";
|
|
11
|
-
import { MEMORY_DECISION_STATUSES } from "./types.js";
|
|
12
|
+
import { MEMORY_CLIENT_ID_RE, MEMORY_DECISION_STATUSES, MEMORY_SCOPES, MEMORY_SLUG_RE, } from "./types.js";
|
|
12
13
|
/** A decision as one mirror line; the shape is what makes a line a decision. */
|
|
13
14
|
const DecisionSchema = z.object({
|
|
14
15
|
id: z.string().min(1),
|
|
@@ -18,6 +19,17 @@ const DecisionSchema = z.object({
|
|
|
18
19
|
updatedAt: z.string().min(1),
|
|
19
20
|
status: z.enum(MEMORY_DECISION_STATUSES),
|
|
20
21
|
});
|
|
22
|
+
/**
|
|
23
|
+
* A pending forget as one mirror line. The slug and the client id are the two values
|
|
24
|
+
* later code acts on — the slug names the note an approval retires, the client id names
|
|
25
|
+
* the write — so a line carries a note slug and an outbox id, never an arbitrary string.
|
|
26
|
+
*/
|
|
27
|
+
const PendingForgetSchema = z.object({
|
|
28
|
+
clientId: z.string().regex(MEMORY_CLIENT_ID_RE),
|
|
29
|
+
scope: z.enum(MEMORY_SCOPES),
|
|
30
|
+
slug: z.string().regex(MEMORY_SLUG_RE),
|
|
31
|
+
reason: z.string(),
|
|
32
|
+
});
|
|
21
33
|
const SyncStateSchema = z.object({
|
|
22
34
|
provider: z.string().nullable(),
|
|
23
35
|
cursor: z.object({ project: z.string().nullable(), user: z.string().nullable() }),
|
|
@@ -43,6 +55,37 @@ export function parseDecisionLine(line) {
|
|
|
43
55
|
const parsed = DecisionSchema.safeParse(parseJson(line));
|
|
44
56
|
return parsed.success ? parsed.data : null;
|
|
45
57
|
}
|
|
58
|
+
/** One mirror line as a pending forget, or `null` when the line is not one. */
|
|
59
|
+
export function parsePendingForgetLine(line) {
|
|
60
|
+
const parsed = PendingForgetSchema.safeParse(parseJson(line));
|
|
61
|
+
return parsed.success ? parsed.data : null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A JSONL record's text as its entries. A line the decoder refuses is skipped with one
|
|
65
|
+
* report naming its 1-based line number, never fatal: the other lines are still the
|
|
66
|
+
* record. A record file that is not there (`null`) is the empty record, not the absent
|
|
67
|
+
* one — a store that has never recorded anything answers `[]` and creates nothing.
|
|
68
|
+
*/
|
|
69
|
+
export function decodeRecordLines(text, decode, warnSkipped) {
|
|
70
|
+
if (text === null)
|
|
71
|
+
return [];
|
|
72
|
+
const entries = [];
|
|
73
|
+
for (const [index, raw] of text.split("\n").entries()) {
|
|
74
|
+
if (raw.trim() === "")
|
|
75
|
+
continue;
|
|
76
|
+
const entry = decode(raw);
|
|
77
|
+
if (entry === null) {
|
|
78
|
+
warnSkipped(index + 1);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
entries.push(entry);
|
|
82
|
+
}
|
|
83
|
+
return entries;
|
|
84
|
+
}
|
|
85
|
+
/** One record file's text: one JSON object per line, and nothing at all when empty. */
|
|
86
|
+
export function encodeRecordLines(entries) {
|
|
87
|
+
return entries.map((entry) => `${JSON.stringify(entry)}\n`).join("");
|
|
88
|
+
}
|
|
46
89
|
/** The whole `sync.json` text as a state, or `null` when it does not hold one. */
|
|
47
90
|
export function parseSyncStateText(text) {
|
|
48
91
|
const parsed = SyncStateSchema.safeParse(parseJson(text));
|
package/dist/memory/store.d.ts
CHANGED
|
@@ -15,18 +15,19 @@
|
|
|
15
15
|
* The caller's global cwd is never changed. Post-checks report moved directories;
|
|
16
16
|
* they are not the mechanism that prevents access to replacements.
|
|
17
17
|
*
|
|
18
|
-
* The store also owns the
|
|
19
|
-
*
|
|
20
|
-
* state) and each scope's generated `MEMORY.md`.
|
|
21
|
-
* paths, so they are read and written without a slug to
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
18
|
+
* The store also owns the record files beside the notes: the `decisions.jsonl`
|
|
19
|
+
* mirror (one JSON object per line), the `pending-forgets.jsonl` record of queued
|
|
20
|
+
* forgets, `sync.json` (the sync state) and each scope's generated `MEMORY.md`.
|
|
21
|
+
* They hold no user-authored paths, so they are read and written without a slug to
|
|
22
|
+
* validate — save a pending forget's own slug, which names a note and must be one.
|
|
23
|
+
* The only scope rule is that the user files need a user directory. A record read is
|
|
24
|
+
* still confined: it is read under the project directory's pin and never follows a
|
|
25
|
+
* link out of the store.
|
|
25
26
|
*/
|
|
26
27
|
import type { Dirent } from "node:fs";
|
|
27
28
|
import type { AgentLogger } from "#src/infra/agent-logger";
|
|
28
29
|
import { withPinnedDirectory } from "#src/internal/pinned-directory";
|
|
29
|
-
import { type MemoryDecision, type MemoryNote, type MemoryScope } from "./types.ts";
|
|
30
|
+
import { type MemoryDecision, type MemoryNote, type MemoryPendingForget, type MemoryScope } from "./types.ts";
|
|
30
31
|
interface Identity {
|
|
31
32
|
readonly dev: number;
|
|
32
33
|
readonly ino: number;
|
|
@@ -65,6 +66,7 @@ export interface MemoryStorePaths {
|
|
|
65
66
|
readonly index: string;
|
|
66
67
|
readonly notesDir: string;
|
|
67
68
|
readonly decisions: string;
|
|
69
|
+
readonly pendingForgets: string;
|
|
68
70
|
readonly syncState: string;
|
|
69
71
|
readonly lock: string;
|
|
70
72
|
readonly outboxDir: string;
|
|
@@ -90,6 +92,8 @@ export interface MemoryStore {
|
|
|
90
92
|
listNotes(scope: MemoryScope): Promise<MemoryNote[]>;
|
|
91
93
|
readDecisions(): Promise<MemoryDecision[]>;
|
|
92
94
|
writeDecisions(decisions: readonly MemoryDecision[]): Promise<void>;
|
|
95
|
+
readPendingForgets(): Promise<MemoryPendingForget[]>;
|
|
96
|
+
writePendingForgets(entries: readonly MemoryPendingForget[]): Promise<void>;
|
|
93
97
|
readSyncState(): Promise<MemorySyncState>;
|
|
94
98
|
writeSyncState(state: MemorySyncState): Promise<void>;
|
|
95
99
|
writeIndex(scope: MemoryScope, text: string): Promise<void>;
|