@namzu/sdk 48.3.1 → 49.0.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.
Files changed (149) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/config/registry.d.ts +2 -0
  3. package/dist/config/registry.d.ts.map +1 -1
  4. package/dist/config/registry.js +23 -3
  5. package/dist/config/registry.js.map +1 -1
  6. package/dist/connector/mcp/discovery.d.ts +9 -5
  7. package/dist/connector/mcp/discovery.d.ts.map +1 -1
  8. package/dist/connector/mcp/discovery.js +16 -8
  9. package/dist/connector/mcp/discovery.js.map +1 -1
  10. package/dist/connector/mcp/mcp-toolset.d.ts +3 -0
  11. package/dist/connector/mcp/mcp-toolset.d.ts.map +1 -1
  12. package/dist/connector/mcp/mcp-toolset.js +20 -2
  13. package/dist/connector/mcp/mcp-toolset.js.map +1 -1
  14. package/dist/connector/mcp/reconnect.d.ts +2 -0
  15. package/dist/connector/mcp/reconnect.d.ts.map +1 -1
  16. package/dist/connector/mcp/reconnect.js +36 -5
  17. package/dist/connector/mcp/reconnect.js.map +1 -1
  18. package/dist/plugin/lifecycle.d.ts +14 -10
  19. package/dist/plugin/lifecycle.d.ts.map +1 -1
  20. package/dist/plugin/lifecycle.js +178 -41
  21. package/dist/plugin/lifecycle.js.map +1 -1
  22. package/dist/prompt/resident-learning.js +2 -2
  23. package/dist/prompt/resident-learning.js.map +1 -1
  24. package/dist/prompt/resident-step.d.ts +2 -2
  25. package/dist/prompt/resident-step.js +4 -4
  26. package/dist/prompt/resident-step.js.map +1 -1
  27. package/dist/public-runtime.d.ts +2 -2
  28. package/dist/public-runtime.d.ts.map +1 -1
  29. package/dist/public-runtime.js +2 -2
  30. package/dist/public-runtime.js.map +1 -1
  31. package/dist/registry/tool/screen.d.ts.map +1 -1
  32. package/dist/registry/tool/screen.js +33 -2
  33. package/dist/registry/tool/screen.js.map +1 -1
  34. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  35. package/dist/runtime/jobs/registry.js +26 -5
  36. package/dist/runtime/jobs/registry.js.map +1 -1
  37. package/dist/runtime/query/index.d.ts +6 -0
  38. package/dist/runtime/query/index.d.ts.map +1 -1
  39. package/dist/runtime/query/index.js +20 -1
  40. package/dist/runtime/query/index.js.map +1 -1
  41. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  42. package/dist/runtime/query/iteration/index.js +32 -8
  43. package/dist/runtime/query/iteration/index.js.map +1 -1
  44. package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
  45. package/dist/runtime/query/iteration/outstanding-work.js +2 -0
  46. package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
  47. package/dist/runtime/query/iteration/phases/context.d.ts +4 -0
  48. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  49. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  50. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  51. package/dist/runtime/query/iteration/phases/tool-review.js +7 -1
  52. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  53. package/dist/runtime/query/iteration/step-shaping.d.ts +2 -0
  54. package/dist/runtime/query/iteration/step-shaping.d.ts.map +1 -1
  55. package/dist/runtime/query/iteration/step-shaping.js +5 -1
  56. package/dist/runtime/query/iteration/step-shaping.js.map +1 -1
  57. package/dist/store/index.d.ts +1 -0
  58. package/dist/store/index.d.ts.map +1 -1
  59. package/dist/store/index.js +1 -0
  60. package/dist/store/index.js.map +1 -1
  61. package/dist/store/memory/disk.d.ts +7 -2
  62. package/dist/store/memory/disk.d.ts.map +1 -1
  63. package/dist/store/memory/disk.js +58 -5
  64. package/dist/store/memory/disk.js.map +1 -1
  65. package/dist/store/memory/index.d.ts +9 -0
  66. package/dist/store/memory/index.d.ts.map +1 -1
  67. package/dist/store/memory/index.js +43 -5
  68. package/dist/store/memory/index.js.map +1 -1
  69. package/dist/store/memory/markdown.d.ts +7 -2
  70. package/dist/store/memory/markdown.d.ts.map +1 -1
  71. package/dist/store/memory/markdown.js +33 -0
  72. package/dist/store/memory/markdown.js.map +1 -1
  73. package/dist/store/memory/memory.d.ts +7 -2
  74. package/dist/store/memory/memory.d.ts.map +1 -1
  75. package/dist/store/memory/memory.js +34 -0
  76. package/dist/store/memory/memory.js.map +1 -1
  77. package/dist/store/memory/revision.d.ts +16 -0
  78. package/dist/store/memory/revision.d.ts.map +1 -0
  79. package/dist/store/memory/revision.js +144 -0
  80. package/dist/store/memory/revision.js.map +1 -0
  81. package/dist/tools/builtins/wait-for-job-bounds.d.ts +6 -4
  82. package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
  83. package/dist/tools/builtins/wait-for-job-bounds.js +42 -8
  84. package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
  85. package/dist/tools/builtins/wait-for-job.d.ts +1 -0
  86. package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
  87. package/dist/tools/builtins/wait-for-job.js +24 -10
  88. package/dist/tools/builtins/wait-for-job.js.map +1 -1
  89. package/dist/tools/memory/delete.d.ts.map +1 -1
  90. package/dist/tools/memory/delete.js +38 -3
  91. package/dist/tools/memory/delete.js.map +1 -1
  92. package/dist/tools/memory/read.d.ts.map +1 -1
  93. package/dist/tools/memory/read.js +38 -2
  94. package/dist/tools/memory/read.js.map +1 -1
  95. package/dist/tools/memory/search.d.ts.map +1 -1
  96. package/dist/tools/memory/search.js +38 -7
  97. package/dist/tools/memory/search.js.map +1 -1
  98. package/dist/tools/memory/update.d.ts.map +1 -1
  99. package/dist/tools/memory/update.js +30 -3
  100. package/dist/tools/memory/update.js.map +1 -1
  101. package/dist/toolsets/manager.d.ts +13 -0
  102. package/dist/toolsets/manager.d.ts.map +1 -1
  103. package/dist/toolsets/manager.js +60 -1
  104. package/dist/toolsets/manager.js.map +1 -1
  105. package/dist/turn/memory-recall.d.ts +2 -0
  106. package/dist/turn/memory-recall.d.ts.map +1 -1
  107. package/dist/turn/memory-recall.js +3 -1
  108. package/dist/turn/memory-recall.js.map +1 -1
  109. package/dist/types/memory/index.d.ts +29 -0
  110. package/dist/types/memory/index.d.ts.map +1 -1
  111. package/dist/types/memory/index.js +6 -0
  112. package/dist/types/memory/index.js.map +1 -1
  113. package/dist/types/plugin/index.d.ts +12 -2
  114. package/dist/types/plugin/index.d.ts.map +1 -1
  115. package/dist/types/plugin/index.js +5 -2
  116. package/dist/types/plugin/index.js.map +1 -1
  117. package/package.json +1 -1
  118. package/src/config/registry.ts +24 -3
  119. package/src/connector/mcp/discovery.ts +29 -8
  120. package/src/connector/mcp/mcp-toolset.ts +25 -4
  121. package/src/connector/mcp/reconnect.ts +34 -4
  122. package/src/plugin/lifecycle.ts +217 -46
  123. package/src/prompt/resident-learning.ts +2 -2
  124. package/src/prompt/resident-step.ts +4 -4
  125. package/src/public-runtime.ts +2 -0
  126. package/src/registry/tool/screen.ts +34 -2
  127. package/src/runtime/jobs/registry.ts +25 -8
  128. package/src/runtime/query/index.ts +24 -1
  129. package/src/runtime/query/iteration/index.ts +35 -10
  130. package/src/runtime/query/iteration/outstanding-work.ts +2 -0
  131. package/src/runtime/query/iteration/phases/context.ts +4 -0
  132. package/src/runtime/query/iteration/phases/tool-review.ts +6 -1
  133. package/src/runtime/query/iteration/step-shaping.ts +9 -1
  134. package/src/store/index.ts +1 -0
  135. package/src/store/memory/disk.ts +82 -7
  136. package/src/store/memory/index.ts +66 -7
  137. package/src/store/memory/markdown.ts +54 -2
  138. package/src/store/memory/memory.ts +52 -2
  139. package/src/store/memory/revision.ts +158 -0
  140. package/src/tools/builtins/wait-for-job-bounds.ts +42 -7
  141. package/src/tools/builtins/wait-for-job.ts +25 -11
  142. package/src/tools/memory/delete.ts +33 -3
  143. package/src/tools/memory/read.ts +35 -2
  144. package/src/tools/memory/search.ts +38 -7
  145. package/src/tools/memory/update.ts +28 -3
  146. package/src/toolsets/manager.ts +74 -1
  147. package/src/turn/memory-recall.ts +5 -1
  148. package/src/types/memory/index.ts +43 -0
  149. package/src/types/plugin/index.ts +11 -6
