typeclaw 0.27.0 → 0.28.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.
- package/package.json +1 -1
- package/scripts/generate-schema.ts +4 -6
- package/src/agent/index.ts +26 -4
- package/src/agent/multimodal/look-at.ts +1 -2
- package/src/agent/provider-error.ts +33 -1
- package/src/agent/tools/channel-fetch-attachment.ts +1 -2
- package/src/agent/tools/channel-react.ts +9 -3
- package/src/agent/tools/channel-reply.ts +52 -1
- package/src/agent/tools/channel-send.ts +115 -1
- package/src/bundled-plugins/github-cli-auth/gh-review-detect.ts +175 -0
- package/src/bundled-plugins/github-cli-auth/index.ts +4 -0
- package/src/bundled-plugins/github-cli-auth/review-recorder.ts +93 -0
- package/src/bundled-plugins/guard/policies/managed-config.ts +1 -1
- package/src/bundled-plugins/memory/README.md +3 -21
- package/src/bundled-plugins/memory/index.ts +1 -149
- package/src/bundled-plugins/security/policies/cron-promotion.ts +2 -2
- package/src/channels/adapters/github/inbound.ts +103 -0
- package/src/channels/adapters/github/index.ts +10 -0
- package/src/channels/adapters/github/review-state.ts +137 -0
- package/src/channels/adapters/github/review-thread-resolver.ts +65 -5
- package/src/channels/github-false-receipt.ts +87 -0
- package/src/channels/github-rereview-guard.ts +76 -0
- package/src/channels/github-review-claim.ts +92 -0
- package/src/channels/github-review-turn-ledger.ts +71 -0
- package/src/channels/persistence.ts +4 -102
- package/src/channels/router.ts +181 -7
- package/src/channels/schema.ts +20 -5
- package/src/channels/types.ts +31 -0
- package/src/cli/channel.ts +2 -1
- package/src/cli/init.ts +2 -1
- package/src/config/config.ts +19 -288
- package/src/container/start.ts +0 -2
- package/src/cron/index.ts +3 -44
- package/src/cron/schema.ts +2 -96
- package/src/init/gitignore.ts +1 -2
- package/src/inspect/transcript-view.ts +10 -0
- package/src/secrets/defaults.ts +1 -18
- package/src/secrets/index.ts +0 -2
- package/src/secrets/schema.ts +4 -90
- package/src/secrets/storage.ts +0 -2
- package/src/server/index.ts +11 -5
- package/src/shared/protocol.ts +18 -6
- package/src/skills/typeclaw-config/SKILL.md +9 -11
- package/src/skills/typeclaw-permissions/SKILL.md +1 -1
- package/src/tui/format.ts +13 -0
- package/src/tui/index.ts +21 -7
- package/typeclaw.schema.json +1 -0
- package/src/agent/tools/normalize-ref.ts +0 -11
- package/src/bundled-plugins/memory/migration.ts +0 -633
- package/src/secrets/migrate-kakaotalk.ts +0 -82
- package/src/secrets/migrate.ts +0 -96
|
@@ -1,633 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { cp, mkdir, readdir, readFile, rename, rm, rmdir, unlink, writeFile } from 'node:fs/promises'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
|
|
5
|
-
import { checkCitationSupersetAcrossShards, summarizeMissingCitations } from './citation-superset'
|
|
6
|
-
import { normalizeCitation, parseCitations } from './citations'
|
|
7
|
-
import { clearDreamedIds, loadDreamingState, saveDreamingState } from './dreaming-state'
|
|
8
|
-
import { renderShard, type ShardFrontmatter } from './frontmatter'
|
|
9
|
-
import {
|
|
10
|
-
migratingTmpDir,
|
|
11
|
-
PRE_SHARD_BACKUP_FILENAME,
|
|
12
|
-
preShardBackupPath,
|
|
13
|
-
streamFilePath,
|
|
14
|
-
streamsDir,
|
|
15
|
-
topicsDir,
|
|
16
|
-
} from './paths'
|
|
17
|
-
import { headingToSlug } from './slug'
|
|
18
|
-
import { newEventId, type StreamEvent, streamEventSchema, timestampFromId } from './stream-events'
|
|
19
|
-
import { writeEventsAtomic as defaultWriteEventsAtomic } from './stream-io'
|
|
20
|
-
import { parseTopicsWithBodies } from './topics'
|
|
21
|
-
|
|
22
|
-
export type MigrationResult = {
|
|
23
|
-
migrated: string[]
|
|
24
|
-
skipped: string[]
|
|
25
|
-
legacyProseCount: number
|
|
26
|
-
fragmentCount: number
|
|
27
|
-
watermarkCount: number
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export type MigrationLogger = {
|
|
31
|
-
info: (message: string) => void
|
|
32
|
-
warn: (message: string) => void
|
|
33
|
-
error: (message: string) => void
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type MigrationGit = {
|
|
37
|
-
spawn?: (args: string[], options: { cwd: string }) => Promise<{ exitCode: number; stdout: string; stderr: string }>
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export type RunMigrationOptions = {
|
|
41
|
-
agentDir: string
|
|
42
|
-
logger: MigrationLogger
|
|
43
|
-
git?: MigrationGit
|
|
44
|
-
writeEventsAtomic?: (path: string, events: readonly StreamEvent[]) => Promise<void>
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export type ShardingMigrationResult = {
|
|
48
|
-
migrated: boolean
|
|
49
|
-
topicCount: number
|
|
50
|
-
streamCount: number
|
|
51
|
-
legacy: MigrationResult
|
|
52
|
-
error?: string
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export type RunShardingMigrationOptions = RunMigrationOptions & {
|
|
56
|
-
hooks?: {
|
|
57
|
-
onAfterStageTopics?: () => Promise<void> | void
|
|
58
|
-
onAfterStageStreams?: () => Promise<void> | void
|
|
59
|
-
onAfterStageBackup?: () => Promise<void> | void
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const DAILY_MD_NAME = /^(\d{4}-\d{2}-\d{2})\.md$/
|
|
64
|
-
const DAILY_JSONL_NAME = /^(\d{4}-\d{2}-\d{2})\.jsonl$/
|
|
65
|
-
const LEGACY_FRAGMENT_RE =
|
|
66
|
-
/<!-- fragment source=(\S+) entry=(\S+) -->\n## (.+)\n([\s\S]*?)(?=<!-- fragment |<!-- watermark |$)/g
|
|
67
|
-
const LEGACY_WATERMARK_RE = /<!-- watermark source=(\S+) entry=(\S+) -->/g
|
|
68
|
-
|
|
69
|
-
export async function runShardingMigration(options: RunShardingMigrationOptions): Promise<ShardingMigrationResult> {
|
|
70
|
-
await recoverShardingMigration(options.agentDir, options.logger)
|
|
71
|
-
const legacy = await runMigration(options)
|
|
72
|
-
const empty = (extra?: Partial<ShardingMigrationResult>): ShardingMigrationResult => ({
|
|
73
|
-
migrated: false,
|
|
74
|
-
topicCount: 0,
|
|
75
|
-
streamCount: 0,
|
|
76
|
-
legacy,
|
|
77
|
-
...extra,
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
await recoverShardingOrphans(options.agentDir, options.logger, options.git)
|
|
81
|
-
|
|
82
|
-
if (existsSync(topicsDir(options.agentDir)) || !existsSync(rootMemoryPath(options.agentDir))) {
|
|
83
|
-
return empty()
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const memoryDir = join(options.agentDir, 'memory')
|
|
87
|
-
const tmpDir = migratingTmpDir(options.agentDir)
|
|
88
|
-
await rm(tmpDir, { recursive: true, force: true })
|
|
89
|
-
await mkdir(join(tmpDir, 'topics'), { recursive: true })
|
|
90
|
-
await mkdir(join(tmpDir, 'streams'), { recursive: true })
|
|
91
|
-
|
|
92
|
-
const rootContent = await readFile(rootMemoryPath(options.agentDir), 'utf8')
|
|
93
|
-
const topics = parseTopicsWithBodies(rootContent)
|
|
94
|
-
if (topics.length === 0) {
|
|
95
|
-
await rm(tmpDir, { recursive: true, force: true })
|
|
96
|
-
options.logger.warn('[memory:migration] MEMORY.md has no topics; skipping sharding migration')
|
|
97
|
-
return empty()
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const existingSlugs = new Set<string>()
|
|
101
|
-
const orderedSlugs: string[] = []
|
|
102
|
-
for (const topic of topics) {
|
|
103
|
-
const slug = headingToSlug(topic.heading, existingSlugs)
|
|
104
|
-
existingSlugs.add(slug)
|
|
105
|
-
orderedSlugs.push(slug)
|
|
106
|
-
const body = normalizeCitation(topic.body)
|
|
107
|
-
const frontmatter = frontmatterForTopic(topic.heading, body)
|
|
108
|
-
await writeFile(join(tmpDir, 'topics', `${slug}.md`), renderShard(frontmatter, body), 'utf8')
|
|
109
|
-
}
|
|
110
|
-
await options.hooks?.onAfterStageTopics?.()
|
|
111
|
-
|
|
112
|
-
const streamDates = await collectFlatJsonlDates(memoryDir)
|
|
113
|
-
for (const date of streamDates) {
|
|
114
|
-
await cp(join(memoryDir, `${date}.jsonl`), join(tmpDir, 'streams', `${date}.jsonl`))
|
|
115
|
-
}
|
|
116
|
-
await options.hooks?.onAfterStageStreams?.()
|
|
117
|
-
|
|
118
|
-
await cp(rootMemoryPath(options.agentDir), join(tmpDir, 'MEMORY.md.pre-shard.bak'))
|
|
119
|
-
await options.hooks?.onAfterStageBackup?.()
|
|
120
|
-
|
|
121
|
-
const newShardTexts = await readShardTexts(join(tmpDir, 'topics'))
|
|
122
|
-
const verdict = checkCitationSupersetAcrossShards(new Map([['MEMORY.md', rootContent]]), newShardTexts)
|
|
123
|
-
if (!verdict.ok) {
|
|
124
|
-
const error = `citation superset violation: ${summarizeMissingCitations(verdict.missing)}`
|
|
125
|
-
options.logger.error(`[memory:migration] ${error}`)
|
|
126
|
-
return empty({ error })
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const finalized = await finalizeShardingMigration(options.agentDir, streamDates, options.logger)
|
|
130
|
-
if (!finalized.ok) return empty({ error: finalized.error })
|
|
131
|
-
|
|
132
|
-
await commitShardingMigration(
|
|
133
|
-
options.agentDir,
|
|
134
|
-
{ slugs: orderedSlugs, streamDates, hadRootMemory: true },
|
|
135
|
-
options.logger,
|
|
136
|
-
options.git,
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
options.logger.info(
|
|
140
|
-
`[memory:migration] sharded MEMORY.md into ${topics.length} topic shard(s) and ${streamDates.length} stream file(s)`,
|
|
141
|
-
)
|
|
142
|
-
return { migrated: true, topicCount: topics.length, streamCount: streamDates.length, legacy }
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export async function runMigration(options: RunMigrationOptions): Promise<MigrationResult> {
|
|
146
|
-
const memoryDir = join(options.agentDir, 'memory')
|
|
147
|
-
const result: MigrationResult = {
|
|
148
|
-
migrated: [],
|
|
149
|
-
skipped: [],
|
|
150
|
-
legacyProseCount: 0,
|
|
151
|
-
fragmentCount: 0,
|
|
152
|
-
watermarkCount: 0,
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
let entries: string[]
|
|
156
|
-
try {
|
|
157
|
-
entries = await readdir(memoryDir)
|
|
158
|
-
} catch {
|
|
159
|
-
return result
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const dates = collectDailyDates(entries)
|
|
163
|
-
for (const date of dates) {
|
|
164
|
-
const mdPath = join(memoryDir, `${date}.md`)
|
|
165
|
-
const jsonlPath = join(memoryDir, `${date}.jsonl`)
|
|
166
|
-
const hasMd = existsSync(mdPath)
|
|
167
|
-
const hasJsonl = existsSync(jsonlPath)
|
|
168
|
-
|
|
169
|
-
if (hasJsonl && !hasMd) {
|
|
170
|
-
result.skipped.push(date)
|
|
171
|
-
continue
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (hasJsonl && hasMd) {
|
|
175
|
-
options.logger.warn(`[memory:migration] ${date}: skipped because both .md and .jsonl exist`)
|
|
176
|
-
result.skipped.push(date)
|
|
177
|
-
continue
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (!hasMd) continue
|
|
181
|
-
|
|
182
|
-
const content = await readFile(mdPath, 'utf8')
|
|
183
|
-
const events = parseLegacyMarkdown(content)
|
|
184
|
-
const invalid = findInvalidEvent(events)
|
|
185
|
-
if (invalid !== null) {
|
|
186
|
-
options.logger.error(
|
|
187
|
-
`[memory:migration] ${date}.md: event ${invalid.index + 1} failed validation: ${invalid.reason}`,
|
|
188
|
-
)
|
|
189
|
-
result.skipped.push(date)
|
|
190
|
-
continue
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const counts = countEvents(events)
|
|
194
|
-
try {
|
|
195
|
-
await (options.writeEventsAtomic ?? defaultWriteEventsAtomic)(jsonlPath, events)
|
|
196
|
-
} catch (err) {
|
|
197
|
-
options.logger.error(`[memory:migration] ${date}.md: failed to write JSONL: ${describeError(err)}`)
|
|
198
|
-
result.skipped.push(date)
|
|
199
|
-
continue
|
|
200
|
-
}
|
|
201
|
-
await unlink(mdPath)
|
|
202
|
-
|
|
203
|
-
result.fragmentCount += counts.fragmentCount
|
|
204
|
-
result.watermarkCount += counts.watermarkCount
|
|
205
|
-
result.legacyProseCount += counts.legacyProseCount
|
|
206
|
-
result.migrated.push(date)
|
|
207
|
-
options.logger.info(
|
|
208
|
-
`[memory:migration] ${date}: ${counts.fragmentCount} fragments, ${counts.watermarkCount} watermarks, ${counts.legacyProseCount} legacy_prose regions`,
|
|
209
|
-
)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (result.migrated.length > 0) {
|
|
213
|
-
await resetDreamingWatermarks(options.agentDir, result.migrated)
|
|
214
|
-
await commitMigration(options.agentDir, result.migrated, options.logger, options.git)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return result
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function collectDailyDates(entries: readonly string[]): string[] {
|
|
221
|
-
const dates = new Set<string>()
|
|
222
|
-
for (const entry of entries) {
|
|
223
|
-
const md = DAILY_MD_NAME.exec(entry)
|
|
224
|
-
if (md?.[1] !== undefined) dates.add(md[1])
|
|
225
|
-
const jsonl = DAILY_JSONL_NAME.exec(entry)
|
|
226
|
-
if (jsonl?.[1] !== undefined) dates.add(jsonl[1])
|
|
227
|
-
}
|
|
228
|
-
return Array.from(dates).sort()
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function recoverShardingMigration(agentDir: string, logger: MigrationLogger): Promise<void> {
|
|
232
|
-
const tmpDir = migratingTmpDir(agentDir)
|
|
233
|
-
if (!existsSync(tmpDir)) return
|
|
234
|
-
|
|
235
|
-
const hasTopics = existsSync(topicsDir(agentDir))
|
|
236
|
-
await rm(tmpDir, { recursive: true, force: true })
|
|
237
|
-
logger.info(
|
|
238
|
-
hasTopics
|
|
239
|
-
? '[memory:migration] removed leftover sharding tmpdir after completed migration'
|
|
240
|
-
: '[memory:migration] removed stale sharding tmpdir; retrying migration from originals',
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async function recoverShardingOrphans(
|
|
245
|
-
agentDir: string,
|
|
246
|
-
logger: MigrationLogger,
|
|
247
|
-
git: MigrationGit | undefined,
|
|
248
|
-
): Promise<void> {
|
|
249
|
-
if (existsSync(topicsDir(agentDir))) {
|
|
250
|
-
let cleaned = false
|
|
251
|
-
const memoryPath = rootMemoryPath(agentDir)
|
|
252
|
-
if (existsSync(memoryPath)) {
|
|
253
|
-
await unlink(memoryPath)
|
|
254
|
-
cleaned = true
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const memoryDir = join(agentDir, 'memory')
|
|
258
|
-
const dates = await collectFlatJsonlDates(memoryDir)
|
|
259
|
-
for (const date of dates) {
|
|
260
|
-
if (!existsSync(streamFilePath(agentDir, date))) continue
|
|
261
|
-
await unlink(join(memoryDir, `${date}.jsonl`))
|
|
262
|
-
cleaned = true
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (cleaned) logger.info('[memory:migration] cleaned orphaned pre-shard memory files')
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Always called, even when nothing was cleaned this boot AND even when the
|
|
269
|
-
// sharded layout never landed on this agent: pre-#315 migrations and
|
|
270
|
-
// earlier runs of this function unlinked without committing, leaving
|
|
271
|
-
// staged deletions that survive across reboots until cleared explicitly.
|
|
272
|
-
// The earlier guard (`return` when topicsDir is absent) stranded any agent
|
|
273
|
-
// whose pre-shard files were deleted but whose sharding never completed —
|
|
274
|
-
// their staged deletions sat in the index forever.
|
|
275
|
-
await commitPendingLegacyDeletions(agentDir, logger, git)
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
async function collectFlatJsonlDates(memoryDir: string): Promise<string[]> {
|
|
279
|
-
let entries: string[]
|
|
280
|
-
try {
|
|
281
|
-
entries = await readdir(memoryDir)
|
|
282
|
-
} catch {
|
|
283
|
-
return []
|
|
284
|
-
}
|
|
285
|
-
return entries
|
|
286
|
-
.map((entry) => DAILY_JSONL_NAME.exec(entry)?.[1])
|
|
287
|
-
.filter((date): date is string => date !== undefined)
|
|
288
|
-
.sort()
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function frontmatterForTopic(heading: string, body: string): ShardFrontmatter {
|
|
292
|
-
const citations = parseCitations(body)
|
|
293
|
-
const dates = [...citations.keys()].sort()
|
|
294
|
-
let cites = 0
|
|
295
|
-
for (const ids of citations.values()) cites += ids.size
|
|
296
|
-
|
|
297
|
-
return {
|
|
298
|
-
heading,
|
|
299
|
-
cites,
|
|
300
|
-
days: dates.length,
|
|
301
|
-
lastReinforced: dates.at(-1) ?? todayDate(),
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async function readShardTexts(dir: string): Promise<Map<string, string>> {
|
|
306
|
-
const entries = (await readdir(dir)).filter((entry) => entry.endsWith('.md')).sort()
|
|
307
|
-
const out = new Map<string, string>()
|
|
308
|
-
for (const entry of entries) {
|
|
309
|
-
out.set(entry, await readFile(join(dir, entry), 'utf8'))
|
|
310
|
-
}
|
|
311
|
-
return out
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
async function finalizeShardingMigration(
|
|
315
|
-
agentDir: string,
|
|
316
|
-
streamDates: readonly string[],
|
|
317
|
-
logger: MigrationLogger,
|
|
318
|
-
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
319
|
-
const tmpDir = migratingTmpDir(agentDir)
|
|
320
|
-
const renames: Array<[string, string]> = [
|
|
321
|
-
[join(tmpDir, 'topics'), topicsDir(agentDir)],
|
|
322
|
-
[join(tmpDir, 'streams'), streamsDir(agentDir)],
|
|
323
|
-
[join(tmpDir, 'MEMORY.md.pre-shard.bak'), preShardBackupPath(agentDir)],
|
|
324
|
-
]
|
|
325
|
-
|
|
326
|
-
for (const [from, to] of renames) {
|
|
327
|
-
try {
|
|
328
|
-
await rename(from, to)
|
|
329
|
-
} catch (err) {
|
|
330
|
-
const error = `failed to finalize sharding migration: ${describeError(err)}`
|
|
331
|
-
logger.error(`[memory:migration] ${error}`)
|
|
332
|
-
return { ok: false, error }
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
for (const date of streamDates) {
|
|
337
|
-
try {
|
|
338
|
-
await unlink(join(agentDir, 'memory', `${date}.jsonl`))
|
|
339
|
-
} catch (err) {
|
|
340
|
-
logger.warn(`[memory:migration] failed to remove flat stream ${date}.jsonl: ${describeError(err)}`)
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
await unlink(rootMemoryPath(agentDir))
|
|
346
|
-
} catch (err) {
|
|
347
|
-
logger.warn(`[memory:migration] failed to remove root MEMORY.md: ${describeError(err)}`)
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
try {
|
|
351
|
-
await rmdir(tmpDir)
|
|
352
|
-
} catch (err) {
|
|
353
|
-
logger.warn(`[memory:migration] failed to remove sharding tmpdir: ${describeError(err)}`)
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return { ok: true }
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function rootMemoryPath(agentDir: string): string {
|
|
360
|
-
return join(agentDir, 'MEMORY.md')
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function todayDate(): string {
|
|
364
|
-
return new Date().toISOString().slice(0, 10)
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function parseLegacyMarkdown(content: string): StreamEvent[] {
|
|
368
|
-
const events: StreamEvent[] = []
|
|
369
|
-
let cursor = 0
|
|
370
|
-
|
|
371
|
-
while (cursor < content.length) {
|
|
372
|
-
const fragment = nextMatch(LEGACY_FRAGMENT_RE, content, cursor)
|
|
373
|
-
const watermark = nextMatch(LEGACY_WATERMARK_RE, content, cursor)
|
|
374
|
-
const next = earliest(fragment, watermark)
|
|
375
|
-
if (next === null) break
|
|
376
|
-
|
|
377
|
-
addLegacyProse(events, content.slice(cursor, next.match.index))
|
|
378
|
-
if (next.kind === 'fragment') {
|
|
379
|
-
const id = newEventId()
|
|
380
|
-
events.push({
|
|
381
|
-
type: 'fragment',
|
|
382
|
-
id,
|
|
383
|
-
ts: timestampFromId(id),
|
|
384
|
-
source: next.match[1]!,
|
|
385
|
-
entry: next.match[2]!,
|
|
386
|
-
topic: next.match[3]!,
|
|
387
|
-
body: next.match[4]!,
|
|
388
|
-
})
|
|
389
|
-
} else {
|
|
390
|
-
const id = newEventId()
|
|
391
|
-
events.push({
|
|
392
|
-
type: 'watermark',
|
|
393
|
-
id,
|
|
394
|
-
ts: timestampFromId(id),
|
|
395
|
-
source: next.match[1]!,
|
|
396
|
-
entry: next.match[2]!,
|
|
397
|
-
})
|
|
398
|
-
}
|
|
399
|
-
cursor = next.match.index + next.match[0].length
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
addLegacyProse(events, content.slice(cursor))
|
|
403
|
-
return events
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function addLegacyProse(events: StreamEvent[], text: string): void {
|
|
407
|
-
if (text.trim() === '') return
|
|
408
|
-
events.push({ type: 'legacy_prose', ts: new Date().toISOString(), text, origin: 'migration' })
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function nextMatch(regex: RegExp, content: string, cursor: number): RegExpExecArray | null {
|
|
412
|
-
regex.lastIndex = cursor
|
|
413
|
-
return regex.exec(content)
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function earliest(
|
|
417
|
-
fragment: RegExpExecArray | null,
|
|
418
|
-
watermark: RegExpExecArray | null,
|
|
419
|
-
): { kind: 'fragment' | 'watermark'; match: RegExpExecArray } | null {
|
|
420
|
-
if (fragment === null && watermark === null) return null
|
|
421
|
-
if (fragment === null) return { kind: 'watermark', match: watermark! }
|
|
422
|
-
if (watermark === null) return { kind: 'fragment', match: fragment }
|
|
423
|
-
return fragment.index <= watermark.index
|
|
424
|
-
? { kind: 'fragment', match: fragment }
|
|
425
|
-
: { kind: 'watermark', match: watermark }
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function findInvalidEvent(events: readonly StreamEvent[]): { index: number; reason: string } | null {
|
|
429
|
-
for (let i = 0; i < events.length; i++) {
|
|
430
|
-
const parsed = streamEventSchema.safeParse(events[i])
|
|
431
|
-
if (!parsed.success) {
|
|
432
|
-
return { index: i, reason: parsed.error.issues.map((issue) => issue.message).join('; ') }
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
return null
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function countEvents(
|
|
439
|
-
events: readonly StreamEvent[],
|
|
440
|
-
): Pick<MigrationResult, 'fragmentCount' | 'watermarkCount' | 'legacyProseCount'> {
|
|
441
|
-
let fragmentCount = 0
|
|
442
|
-
let watermarkCount = 0
|
|
443
|
-
let legacyProseCount = 0
|
|
444
|
-
for (const event of events) {
|
|
445
|
-
if (event.type === 'fragment') fragmentCount++
|
|
446
|
-
if (event.type === 'watermark') watermarkCount++
|
|
447
|
-
if (event.type === 'legacy_prose') legacyProseCount++
|
|
448
|
-
}
|
|
449
|
-
return { fragmentCount, watermarkCount, legacyProseCount }
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
async function resetDreamingWatermarks(agentDir: string, dates: readonly string[]): Promise<void> {
|
|
453
|
-
let state = await loadDreamingState(agentDir)
|
|
454
|
-
const ts = new Date().toISOString()
|
|
455
|
-
for (const date of dates) {
|
|
456
|
-
state = clearDreamedIds(state, date, ts)
|
|
457
|
-
}
|
|
458
|
-
await saveDreamingState(agentDir, state)
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
async function commitMigration(
|
|
462
|
-
agentDir: string,
|
|
463
|
-
dates: readonly string[],
|
|
464
|
-
logger: MigrationLogger,
|
|
465
|
-
git: MigrationGit | undefined,
|
|
466
|
-
): Promise<void> {
|
|
467
|
-
const spawn = git?.spawn ?? spawnGit
|
|
468
|
-
const inside = await spawn(['rev-parse', '--is-inside-work-tree'], { cwd: agentDir })
|
|
469
|
-
if (inside.exitCode !== 0) {
|
|
470
|
-
logger.info('[memory:migration] not in a git repo; skipping git commit')
|
|
471
|
-
return
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const jsonlPaths = dates.map((date) => `memory/${date}.jsonl`)
|
|
475
|
-
const addJsonl = await spawn(['add', '--', ...jsonlPaths], { cwd: agentDir })
|
|
476
|
-
if (addJsonl.exitCode !== 0) {
|
|
477
|
-
logger.warn(`[memory:migration] git add failed: ${addJsonl.stderr || addJsonl.stdout}`.trim())
|
|
478
|
-
return
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
for (const date of dates) {
|
|
482
|
-
const mdPath = `memory/${date}.md`
|
|
483
|
-
const tracked = await spawn(['ls-files', '--error-unmatch', '--', mdPath], { cwd: agentDir })
|
|
484
|
-
if (tracked.exitCode !== 0) continue
|
|
485
|
-
const addDeletedMd = await spawn(['add', '-u', '--', mdPath], { cwd: agentDir })
|
|
486
|
-
if (addDeletedMd.exitCode !== 0) {
|
|
487
|
-
logger.warn(`[memory:migration] git add failed: ${addDeletedMd.stderr || addDeletedMd.stdout}`.trim())
|
|
488
|
-
return
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const commit = await spawn(
|
|
493
|
-
['commit', '-m', `memory: migrate ${dates.length} daily stream(s) to JSONL`, '--no-edit'],
|
|
494
|
-
{
|
|
495
|
-
cwd: agentDir,
|
|
496
|
-
},
|
|
497
|
-
)
|
|
498
|
-
if (commit.exitCode !== 0) {
|
|
499
|
-
logger.warn(`[memory:migration] git commit failed: ${commit.stderr || commit.stdout}`.trim())
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
async function commitShardingMigration(
|
|
504
|
-
agentDir: string,
|
|
505
|
-
details: { slugs: readonly string[]; streamDates: readonly string[]; hadRootMemory: boolean },
|
|
506
|
-
logger: MigrationLogger,
|
|
507
|
-
git: MigrationGit | undefined,
|
|
508
|
-
): Promise<void> {
|
|
509
|
-
const spawn = git?.spawn ?? spawnGit
|
|
510
|
-
const inside = await spawn(['rev-parse', '--is-inside-work-tree'], { cwd: agentDir })
|
|
511
|
-
if (inside.exitCode !== 0) {
|
|
512
|
-
logger.info('[memory:migration] not in a git repo; skipping git commit')
|
|
513
|
-
return
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const newPaths = [
|
|
517
|
-
...details.slugs.map((slug) => `memory/topics/${slug}.md`),
|
|
518
|
-
...details.streamDates.map((date) => `memory/streams/${date}.jsonl`),
|
|
519
|
-
`memory/${PRE_SHARD_BACKUP_FILENAME}`,
|
|
520
|
-
]
|
|
521
|
-
const addNew = await spawn(['add', '--', ...newPaths], { cwd: agentDir })
|
|
522
|
-
if (addNew.exitCode !== 0) {
|
|
523
|
-
logger.warn(`[memory:migration] git add failed: ${addNew.stderr || addNew.stdout}`.trim())
|
|
524
|
-
return
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
const candidateDeletions = [...details.streamDates.map((date) => `memory/${date}.jsonl`)]
|
|
528
|
-
if (details.hadRootMemory) candidateDeletions.push('MEMORY.md')
|
|
529
|
-
const trackedDeletions: string[] = []
|
|
530
|
-
for (const path of candidateDeletions) {
|
|
531
|
-
const tracked = await spawn(['ls-files', '--error-unmatch', '--', path], { cwd: agentDir })
|
|
532
|
-
if (tracked.exitCode === 0) trackedDeletions.push(path)
|
|
533
|
-
}
|
|
534
|
-
if (trackedDeletions.length > 0) {
|
|
535
|
-
const addDeletions = await spawn(['add', '-u', '--', ...trackedDeletions], { cwd: agentDir })
|
|
536
|
-
if (addDeletions.exitCode !== 0) {
|
|
537
|
-
logger.warn(`[memory:migration] git add failed: ${addDeletions.stderr || addDeletions.stdout}`.trim())
|
|
538
|
-
return
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const commitSharding = await spawn(
|
|
543
|
-
[
|
|
544
|
-
'commit',
|
|
545
|
-
'-m',
|
|
546
|
-
`memory: shard MEMORY.md into ${details.slugs.length} topic(s) and ${details.streamDates.length} daily stream(s)`,
|
|
547
|
-
'--no-edit',
|
|
548
|
-
],
|
|
549
|
-
{ cwd: agentDir },
|
|
550
|
-
)
|
|
551
|
-
if (commitSharding.exitCode !== 0) {
|
|
552
|
-
logger.warn(`[memory:migration] git commit failed: ${commitSharding.stderr || commitSharding.stdout}`.trim())
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
async function commitPendingLegacyDeletions(
|
|
557
|
-
agentDir: string,
|
|
558
|
-
logger: MigrationLogger,
|
|
559
|
-
git: MigrationGit | undefined,
|
|
560
|
-
): Promise<void> {
|
|
561
|
-
const spawn = git?.spawn ?? spawnGit
|
|
562
|
-
const inside = await spawn(['rev-parse', '--is-inside-work-tree'], { cwd: agentDir })
|
|
563
|
-
if (inside.exitCode !== 0) return
|
|
564
|
-
|
|
565
|
-
const pending = await collectLegacyDeletions(agentDir, spawn)
|
|
566
|
-
if (pending.all.length === 0) return
|
|
567
|
-
|
|
568
|
-
// `git add -u` errors with "pathspec did not match" on paths whose deletion
|
|
569
|
-
// is already in the index, so stage only the working-tree-only deletions.
|
|
570
|
-
// The already-staged set is picked up by the commit directly.
|
|
571
|
-
if (pending.workingTreeOnly.length > 0) {
|
|
572
|
-
const addDeletions = await spawn(['add', '-u', '--', ...pending.workingTreeOnly], { cwd: agentDir })
|
|
573
|
-
if (addDeletions.exitCode !== 0) {
|
|
574
|
-
logger.warn(`[memory:migration] git add failed: ${addDeletions.stderr || addDeletions.stdout}`.trim())
|
|
575
|
-
return
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
const commit = await spawn(
|
|
580
|
-
[
|
|
581
|
-
'commit',
|
|
582
|
-
'-m',
|
|
583
|
-
`memory: clean up ${pending.all.length} pre-shard file(s) orphaned by earlier migration`,
|
|
584
|
-
'--no-edit',
|
|
585
|
-
],
|
|
586
|
-
{ cwd: agentDir },
|
|
587
|
-
)
|
|
588
|
-
if (commit.exitCode !== 0) {
|
|
589
|
-
logger.warn(`[memory:migration] git commit failed: ${commit.stderr || commit.stdout}`.trim())
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
async function collectLegacyDeletions(
|
|
594
|
-
agentDir: string,
|
|
595
|
-
spawn: NonNullable<MigrationGit['spawn']>,
|
|
596
|
-
): Promise<{ all: string[]; workingTreeOnly: string[] }> {
|
|
597
|
-
const isLegacy = (line: string): boolean => line === 'MEMORY.md' || /^memory\/\d{4}-\d{2}-\d{2}\.jsonl$/.test(line)
|
|
598
|
-
const parse = (out: string): string[] =>
|
|
599
|
-
out
|
|
600
|
-
.split('\n')
|
|
601
|
-
.map((line) => line.trim())
|
|
602
|
-
.filter(isLegacy)
|
|
603
|
-
|
|
604
|
-
const allDiff = await spawn(['diff', 'HEAD', '--name-only', '--diff-filter=D', '--', 'memory/', 'MEMORY.md'], {
|
|
605
|
-
cwd: agentDir,
|
|
606
|
-
})
|
|
607
|
-
if (allDiff.exitCode !== 0) return { all: [], workingTreeOnly: [] }
|
|
608
|
-
const all = parse(allDiff.stdout)
|
|
609
|
-
if (all.length === 0) return { all: [], workingTreeOnly: [] }
|
|
610
|
-
|
|
611
|
-
const wtDiff = await spawn(['diff', '--name-only', '--diff-filter=D', '--', 'memory/', 'MEMORY.md'], {
|
|
612
|
-
cwd: agentDir,
|
|
613
|
-
})
|
|
614
|
-
const workingTreeOnly = wtDiff.exitCode === 0 ? parse(wtDiff.stdout) : []
|
|
615
|
-
return { all, workingTreeOnly }
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
async function spawnGit(
|
|
619
|
-
args: string[],
|
|
620
|
-
options: { cwd: string },
|
|
621
|
-
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
622
|
-
const proc = Bun.spawn({ cmd: ['git', ...args], cwd: options.cwd, stdout: 'pipe', stderr: 'pipe' })
|
|
623
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
624
|
-
new Response(proc.stdout).text(),
|
|
625
|
-
new Response(proc.stderr).text(),
|
|
626
|
-
proc.exited,
|
|
627
|
-
])
|
|
628
|
-
return { exitCode, stdout, stderr }
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
function describeError(err: unknown): string {
|
|
632
|
-
return err instanceof Error ? err.message : String(err)
|
|
633
|
-
}
|