@nathapp/nax-agent 0.91.7 → 0.91.8-canary.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +1 -0
  4. package/dist/internal/atomic-write.d.ts +22 -7
  5. package/dist/internal/atomic-write.js +107 -12
  6. package/dist/internal/pinned-directory-worker.d.ts +52 -0
  7. package/dist/internal/pinned-directory-worker.js +105 -0
  8. package/dist/internal/pinned-directory.d.ts +48 -0
  9. package/dist/internal/pinned-directory.js +168 -0
  10. package/dist/internal.d.ts +1 -0
  11. package/dist/internal.js +1 -0
  12. package/dist/memory/delivery.d.ts +38 -0
  13. package/dist/memory/delivery.js +28 -0
  14. package/dist/memory/index-render.d.ts +51 -0
  15. package/dist/memory/index-render.js +210 -0
  16. package/dist/memory/index.d.ts +12 -0
  17. package/dist/memory/index.js +11 -0
  18. package/dist/memory/local-provider.d.ts +3 -0
  19. package/dist/memory/local-provider.js +80 -0
  20. package/dist/memory/mirror-lock.d.ts +109 -0
  21. package/dist/memory/mirror-lock.js +334 -0
  22. package/dist/memory/note.d.ts +15 -0
  23. package/dist/memory/note.js +131 -0
  24. package/dist/memory/operations.d.ts +120 -0
  25. package/dist/memory/operations.js +440 -0
  26. package/dist/memory/outbox.d.ts +35 -0
  27. package/dist/memory/outbox.js +289 -0
  28. package/dist/memory/provider-types.d.ts +50 -0
  29. package/dist/memory/provider-types.js +1 -0
  30. package/dist/memory/records.d.ts +29 -0
  31. package/dist/memory/records.js +50 -0
  32. package/dist/memory/refusal.d.ts +36 -0
  33. package/dist/memory/refusal.js +25 -0
  34. package/dist/memory/search.d.ts +17 -0
  35. package/dist/memory/search.js +112 -0
  36. package/dist/memory/store.d.ts +102 -0
  37. package/dist/memory/store.js +455 -0
  38. package/dist/memory/support.d.ts +63 -0
  39. package/dist/memory/support.js +98 -0
  40. package/dist/memory/sync-flight.d.ts +25 -0
  41. package/dist/memory/sync-flight.js +65 -0
  42. package/dist/memory/sync.d.ts +82 -0
  43. package/dist/memory/sync.js +344 -0
  44. package/dist/memory/types.d.ts +96 -0
  45. package/dist/memory/types.js +51 -0
  46. package/dist/memory/validate.d.ts +31 -0
  47. package/dist/memory/validate.js +104 -0
  48. package/dist/session-store/index.d.ts +1 -1
  49. package/dist/session-store/index.js +1 -1
  50. package/package.json +3 -3
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The memory note model — the shapes and limits shared by the note codec, the
3
+ * mirror store, the index renderer, the providers and the sync loop.
4
+ *
5
+ * A note is one Markdown file with frontmatter (`note.ts` writes and reads it);
6
+ * a decision is one line of the mirror's read-only `decisions.jsonl`; a hit is
7
+ * one keyword-search result; a timers object is the race seam the sync loop
8
+ * injects in tests.
9
+ */
10
+ export declare const MEMORY_NOTE_TYPES: readonly ["user", "feedback", "project", "reference"];
11
+ export type MemoryNoteType = (typeof MEMORY_NOTE_TYPES)[number];
12
+ export declare const MEMORY_SCOPES: readonly ["project", "user"];
13
+ export type MemoryScope = (typeof MEMORY_SCOPES)[number];
14
+ export declare const MEMORY_NOTE_STATUSES: readonly ["active", "pending", "superseded", "rejected", "deleted"];
15
+ export type MemoryNoteStatus = (typeof MEMORY_NOTE_STATUSES)[number];
16
+ export declare const MEMORY_DECISION_STATUSES: readonly ["active", "superseded", "rejected"];
17
+ export type MemoryDecisionStatus = (typeof MEMORY_DECISION_STATUSES)[number];
18
+ export interface MemoryNote {
19
+ readonly id: string;
20
+ readonly slug: string;
21
+ readonly type: MemoryNoteType;
22
+ readonly scope: MemoryScope;
23
+ readonly description: string;
24
+ readonly source: string;
25
+ readonly updatedAt: string;
26
+ readonly status: MemoryNoteStatus;
27
+ readonly body: string;
28
+ }
29
+ export interface MemoryDecision {
30
+ readonly id: string;
31
+ readonly subject: string;
32
+ readonly predicate: string;
33
+ readonly object: string;
34
+ readonly updatedAt: string;
35
+ readonly status: MemoryDecisionStatus;
36
+ }
37
+ export interface MemoryHit {
38
+ readonly slug: string;
39
+ readonly scope: MemoryScope;
40
+ readonly type: MemoryNoteType;
41
+ readonly description: string;
42
+ readonly status: MemoryNoteStatus;
43
+ readonly score: number;
44
+ readonly snippet: string;
45
+ }
46
+ export interface MemoryNoteWrite {
47
+ readonly slug: string;
48
+ readonly type: MemoryNoteType;
49
+ readonly scope: MemoryScope;
50
+ readonly description: string;
51
+ readonly body: string;
52
+ }
53
+ export interface MemoryWriteRefusal {
54
+ readonly field: "slug" | "type" | "description" | "body" | "source" | "reason";
55
+ readonly reason: string;
56
+ }
57
+ export interface MemoryTimers {
58
+ setTimeout(callback: () => void, ms: number): unknown;
59
+ clearTimeout(id: unknown): void;
60
+ }
61
+ /** A note slug: 1-64 characters, lower-case alphanumerics and interior `-`. */
62
+ export declare const MEMORY_SLUG_RE: RegExp;
63
+ /** An outbox client id: 1-128 letters, digits, `_` or `-`. */
64
+ export declare const MEMORY_CLIENT_ID_RE: RegExp;
65
+ export declare const MEMORY_NOTE_BODY_MAX_BYTES = 8192;
66
+ export declare const MEMORY_DESCRIPTION_MAX_CHARS = 200;
67
+ export declare const MEMORY_SOURCE_MAX_CHARS = 64;
68
+ /**
69
+ * The longest justification a forget may carry.
70
+ *
71
+ * A reason is a one-line answer to "why did this note go", and it is persisted twice over: on
72
+ * the request the provider is pushed, and inside the outbox entry a forget that cannot be
73
+ * delivered leaves behind. Bounding it here — the same cap as a note's description, because it
74
+ * is the same kind of text — keeps a terminal or a step from filling the mirror's queue with a
75
+ * justification of its own making. `forgetReasonRefusal` is where the rule is applied to a
76
+ * forget, and the outbox applies that same rule when it takes a request, so the queue never
77
+ * holds one it cannot replay. An entry already on disk is read whatever its reason's length:
78
+ * the bound is a condition on writing, not on reading.
79
+ */
80
+ export declare const MEMORY_FORGET_REASON_MAX_CHARS = 200;
81
+ export declare const MEMORY_INDEX_MAX_BYTES = 24576;
82
+ export declare const MEMORY_INDEX_MAX_LINES = 200;
83
+ export declare const MEMORY_INDEX_MIN_LINES = 16;
84
+ export declare const MEMORY_INDEX_MIN_BYTES = 512;
85
+ export declare const MEMORY_PROBE_TIMEOUT_MS = 2000;
86
+ export declare const MEMORY_OP_TIMEOUT_MS = 10000;
87
+ export declare const MEMORY_PULL_PAGE_SIZE = 200;
88
+ export declare const MEMORY_PULL_MAX_PAGES = 50;
89
+ /**
90
+ * The key a note is addressed by wherever the mirror and the queue meet: `<scope>/<slug>`.
91
+ *
92
+ * The outbox holds writes by this key and a save asks the outbox whether the key it is
93
+ * about to write is already held, so the two spellings have to be one: a save that asks
94
+ * with a different key would never see the entry it must queue behind.
95
+ */
96
+ export declare function memoryNoteKey(scope: MemoryScope, slug: string): string;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The memory note model — the shapes and limits shared by the note codec, the
3
+ * mirror store, the index renderer, the providers and the sync loop.
4
+ *
5
+ * A note is one Markdown file with frontmatter (`note.ts` writes and reads it);
6
+ * a decision is one line of the mirror's read-only `decisions.jsonl`; a hit is
7
+ * one keyword-search result; a timers object is the race seam the sync loop
8
+ * injects in tests.
9
+ */
10
+ export const MEMORY_NOTE_TYPES = ["user", "feedback", "project", "reference"];
11
+ export const MEMORY_SCOPES = ["project", "user"];
12
+ export const MEMORY_NOTE_STATUSES = ["active", "pending", "superseded", "rejected", "deleted"];
13
+ export const MEMORY_DECISION_STATUSES = ["active", "superseded", "rejected"];
14
+ /** A note slug: 1-64 characters, lower-case alphanumerics and interior `-`. */
15
+ export const MEMORY_SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
16
+ /** An outbox client id: 1-128 letters, digits, `_` or `-`. */
17
+ export const MEMORY_CLIENT_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
18
+ export const MEMORY_NOTE_BODY_MAX_BYTES = 8192;
19
+ export const MEMORY_DESCRIPTION_MAX_CHARS = 200;
20
+ export const MEMORY_SOURCE_MAX_CHARS = 64;
21
+ /**
22
+ * The longest justification a forget may carry.
23
+ *
24
+ * A reason is a one-line answer to "why did this note go", and it is persisted twice over: on
25
+ * the request the provider is pushed, and inside the outbox entry a forget that cannot be
26
+ * delivered leaves behind. Bounding it here — the same cap as a note's description, because it
27
+ * is the same kind of text — keeps a terminal or a step from filling the mirror's queue with a
28
+ * justification of its own making. `forgetReasonRefusal` is where the rule is applied to a
29
+ * forget, and the outbox applies that same rule when it takes a request, so the queue never
30
+ * holds one it cannot replay. An entry already on disk is read whatever its reason's length:
31
+ * the bound is a condition on writing, not on reading.
32
+ */
33
+ export const MEMORY_FORGET_REASON_MAX_CHARS = 200;
34
+ export const MEMORY_INDEX_MAX_BYTES = 24_576;
35
+ export const MEMORY_INDEX_MAX_LINES = 200;
36
+ export const MEMORY_INDEX_MIN_LINES = 16;
37
+ export const MEMORY_INDEX_MIN_BYTES = 512;
38
+ export const MEMORY_PROBE_TIMEOUT_MS = 2000;
39
+ export const MEMORY_OP_TIMEOUT_MS = 10_000;
40
+ export const MEMORY_PULL_PAGE_SIZE = 200;
41
+ export const MEMORY_PULL_MAX_PAGES = 50;
42
+ /**
43
+ * The key a note is addressed by wherever the mirror and the queue meet: `<scope>/<slug>`.
44
+ *
45
+ * The outbox holds writes by this key and a save asks the outbox whether the key it is
46
+ * about to write is already held, so the two spellings have to be one: a save that asks
47
+ * with a different key would never see the entry it must queue behind.
48
+ */
49
+ export function memoryNoteKey(scope, slug) {
50
+ return `${scope}/${slug}`;
51
+ }
@@ -0,0 +1,31 @@
1
+ import { type MemoryNoteWrite, type MemoryWriteRefusal } from "./types.ts";
2
+ /**
3
+ * The write as received. `type` stays `string` because the value arrives from
4
+ * an agent tool call: narrowing it in the type system would leave the runtime
5
+ * check unreachable without a cast in the very test that has to exercise it.
6
+ */
7
+ export type MemoryNoteWriteInput = Omit<MemoryNoteWrite, "type"> & {
8
+ readonly type: string;
9
+ };
10
+ /**
11
+ * The first secret-shaped run in `text`, as its pattern's `kind` label. Every
12
+ * pattern carries the `/g` flag (the logger needs it for `replace`), so
13
+ * `lastIndex` is reset before each scan — otherwise a second call would resume
14
+ * where the first stopped and skip a match.
15
+ */
16
+ export declare function findSecret(text: string): {
17
+ readonly kind: string;
18
+ } | null;
19
+ /**
20
+ * The refusal for a forget's reason, or `null` when it may be recorded.
21
+ *
22
+ * A reason is what a later session reads to know why a note went, so it is held to the same
23
+ * one-line, non-empty, length-capped rule as a note's description: it is persisted on the
24
+ * request and inside a queued entry, and neither of those is a place to keep an essay.
25
+ */
26
+ export declare function forgetReasonRefusal(reason: string): MemoryWriteRefusal | null;
27
+ /**
28
+ * The refusal for a note write, or `null` when it may be stored. Stops at the
29
+ * first problem, in the design's order, so the caller reports one reason.
30
+ */
31
+ export declare function validateMemoryNoteWrite(write: MemoryNoteWriteInput, source: string): MemoryWriteRefusal | null;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Write validation — the check a save runs before anything is persisted.
3
+ *
4
+ * `validateMemoryNoteWrite` answers one question for the save path
5
+ * (`operations.ts`): may this note be stored at all? It returns `null` when it
6
+ * may, or the first `MemoryWriteRefusal` it found, checking in the design's
7
+ * order: slug, type, description, source, body size, then secrets.
8
+ *
9
+ * `findSecret` is the secret half on its own: it runs the logger's
10
+ * `SECRET_VALUE_PATTERNS` over free text and reports the kind of the first
11
+ * accepted match. Both functions are pure — no file, no logger, no I/O — so the
12
+ * refusal text is all the caller has to work with, and it never contains the
13
+ * matched text.
14
+ */
15
+ import { SECRET_VALUE_PATTERNS } from "#src/internal/redact";
16
+ import { MEMORY_DESCRIPTION_MAX_CHARS, MEMORY_FORGET_REASON_MAX_CHARS, MEMORY_NOTE_BODY_MAX_BYTES, MEMORY_NOTE_TYPES, MEMORY_SLUG_RE, MEMORY_SOURCE_MAX_CHARS, } from "./types.js";
17
+ /** A one-line field may carry no line break and no ASCII control character (a tab or DEL included). */
18
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: matching control characters is the point of this check
19
+ const CONTROL_OR_LINE_BREAK = /[\u0000-\u001f\u007f]/;
20
+ function isMemoryNoteType(value) {
21
+ return MEMORY_NOTE_TYPES.some((type) => type === value);
22
+ }
23
+ function slugRefusal(slug) {
24
+ if (MEMORY_SLUG_RE.test(slug))
25
+ return null;
26
+ return { field: "slug", reason: 'slug must be 1-64 lower-case letters, digits or "-"' };
27
+ }
28
+ function typeRefusal(type) {
29
+ if (isMemoryNoteType(type))
30
+ return null;
31
+ return { field: "type", reason: `type must be one of ${MEMORY_NOTE_TYPES.join(", ")}` };
32
+ }
33
+ /** The shared check for `description` and `source`: non-empty, one line, at most `maxChars`. */
34
+ function lineRefusal(field, text, maxChars) {
35
+ if (text === "")
36
+ return { field, reason: `${field} must not be empty` };
37
+ if (CONTROL_OR_LINE_BREAK.test(text)) {
38
+ return { field, reason: `${field} must not contain a line break or control character` };
39
+ }
40
+ if (text.length > maxChars)
41
+ return { field, reason: `${field} must be at most ${maxChars} characters` };
42
+ return null;
43
+ }
44
+ /** The body cap counts UTF-8 bytes, so a multi-byte body can fail below the character limit. */
45
+ function bodyRefusal(body) {
46
+ if (Buffer.byteLength(body, "utf8") <= MEMORY_NOTE_BODY_MAX_BYTES)
47
+ return null;
48
+ return { field: "body", reason: `body must be at most ${MEMORY_NOTE_BODY_MAX_BYTES} bytes` };
49
+ }
50
+ /**
51
+ * A refusal when `text` carries a secret. The reason names the pattern's kind
52
+ * and never the matched text: a refusal is shown to the agent and logged.
53
+ */
54
+ function secretRefusal(field, text) {
55
+ const secret = findSecret(text);
56
+ return secret === null ? null : { field, reason: `secret (${secret.kind}) must not be stored` };
57
+ }
58
+ /**
59
+ * The first secret-shaped run in `text`, as its pattern's `kind` label. Every
60
+ * pattern carries the `/g` flag (the logger needs it for `replace`), so
61
+ * `lastIndex` is reset before each scan — otherwise a second call would resume
62
+ * where the first stopped and skip a match.
63
+ */
64
+ export function findSecret(text) {
65
+ for (const { kind, re, acceptMatch } of SECRET_VALUE_PATTERNS) {
66
+ re.lastIndex = 0;
67
+ let match = re.exec(text);
68
+ while (match !== null) {
69
+ const matched = match[0];
70
+ // A zero-length match would leave `lastIndex` unchanged and spin forever.
71
+ if (matched === "") {
72
+ re.lastIndex += 1;
73
+ }
74
+ else if (acceptMatch === undefined || acceptMatch(matched)) {
75
+ return { kind };
76
+ }
77
+ match = re.exec(text);
78
+ }
79
+ }
80
+ return null;
81
+ }
82
+ /**
83
+ * The refusal for a forget's reason, or `null` when it may be recorded.
84
+ *
85
+ * A reason is what a later session reads to know why a note went, so it is held to the same
86
+ * one-line, non-empty, length-capped rule as a note's description: it is persisted on the
87
+ * request and inside a queued entry, and neither of those is a place to keep an essay.
88
+ */
89
+ export function forgetReasonRefusal(reason) {
90
+ return lineRefusal("reason", reason, MEMORY_FORGET_REASON_MAX_CHARS);
91
+ }
92
+ /**
93
+ * The refusal for a note write, or `null` when it may be stored. Stops at the
94
+ * first problem, in the design's order, so the caller reports one reason.
95
+ */
96
+ export function validateMemoryNoteWrite(write, source) {
97
+ return (slugRefusal(write.slug) ??
98
+ typeRefusal(write.type) ??
99
+ lineRefusal("description", write.description, MEMORY_DESCRIPTION_MAX_CHARS) ??
100
+ lineRefusal("source", source, MEMORY_SOURCE_MAX_CHARS) ??
101
+ bodyRefusal(write.body) ??
102
+ secretRefusal("description", write.description) ??
103
+ secretRefusal("body", write.body));
104
+ }
@@ -1,3 +1,3 @@
1
- export { LOCK_HEARTBEAT_MS, LOCK_STALE_MS } from "./session-lock.ts";
1
+ export { acquireSessionLock, LOCK_HEARTBEAT_MS, LOCK_STALE_MS } from "./session-lock.ts";
2
2
  export { defaultSessionsDir, SESSION_ID_PATTERN, SESSION_META_MODES, type SessionMeta, type SessionMetaMode, } from "./session-meta.ts";