@@ -1,13 +1,14 @@
1
1
  import type { MemoryId } from '../../types/ids/index.js'
2
2
  import type {
3
+ ConditionalMemoryStore,
3
4
  CreateMemoryParams,
4
5
  MemoryContent,
5
6
  MemoryIndexEntry,
6
7
  MemoryRecord,
7
8
  MemorySearchParams,
8
9
  MemorySearchResult,
9
- MemoryStore,
10
10
  UpdateMemoryParams,
11
+ VersionedMemoryRecord,
11
12
  } from '../../types/memory/index.js'
12
13
  import { assertMemoryStatus } from '../../types/memory/index.js'
13
14
  import { generateMemoryId } from '../../utils/id.js'
@@ -18,8 +19,9 @@ import {
18
19
  nameHolder,
19
20
  withOptionalFields,
20
21
  } from './naming.js'
22
+ import { assertMemoryRevision, memoryRevision } from './revision.js'
21
23
 
22
- export class InMemoryMemoryStore implements MemoryStore {
24
+ export class InMemoryMemoryStore implements ConditionalMemoryStore {
23
25
  private content = new Map<string, MemoryContent>()
24
26
  private index = new InMemoryMemoryIndex()
25
27
 
@@ -70,10 +72,45 @@ export class InMemoryMemoryStore implements MemoryStore {
70
72
  return entry && content ? structuredClone({ entry, content }) : undefined
71
73
  }
72
74
 
75
+ async getVersionedRecord(id: MemoryId): Promise<VersionedMemoryRecord | undefined> {
76
+ const entry = this.index.getEntry(id)
77
+ const content = this.content.get(id)
78
+ if (!entry || !content) return undefined
79
+ const record = structuredClone({ entry, content })
80
+ return { ...record, revision: memoryRevision(record) }
81
+ }
82
+
73
83
  async update(id: MemoryId, updates: UpdateMemoryParams): Promise<MemoryIndexEntry | undefined> {
84
+ return this.updateNow(id, updates)
85
+ }
86
+
87
+ async updateIfRevision(
88
+ id: MemoryId,
89
+ updates: UpdateMemoryParams,
90
+ expectedRevision: string,
91
+ ): Promise<MemoryIndexEntry> {
92
+ // No await between the check and mutation: one in-memory operation turn.
93
+ const entry = this.updateNow(id, updates, expectedRevision)
94
+ if (!entry) throw new Error('A conditional memory update unexpectedly found no record.')
95
+ return entry
96
+ }
97
+
98
+ private updateNow(
99
+ id: MemoryId,
100
+ updates: UpdateMemoryParams,
101
+ expectedRevision?: string,
102
+ ): MemoryIndexEntry | undefined {
74
103
  if (updates.status !== undefined) assertMemoryStatus(updates.status)
75
104
  assertOptionalMemoryFields(updates)
76
105
  const existing = this.index.getEntry(id)
106
+ if (expectedRevision !== undefined) {
107
+ const content = this.content.get(id)
108
+ assertMemoryRevision(
109
+ id,
110
+ existing && content ? { entry: existing, content } : undefined,
111
+ expectedRevision,
112
+ )
113
+ }
77
114
  if (!existing) return undefined
78
115
  if (updates.name !== undefined) {
79
116
  const holder = nameHolder(this.index.allEntries(), updates.name, id)
@@ -118,6 +155,19 @@ export class InMemoryMemoryStore implements MemoryStore {
118
155
  }
119
156
 
120
157
  async delete(id: MemoryId): Promise<boolean> {
158
+ return this.deleteNow(id)
159
+ }
160
+
161
+ async deleteIfRevision(id: MemoryId, expectedRevision: string): Promise<void> {
162
+ this.deleteNow(id, expectedRevision)
163
+ }
164
+
165
+ private deleteNow(id: MemoryId, expectedRevision?: string): boolean {
166
+ if (expectedRevision !== undefined) {
167
+ const entry = this.index.getEntry(id)
168
+ const content = this.content.get(id)
169
+ assertMemoryRevision(id, entry && content ? { entry, content } : undefined, expectedRevision)
170
+ }
121
171
  const existed = this.index.remove(id)
122
172
  this.content.delete(id)
123
173
  return existed
@@ -0,0 +1,158 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { NamzuError } from '../../types/errors/index.js'
3
+ import type { MemoryId } from '../../types/ids/index.js'
4
+ import type { MemoryRecord } from '../../types/memory/index.js'
5
+
6
+ /** A record can be read, but its metadata cannot be fingerprinted synchronously. */
7
+ export class MemoryRevisionUnavailableError extends Error {
8
+ constructor() {
9
+ super('This memory contains metadata that cannot be revision-checked.')
10
+ this.name = 'MemoryRevisionUnavailableError'
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Encode the same structured-clone snapshot `getRecord` returns. Type tags,
16
+ * object references and sorted plain-object keys preserve values JSON loses:
17
+ * BigInt, undefined, Map/Set contents, Date, typed arrays and cycles. Map and
18
+ * Set order is kept because it is observable. Unknown host objects (notably
19
+ * Blob, whose bytes need asynchronous reading) fail closed rather than hash as
20
+ * `{}` and allow a stale conditional write.
21
+ */
22
+ function canonicalRecord(record: MemoryRecord): string {
23
+ let snapshot: MemoryRecord
24
+ try {
25
+ snapshot = structuredClone(record)
26
+ } catch {
27
+ throw new MemoryRevisionUnavailableError()
28
+ }
29
+ const seen = new WeakMap<object, number>()
30
+ let nextId = 0
31
+
32
+ function encode(value: unknown): unknown {
33
+ if (value === null) return ['null']
34
+ switch (typeof value) {
35
+ case 'undefined':
36
+ return ['undefined']
37
+ case 'string':
38
+ return ['string', value]
39
+ case 'boolean':
40
+ return ['boolean', value]
41
+ case 'bigint':
42
+ return ['bigint', value.toString()]
43
+ case 'number':
44
+ return ['number', Object.is(value, -0) ? '-0' : String(value)]
45
+ case 'function':
46
+ case 'symbol':
47
+ throw new MemoryRevisionUnavailableError()
48
+ case 'object':
49
+ break
50
+ }
51
+
52
+ const object = value as object
53
+ const previous = seen.get(object)
54
+ if (previous !== undefined) return ['ref', previous]
55
+ const id = nextId++
56
+ seen.set(object, id)
57
+ if (Array.isArray(object)) {
58
+ return [
59
+ 'array',
60
+ id,
61
+ object.length,
62
+ Object.keys(object)
63
+ .sort()
64
+ .map((key) => [key, encode((object as unknown as Record<string, unknown>)[key])]),
65
+ ]
66
+ }
67
+ if (object instanceof Map) {
68
+ return ['map', id, [...object].map(([key, item]) => [encode(key), encode(item)])]
69
+ }
70
+ if (object instanceof Set) return ['set', id, [...object].map(encode)]
71
+ if (object instanceof Date) return ['date', id, encode(object.getTime())]
72
+ if (object instanceof RegExp)
73
+ return ['regexp', id, object.source, object.flags, object.lastIndex]
74
+ if (object instanceof ArrayBuffer) {
75
+ const buffer = object as ArrayBuffer & {
76
+ readonly maxByteLength?: number
77
+ readonly resizable?: boolean
78
+ }
79
+ return [
80
+ 'array-buffer',
81
+ id,
82
+ buffer.byteLength,
83
+ buffer.maxByteLength,
84
+ buffer.resizable,
85
+ Buffer.from(buffer).toString('hex'),
86
+ ]
87
+ }
88
+ if (ArrayBuffer.isView(object)) {
89
+ // A view exposes its entire backing buffer through `.buffer`, even
90
+ // when its visible slice is one byte. Structured cloning keeps that
91
+ // backing and preserves aliases between multiple views.
92
+ if (
93
+ (typeof SharedArrayBuffer !== 'undefined' && object.buffer instanceof SharedArrayBuffer) ||
94
+ (object.buffer instanceof ArrayBuffer &&
95
+ (object.buffer as ArrayBuffer & { readonly resizable?: boolean }).resizable === true)
96
+ )
97
+ throw new MemoryRevisionUnavailableError()
98
+ return [
99
+ 'array-buffer-view',
100
+ id,
101
+ object.constructor.name,
102
+ object.byteOffset,
103
+ object.byteLength,
104
+ encode(object.buffer),
105
+ ]
106
+ }
107
+ if (object instanceof Error) {
108
+ return ['error', id, object.name, object.message, object.stack, encode(object.cause)]
109
+ }
110
+ if (object instanceof Number || object instanceof String || object instanceof Boolean)
111
+ return ['boxed', id, object.constructor.name, encode(object.valueOf())]
112
+ if (typeof SharedArrayBuffer !== 'undefined' && object instanceof SharedArrayBuffer)
113
+ throw new MemoryRevisionUnavailableError()
114
+ const prototype = Object.getPrototypeOf(object)
115
+ if (prototype !== Object.prototype && prototype !== null)
116
+ throw new MemoryRevisionUnavailableError()
117
+ return [
118
+ 'object',
119
+ id,
120
+ Object.keys(object as Record<string, unknown>)
121
+ .sort()
122
+ .map((key) => [key, encode((object as Record<string, unknown>)[key])]),
123
+ ]
124
+ }
125
+
126
+ return JSON.stringify(encode(snapshot))
127
+ }
128
+
129
+ /** A content-derived token; it includes metadata as well as the body. */
130
+ export function memoryRevision(record: MemoryRecord): string {
131
+ return `m2:${createHash('sha256').update(canonicalRecord(record)).digest('hex')}`
132
+ }
133
+
134
+ /** The record changed or disappeared after its revision was read. */
135
+ export class MemoryRevisionConflictError extends NamzuError {
136
+ readonly memoryId: MemoryId
137
+
138
+ constructor(memoryId: MemoryId) {
139
+ super({
140
+ code: 'storage_error',
141
+ message: `Memory ${memoryId} changed since it was read. Read it again before changing it.`,
142
+ details: { memoryId, reason: 'revision_conflict' },
143
+ retryable: false,
144
+ })
145
+ this.name = 'MemoryRevisionConflictError'
146
+ this.memoryId = memoryId
147
+ }
148
+ }
149
+
150
+ export function assertMemoryRevision(
151
+ id: MemoryId,
152
+ record: MemoryRecord | undefined,
153
+ expectedRevision: string,
154
+ ): asserts record is MemoryRecord {
155
+ if (!record || memoryRevision(record) !== expectedRevision) {
156
+ throw new MemoryRevisionConflictError(id)
157
+ }
158
+ }
@@ -1,4 +1,5 @@
1
1
  import type { BackgroundJobRegistryRef } from '../../types/tool/index.js'
2
+ import { subscribeToAbort } from '../../utils/abort.js'
2
3
 
3
4
  /**
4
5
  * Waiting on a background job, bounded by two different questions — the
@@ -42,6 +43,8 @@ interface JobWaitProgress {
42
43
  readonly output: string
43
44
  readonly nextOffset: number
44
45
  readonly droppedBytes: number
46
+ /** Earlier bytes omitted from this wait's own bounded output, separately from registry loss. */
47
+ readonly omittedOutputBytes: number
45
48
  }
46
49
 
47
50
  export type JobWaitOutcome =
@@ -67,12 +70,27 @@ export type JobWaitOutcome =
67
70
  */
68
71
  const POLL_INTERVAL_MS = 1_000
69
72
 
73
+ /** Leave room for the status and recovery instructions inside the tool-result budget. */
74
+ const MAX_WAIT_OUTPUT_BYTES = 32 * 1024
75
+
76
+ function boundedTail(value: string): { output: string; omittedBytes: number } {
77
+ const bytes = Buffer.from(value, 'utf8')
78
+ if (bytes.length <= MAX_WAIT_OUTPUT_BYTES) return { output: value, omittedBytes: 0 }
79
+ let start = bytes.length - MAX_WAIT_OUTPUT_BYTES
80
+ // The cap is in bytes, but the displayed tail must begin at a UTF-8 character.
81
+ while (start < bytes.length && ((bytes[start] ?? 0) & 0xc0) === 0x80) start++
82
+ return {
83
+ output: bytes.subarray(start).toString('utf8'),
84
+ omittedBytes: start,
85
+ }
86
+ }
87
+
70
88
  /**
71
- * Await a job under both bounds, accumulating its output as it goes.
89
+ * Await a job under both bounds, retaining a bounded tail as it goes.
72
90
  *
73
- * Returns the output gathered so far either way: a completed wait has all
74
- * of it, and a timed-out one has everything read up to the moment it gave
75
- * up, so the caller never has to throw away a partial answer.
91
+ * A completed or timed-out wait returns the bytes it observed up to its
92
+ * absolute cursor. If the displayed tail could not hold all of them, its
93
+ * omitted-byte count keeps that gap visible to the caller.
76
94
  */
77
95
  export async function waitForJobWithBounds(
78
96
  jobs: Pick<BackgroundJobRegistryRef, 'read' | 'waitForExit'>,
@@ -90,8 +108,16 @@ export async function waitForJobWithBounds(
90
108
  let cursor = options.fromOffset ?? 0
91
109
  let output = ''
92
110
  let droppedBytes = 0
111
+ let omittedOutputBytes = 0
93
112
  let settled = false
94
113
  let poll: ReturnType<typeof setInterval> | undefined
114
+ // Each bounded wait owns its exit subscription. A timeout must release that
115
+ // subscription even when the job itself runs forever.
116
+ const waitAbort = new AbortController()
117
+ const forwardAbort = (): void => waitAbort.abort(options.signal?.reason)
118
+ let disposeCallerAbort = () => {}
119
+ if (options.signal?.aborted) forwardAbort()
120
+ else if (options.signal) disposeCallerAbort = subscribeToAbort(options.signal, forwardAbort)
95
121
 
96
122
  /** Read whatever is new since `cursor`, and count it as progress if it is. */
97
123
  const drain = (): void => {
@@ -99,11 +125,15 @@ export async function waitForJobWithBounds(
99
125
  if (chunk.droppedBytes > 0) droppedBytes += chunk.droppedBytes
100
126
  if (chunk.nextOffset > cursor) lastProgressAt = now()
101
127
  cursor = chunk.nextOffset
102
- if (chunk.chunk) output += chunk.chunk
128
+ if (chunk.chunk) {
129
+ const bounded = boundedTail(output + chunk.chunk)
130
+ output = bounded.output
131
+ omittedOutputBytes += bounded.omittedBytes
132
+ }
103
133
  }
104
134
 
105
135
  try {
106
- const exited = waitForExit(id, { signal: options.signal }).then((job): JobWaitOutcome => {
136
+ const exited = waitForExit(id, { signal: waitAbort.signal }).then((job): JobWaitOutcome => {
107
137
  // One last read: the job can exit between ticks, and the bytes
108
138
  // it wrote in its final moment are exactly the ones a caller
109
139
  // most wants — the error, the summary line, the exit trace.
@@ -115,6 +145,7 @@ export async function waitForJobWithBounds(
115
145
  output,
116
146
  nextOffset: cursor,
117
147
  droppedBytes,
148
+ omittedOutputBytes,
118
149
  }
119
150
  })
120
151
 
@@ -136,6 +167,7 @@ export async function waitForJobWithBounds(
136
167
  output,
137
168
  nextOffset: cursor,
138
169
  droppedBytes,
170
+ omittedOutputBytes,
139
171
  })
140
172
  return
141
173
  }
@@ -150,6 +182,7 @@ export async function waitForJobWithBounds(
150
182
  output,
151
183
  nextOffset: cursor,
152
184
  droppedBytes,
185
+ omittedOutputBytes,
153
186
  })
154
187
  }
155
188
  }
@@ -165,6 +198,8 @@ export async function waitForJobWithBounds(
165
198
  } finally {
166
199
  settled = true
167
200
  if (poll !== undefined) clearInterval(poll)
201
+ disposeCallerAbort()
202
+ waitAbort.abort(new Error('Job wait finished'))
168
203
  }
169
204
  }
170
205
 
@@ -174,7 +209,7 @@ export function describeJobWaitTimeout(
174
209
  outcome: Extract<JobWaitOutcome, { kind: 'timeout' }>,
175
210
  ): string {
176
211
  const seconds = Math.round(outcome.elapsedMs / 1000)
177
- const resume = `call wait_for_job again, or job read with from_offset ${outcome.nextOffset}, to see what it does next`
212
+ const resume = `call wait_for_job or job read with from_offset ${outcome.nextOffset} to see only what comes next`
178
213
  if (outcome.cause === 'idle') {
179
214
  return `Job ${id} went quiet: no new output for a while, after ${seconds}s. It has not been stopped and may still be working — ${resume}.`
180
215
  }
@@ -35,6 +35,12 @@ const MAX_WAIT_MS = readPositiveIntEnv('NAMZU_JOB_WAIT_MAX_MS', 60 * 60 * 1000)
35
35
 
36
36
  const inputSchema = z.object({
37
37
  id: z.string().describe('The job id, as returned by bash with run_in_background.'),
38
+ from_offset: z
39
+ .number()
40
+ .int()
41
+ .nonnegative()
42
+ .optional()
43
+ .describe('Resume after a previous wait: pass its next_offset to receive only new output.'),
38
44
  timeout_ms: z
39
45
  .number()
40
46
  .int()
@@ -60,7 +66,7 @@ type WaitForJobInput = z.infer<typeof inputSchema>
60
66
  export const WaitForJobTool = defineTool({
61
67
  name: 'wait_for_job',
62
68
  description:
63
- 'Block until a background job started by bash with run_in_background ends, and return its accumulated output in one call. Use this instead of calling job with action "read" in a loop: it costs one call and no waiting turns. Gives up — WITHOUT stopping the job — if it runs too long or goes quiet for too long; either outcome says so, and the job keeps running either way.',
69
+ 'Block until a background job started by bash with run_in_background ends, and return its bounded output in one call. Use this instead of calling job with action "read" in a loop: it costs one call and no waiting turns. A wait that runs too long or goes quiet returns its output and next_offset without stopping the job; pass that offset as from_offset on the next wait to avoid repeats.',
64
70
  inputSchema,
65
71
  category: 'shell',
66
72
  permissions: ['shell_execute'],
@@ -128,6 +134,7 @@ export const WaitForJobTool = defineTool({
128
134
  outcome = await waitForJobWithBounds(jobs, input.id, {
129
135
  wallMs: input.timeout_ms ?? DEFAULT_TIMEOUT_MS,
130
136
  idleMs: input.idle_timeout_ms ?? DEFAULT_IDLE_TIMEOUT_MS,
137
+ ...(input.from_offset === undefined ? {} : { fromOffset: input.from_offset }),
131
138
  signal: context.abortSignal,
132
139
  })
133
140
  } catch {
@@ -144,26 +151,32 @@ export const WaitForJobTool = defineTool({
144
151
  }
145
152
  }
146
153
 
154
+ const notices = [
155
+ ...(outcome.droppedBytes > 0
156
+ ? [`[${outcome.droppedBytes} bytes were dropped by the job's output retention cap]`]
157
+ : []),
158
+ ...(outcome.omittedOutputBytes > 0
159
+ ? [`[${outcome.omittedOutputBytes} earlier bytes omitted from this bounded wait result]`]
160
+ : []),
161
+ ]
162
+ const observed = `${notices.length > 0 ? `${notices.join('\n')}\n` : ''}${outcome.output || '(no new output)'}`
163
+
147
164
  if (outcome.kind === 'timeout') {
148
165
  return {
149
- success: false,
150
- output: describeJobWaitTimeout(input.id, outcome),
166
+ // The wait observed a bound, not a failed command. A tool error
167
+ // would also be promoted as a durable failure by the turn's memory.
168
+ success: true,
169
+ output: `${describeJobWaitTimeout(input.id, outcome)}\n\n${observed}\n\n[next_offset ${outcome.nextOffset}]`,
151
170
  data: {
152
171
  jobId: input.id,
153
172
  timedOut: outcome.cause,
154
173
  nextOffset: outcome.nextOffset,
155
174
  droppedBytes: outcome.droppedBytes,
175
+ omittedOutputBytes: outcome.omittedOutputBytes,
156
176
  },
157
177
  }
158
178
  }
159
179
 
160
- // The dropped count is stated, never absorbed — same rule `job read`
161
- // follows, for the same reason: a job whose middle vanished quietly
162
- // reads as a complete result that happens to be short.
163
- const notice =
164
- outcome.droppedBytes > 0
165
- ? `[${outcome.droppedBytes} bytes were dropped before this point — the job produced output faster than the retention cap holds]\n`
166
- : ''
167
180
  const status =
168
181
  outcome.exitCode === undefined
169
182
  ? outcome.status
@@ -171,12 +184,13 @@ export const WaitForJobTool = defineTool({
171
184
 
172
185
  return {
173
186
  success: true,
174
- output: `${notice}${outcome.output || '(no output)'}\n\n[job ${input.id} is ${status}; next_offset ${outcome.nextOffset}]`,
187
+ output: `${observed}\n\n[job ${input.id} is ${status}; next_offset ${outcome.nextOffset}]`,
175
188
  data: {
176
189
  jobId: input.id,
177
190
  status: outcome.status,
178
191
  nextOffset: outcome.nextOffset,
179
192
  droppedBytes: outcome.droppedBytes,
193
+ omittedOutputBytes: outcome.omittedOutputBytes,
180
194
  ...(outcome.exitCode === undefined ? {} : { exitCode: outcome.exitCode }),
181
195
  },
182
196
  }
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod'
2
+ import { MemoryRevisionConflictError } from '../../store/memory/revision.js'
2
3
  import type { MemoryStore } from '../../types/memory/index.js'
4
+ import { hasConditionalMemoryWrites } from '../../types/memory/index.js'
3
5
  import type { ToolDefinition } from '../../types/tool/index.js'
4
6
  import { asMemoryId } from '../../utils/id.js'
5
7
  import { defineTool } from '../defineTool.js'
@@ -8,17 +10,45 @@ export function buildDeleteMemoryTool(store: MemoryStore): ToolDefinition {
8
10
  return defineTool({
9
11
  name: 'delete_memory',
10
12
  description:
11
- 'Permanently delete a stored memory by ID. Use update_memory with status archived when the record should remain available for inspection. Deletion does not erase past conversation transcripts.',
13
+ 'Permanently delete a stored memory by ID. Use update_memory with status archived when the record should remain available for inspection. Deletion does not erase past conversation transcripts. Pass the revision from read_memory when available to refuse a stale deletion.',
12
14
  inputSchema: z.object({
13
15
  id: z.string().describe('Memory ID to permanently delete'),
16
+ revision: z
17
+ .string()
18
+ .min(1)
19
+ .optional()
20
+ .describe('Opaque revision from read_memory; refuse deletion if the record changed'),
14
21
  }),
15
22
  category: 'custom',
16
23
  permissions: [],
17
24
  readOnly: false,
18
25
  destructive: true,
19
26
  concurrencySafe: true,
20
- async execute({ id }) {
21
- const removed = await store.delete(asMemoryId(id))
27
+ async execute({ id, revision }) {
28
+ if (revision !== undefined && !hasConditionalMemoryWrites(store)) {
29
+ return {
30
+ success: false,
31
+ output: 'This memory store does not support revision-checked deletion.',
32
+ error: 'Conditional memory deletion unavailable',
33
+ }
34
+ }
35
+ let removed: boolean
36
+ try {
37
+ if (revision === undefined) {
38
+ removed = await store.delete(asMemoryId(id))
39
+ } else if (hasConditionalMemoryWrites(store)) {
40
+ await store.deleteIfRevision(asMemoryId(id), revision)
41
+ removed = true
42
+ } else throw new Error('Conditional memory support changed during the deletion.')
43
+ } catch (error) {
44
+ if (!(error instanceof MemoryRevisionConflictError)) throw error
45
+ return {
46
+ success: false,
47
+ output: error.message,
48
+ error: 'Memory changed since read',
49
+ data: { reason: 'revision_conflict' },
50
+ }
51
+ }
22
52
  return {
23
53
  success: removed,
24
54
  output: removed ? `Memory deleted: ${id}.` : `Memory ${id} not found.`,
@@ -4,8 +4,10 @@ import {
4
4
  describeMemoryAge,
5
5
  memoryLinkNames,
6
6
  } from '../../store/memory/links.js'
7
+ import { MemoryRevisionUnavailableError } from '../../store/memory/revision.js'
7
8
  import type { MemoryId } from '../../types/ids/index.js'
8
9
  import type { MemoryContent, MemoryIndexEntry, MemoryStore } from '../../types/memory/index.js'
10
+ import { hasConditionalMemoryWrites } from '../../types/memory/index.js'
9
11
  import type { ToolDefinition } from '../../types/tool/index.js'
10
12
  import { defineTool } from '../defineTool.js'
11
13
  import { allMemoryEntries, resolveMemoryReference } from './resolve.js'
@@ -43,7 +45,22 @@ export function buildReadMemoryTool(store: MemoryStore): ToolDefinition {
43
45
 
44
46
  let entry: MemoryIndexEntry | undefined
45
47
  let content: MemoryContent | undefined
46
- if (store.getRecord) {
48
+ let revision: string | undefined
49
+ if (hasConditionalMemoryWrites(store)) {
50
+ try {
51
+ const record = await store.getVersionedRecord(memoryId)
52
+ entry = record?.entry
53
+ content = record?.content
54
+ revision = record?.revision
55
+ } catch (error) {
56
+ if (!(error instanceof MemoryRevisionUnavailableError)) throw error
57
+ // Legacy in-memory records may hold cloneable host objects with
58
+ // inaccessible bytes. Read them without claiming CAS support.
59
+ const record = await store.getRecord?.(memoryId)
60
+ entry = record?.entry
61
+ content = record?.content ?? (await store.get(memoryId))
62
+ }
63
+ } else if (store.getRecord) {
47
64
  const record = await store.getRecord(memoryId)
48
65
  entry = record?.entry
49
66
  content = record?.content
@@ -92,9 +109,24 @@ export function buildReadMemoryTool(store: MemoryStore): ToolDefinition {
92
109
  // A JSON body stays exactly the stored text, parseable as it was
93
110
  // before these notes existed; its age and links are in `data`.
94
111
  const annotate = notes.length > 0 && content.format !== 'json'
112
+ const output = annotate ? `${content.content}\n\n---\n${notes.join('\n')}` : content.content
95
113
  return {
96
114
  success: true,
97
- output: annotate ? `${content.content}\n\n---\n${notes.join('\n')}` : content.content,
115
+ output,
116
+ // The executor sends output or content to the model, not data. Keep
117
+ // output byte-compatible (including JSON) while exposing the token
118
+ // in a separate model-visible text block.
119
+ ...(revision !== undefined
120
+ ? {
121
+ content: [
122
+ { type: 'text' as const, text: output },
123
+ {
124
+ type: 'text' as const,
125
+ text: `Memory revision: ${revision}. Pass it as revision to update_memory or delete_memory to reject stale changes.`,
126
+ },
127
+ ],
128
+ }
129
+ : {}),
98
130
  data: {
99
131
  id: content.id,
100
132
  format: content.format,
@@ -102,6 +134,7 @@ export function buildReadMemoryTool(store: MemoryStore): ToolDefinition {
102
134
  ...(entry?.name !== undefined ? { name: entry.name } : {}),
103
135
  ...(entry?.type !== undefined ? { type: entry.type } : {}),
104
136
  ...(entry ? { updatedAt: entry.updatedAt } : {}),
137
+ ...(revision !== undefined ? { revision } : {}),
105
138
  ...(resolved.length > 0 ? { links: resolved } : {}),
106
139
  },
107
140
  }
@@ -10,12 +10,13 @@ import type { ToolDefinition } from '../../types/tool/index.js'
10
10
  import { defineTool } from '../defineTool.js'
11
11
 
12
12
  type SearchMemory = (params: MemorySearchParams) => MemorySearchResult | Promise<MemorySearchResult>
13
+ const MAX_MEMORY_SCAN = 256
13
14
 
14
15
  function defineSearchMemoryTool(search: SearchMemory): ToolDefinition {
15
16
  return defineTool({
16
17
  name: 'search_memory',
17
18
  description:
18
- 'Search active stored memories by relevant words or tags. The built-in store ranks matches in names, titles, descriptions, summaries and full content. Returns the title, name, type, age and description of each match; use read_memory for evidence. Set status to archived to inspect obsolete records.',
19
+ 'Search active stored memories by relevant words or tags. The built-in stores rank matches in names, titles, descriptions, summaries and full content. Built-in search examines at most 256 recent candidates per call; if incomplete, continue with the returned scan_offset. Use read_memory for evidence. Set status to archived to inspect obsolete records.',
19
20
  inputSchema: z.object({
20
21
  query: z.string().optional().describe('Relevant words or identifiers to search'),
21
22
  tags: z.array(z.string()).optional().describe('Filter by tags (all must match)'),
@@ -30,20 +31,47 @@ function defineSearchMemoryTool(search: SearchMemory): ToolDefinition {
30
31
  .max(50)
31
32
  .default(10)
32
33
  .describe('Maximum results to return (1–50)'),
34
+ scan_offset: z
35
+ .number()
36
+ .int()
37
+ .min(0)
38
+ .default(0)
39
+ .describe('Candidate offset from an earlier incomplete search; starts at 0'),
33
40
  }),
34
41
  category: 'analysis',
35
42
  permissions: [],
36
43
  readOnly: true,
37
44
  destructive: false,
38
45
  concurrencySafe: true,
39
- async execute({ query, tags, status, limit }) {
40
- const result = await search({ query, tags, status, limit })
46
+ async execute({ query, tags, status, limit, scan_offset }) {
47
+ const result = await search({
48
+ query,
49
+ tags,
50
+ status,
51
+ limit,
52
+ maxScanned: MAX_MEMORY_SCAN,
53
+ scanOffset: scan_offset,
54
+ })
55
+ const continuation = result.truncated
56
+ ? result.nextScanOffset === undefined
57
+ ? ' Search incomplete; narrow the query or read a known ID.'
58
+ : ` Search incomplete; continue with scan_offset ${result.nextScanOffset}.`
59
+ : ''
60
+ const page = scan_offset > 0 || result.truncated ? ' in this scan page' : ''
41
61
 
42
62
  if (result.entries.length === 0) {
43
63
  return {
44
64
  success: true,
45
- output: 'No memories found.',
46
- data: { entries: [], totalCount: 0 },
65
+ output: `No memories found${page}.${continuation}`,
66
+ data: {
67
+ entries: [],
68
+ totalCount: result.totalCount,
69
+ truncated: result.truncated === true,
70
+ ...(result.scannedCount !== undefined ? { scannedCount: result.scannedCount } : {}),
71
+ ...(result.nextScanOffset !== undefined
72
+ ? { nextScanOffset: result.nextScanOffset }
73
+ : {}),
74
+ },
47
75
  }
48
76
  }
49
77
 
@@ -57,8 +85,8 @@ function defineSearchMemoryTool(search: SearchMemory): ToolDefinition {
57
85
 
58
86
  const output =
59
87
  result.totalCount > result.entries.length
60
- ? `Found ${result.totalCount} memories (showing ${result.entries.length}):\n${lines.join('\n')}`
61
- : `Found ${result.totalCount} memories:\n${lines.join('\n')}`
88
+ ? `Found ${result.totalCount} memories${page} (showing ${result.entries.length}):\n${lines.join('\n')}${continuation}`
89
+ : `Found ${result.totalCount} memories${page}:\n${lines.join('\n')}${continuation}`
62
90
 
63
91
  return {
64
92
  success: true,
@@ -66,6 +94,9 @@ function defineSearchMemoryTool(search: SearchMemory): ToolDefinition {
66
94
  data: {
67
95
  entries: result.entries,
68
96
  totalCount: result.totalCount,
97
+ truncated: result.truncated === true,
98
+ ...(result.scannedCount !== undefined ? { scannedCount: result.scannedCount } : {}),
99
+ ...(result.nextScanOffset !== undefined ? { nextScanOffset: result.nextScanOffset } : {}),
69
100
  },
70
101
  }
71
102
  },