3
3
  export { createSessionStorage, LIST_PAGE_SIZE, processAlive, type SessionListEntry, type SessionListPage, type SessionLockHolder, type SessionLockHolderState, type SessionStorage, type SessionStorageDeps, } from "./storage.ts";
@@ -1,3 +1,3 @@
1
- export { LOCK_HEARTBEAT_MS, LOCK_STALE_MS } from "./session-lock.js";
1
+ export { acquireSessionLock, LOCK_HEARTBEAT_MS, LOCK_STALE_MS } from "./session-lock.js";
2
2
  export { defaultSessionsDir, SESSION_ID_PATTERN, SESSION_META_MODES, } from "./session-meta.js";
3
3
  export { createSessionStorage, LIST_PAGE_SIZE, processAlive, } from "./storage.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax-agent",
3
- "version": "0.91.7",
3
+ "version": "0.91.8-canary.1",
4
4
  "description": "nax's native coding agent: session contract, native loop, tools, permissions, sandbox, command-safety.",
5
5
  "license": "MIT",
6
6
  "author": "William Khoo",
@@ -47,13 +47,13 @@
47
47
  "dependencies": {
48
48
  "@anthropic-ai/sandbox-runtime": "0.0.77",
49
49
  "@modelcontextprotocol/sdk": "^1.32.1",
50
- "@nathapp/nax-ai": "0.91.7",
50
+ "@nathapp/nax-ai": "0.91.8-canary.1",
51
51
  "zod": "^4.3.6"
52
52
  },
53
53
  "publishConfig": {
54
54
  "access": "public",
55
55
  "registry": "https://registry.npmjs.org/",
56
56
  "provenance": true,
57
- "tag": "latest"
57
+ "tag": "canary"
58
58
  }
59
59
  }