@lifenrir/dsh-external-task-context-patch 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-zh.md +103 -0
- package/README.md +124 -2
- package/install.mjs +543 -0
- package/lib.mjs +274 -0
- package/package.json +33 -4
- package/src/index.ts +189 -0
- package/src/log.ts +235 -0
- package/src/run.ts +650 -0
package/src/log.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-run Claude Code execution logs: an append-only JSONL message stream and
|
|
3
|
+
* a rewritten metadata sidecar. Logging is best effort: every failure is
|
|
4
|
+
* reported to the supplied sink and never reaches the run.
|
|
5
|
+
*
|
|
6
|
+
* @module @deepseek-ai/dsh-subagent-claude-code/log
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
createWriteStream,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
rmSync,
|
|
14
|
+
statSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
type WriteStream,
|
|
17
|
+
} from 'node:fs'
|
|
18
|
+
import { homedir } from 'node:os'
|
|
19
|
+
import { join, resolve } from 'node:path'
|
|
20
|
+
import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk'
|
|
21
|
+
|
|
22
|
+
/** Default number of days a finished run log is retained. */
|
|
23
|
+
export const DEFAULT_LOG_RETENTION_DAYS = 7
|
|
24
|
+
|
|
25
|
+
/** Milliseconds in one day, used by retention cleanup. */
|
|
26
|
+
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1_000
|
|
27
|
+
|
|
28
|
+
/** Terminal status recorded for a finished run. */
|
|
29
|
+
export type RunLogStatus = 'completed' | 'failed' | 'cancelled'
|
|
30
|
+
|
|
31
|
+
/** Metadata sidecar status, which is `running` until the run terminates. */
|
|
32
|
+
type MetaStatus = 'running' | RunLogStatus
|
|
33
|
+
|
|
34
|
+
/** Facts needed to open one run's logs. */
|
|
35
|
+
export interface RunLogWriterOptions {
|
|
36
|
+
/** Directory that receives the JSONL and metadata files. */
|
|
37
|
+
readonly directory: string
|
|
38
|
+
/** Run identity, also the file stem used by both files. */
|
|
39
|
+
readonly runId: string
|
|
40
|
+
/** Child working directory recorded in the start line and metadata. */
|
|
41
|
+
readonly cwd: string
|
|
42
|
+
/** Selected native model, or null when native settings choose it. */
|
|
43
|
+
readonly model?: string | null
|
|
44
|
+
/** Run start time; defaults to the writer creation time. */
|
|
45
|
+
readonly startedAt?: number
|
|
46
|
+
/** Sink for swallowed logging failures. */
|
|
47
|
+
readonly onError?: (error: Error) => void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Append-only writer for one run's execution log. */
|
|
51
|
+
export interface RunLogWriter {
|
|
52
|
+
/** Append one raw SDK message. */
|
|
53
|
+
message(message: SDKMessage): void
|
|
54
|
+
/** Append the terminal record and rewrite the metadata sidecar. */
|
|
55
|
+
finish(status: RunLogStatus, result?: string | null): void
|
|
56
|
+
/** Stop accepting records and release the JSONL stream. */
|
|
57
|
+
close(): void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Shape of the rewritten metadata sidecar. */
|
|
61
|
+
interface MetaFile {
|
|
62
|
+
readonly runId: string
|
|
63
|
+
readonly cwd: string
|
|
64
|
+
readonly model: string | null
|
|
65
|
+
readonly startedAt: number
|
|
66
|
+
readonly endedAt?: number
|
|
67
|
+
readonly status: MetaStatus
|
|
68
|
+
readonly claudeSessionId?: string
|
|
69
|
+
readonly claudeConfigDir: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve the DeepSeek Harness home directory.
|
|
74
|
+
*
|
|
75
|
+
* Inlined from `@deepseek-ai/dsh-home-paths` so the patched bundle has no
|
|
76
|
+
* dependency on that package, which the official provider does not ship.
|
|
77
|
+
* @returns `$DSH_HOME` when set and non-empty, otherwise `<os home>/.dsh`.
|
|
78
|
+
*/
|
|
79
|
+
function resolveDshHome(): string {
|
|
80
|
+
const configured = process.env.DSH_HOME
|
|
81
|
+
return configured !== undefined && configured.trim().length > 0
|
|
82
|
+
? resolve(configured)
|
|
83
|
+
: join(homedir(), '.dsh')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Default log directory under the resolved DeepSeek Harness home.
|
|
88
|
+
* @returns `<DSH_HOME>/logs/subagent-claude-code`, resolved when called.
|
|
89
|
+
*/
|
|
90
|
+
export function defaultRunLogDirectory(): string {
|
|
91
|
+
return join(resolveDshHome(), 'logs', 'subagent-claude-code')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve Claude Code's native configuration root.
|
|
96
|
+
* @returns `$CLAUDE_CONFIG_DIR` when set and non-empty, otherwise `<os home>/.claude`.
|
|
97
|
+
*/
|
|
98
|
+
function resolveClaudeConfigDir(): string {
|
|
99
|
+
const configured = process.env.CLAUDE_CONFIG_DIR
|
|
100
|
+
return configured !== undefined && configured.trim().length > 0
|
|
101
|
+
? resolve(configured)
|
|
102
|
+
: join(homedir(), '.claude')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Whether a directory entry carries one of the two run-log file names. */
|
|
106
|
+
function isRunLogFile(name: string): boolean {
|
|
107
|
+
return name.endsWith('.jsonl') || name.endsWith('.meta.json')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Delete run-log files older than the retention window.
|
|
112
|
+
* @param directory - log directory to scan; absence and unreadable entries are ignored.
|
|
113
|
+
* @param retentionDays - maximum file age in days.
|
|
114
|
+
*/
|
|
115
|
+
export function cleanupExpiredRunLogs(directory: string, retentionDays: number): void {
|
|
116
|
+
const cutoff = Date.now() - retentionDays * MILLISECONDS_PER_DAY
|
|
117
|
+
let names: string[]
|
|
118
|
+
try {
|
|
119
|
+
names = readdirSync(directory)
|
|
120
|
+
} catch {
|
|
121
|
+
// A missing or unreadable directory leaves nothing to clean.
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
for (const name of names) {
|
|
125
|
+
if (!isRunLogFile(name)) continue
|
|
126
|
+
const path = join(directory, name)
|
|
127
|
+
try {
|
|
128
|
+
if (statSync(path).mtimeMs < cutoff) rmSync(path, { force: true })
|
|
129
|
+
} catch {
|
|
130
|
+
// A concurrent run may have removed or replaced the entry.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create the best-effort writer for one run.
|
|
137
|
+
* @param options - directory, run identity, and metadata facts.
|
|
138
|
+
* @returns a writer that swallows every logging failure through `options.onError`.
|
|
139
|
+
*/
|
|
140
|
+
export function createRunLogWriter(options: RunLogWriterOptions): RunLogWriter {
|
|
141
|
+
const report = (error: unknown): void => {
|
|
142
|
+
try {
|
|
143
|
+
options.onError?.(error instanceof Error ? error : new Error(String(error)))
|
|
144
|
+
} catch {
|
|
145
|
+
// The diagnostic sink cannot break the log writer.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const jsonlPath = join(options.directory, `${options.runId}.jsonl`)
|
|
150
|
+
const metaPath = join(options.directory, `${options.runId}.meta.json`)
|
|
151
|
+
const startedAt = options.startedAt ?? Date.now()
|
|
152
|
+
let sessionId: string | undefined
|
|
153
|
+
let status: MetaStatus = 'running'
|
|
154
|
+
let endedAt: number | undefined
|
|
155
|
+
let finished = false
|
|
156
|
+
let closed = false
|
|
157
|
+
|
|
158
|
+
const writeMeta = (): void => {
|
|
159
|
+
const meta: MetaFile = {
|
|
160
|
+
runId: options.runId,
|
|
161
|
+
cwd: options.cwd,
|
|
162
|
+
model: options.model ?? null,
|
|
163
|
+
startedAt,
|
|
164
|
+
status,
|
|
165
|
+
claudeConfigDir: resolveClaudeConfigDir(),
|
|
166
|
+
...endedAt === undefined ? {} : { endedAt },
|
|
167
|
+
...sessionId === undefined ? {} : { claudeSessionId: sessionId },
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
writeFileSync(metaPath, `${JSON.stringify(meta, null, 2)}\n`, {
|
|
171
|
+
encoding: 'utf8',
|
|
172
|
+
mode: 0o600,
|
|
173
|
+
})
|
|
174
|
+
} catch (error) {
|
|
175
|
+
report(error)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let stream: WriteStream
|
|
180
|
+
try {
|
|
181
|
+
mkdirSync(options.directory, { recursive: true, mode: 0o700 })
|
|
182
|
+
stream = createWriteStream(jsonlPath, { flags: 'a', mode: 0o600 })
|
|
183
|
+
stream.on('error', report)
|
|
184
|
+
} catch (error) {
|
|
185
|
+
report(error)
|
|
186
|
+
return { message() {}, finish() {}, close() {} }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const writeLine = (record: object): void => {
|
|
190
|
+
if (closed) return
|
|
191
|
+
try {
|
|
192
|
+
stream.write(`${JSON.stringify(record)}\n`)
|
|
193
|
+
} catch (error) {
|
|
194
|
+
report(error)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
writeLine({
|
|
199
|
+
kind: 'start',
|
|
200
|
+
time: startedAt,
|
|
201
|
+
runId: options.runId,
|
|
202
|
+
cwd: options.cwd,
|
|
203
|
+
model: options.model ?? null,
|
|
204
|
+
})
|
|
205
|
+
writeMeta()
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
message(message: SDKMessage): void {
|
|
209
|
+
if (finished || closed) return
|
|
210
|
+
const incoming = message.session_id
|
|
211
|
+
if (sessionId === undefined && typeof incoming === 'string' && incoming.length > 0) {
|
|
212
|
+
sessionId = incoming
|
|
213
|
+
writeMeta()
|
|
214
|
+
}
|
|
215
|
+
writeLine({ kind: 'message', time: Date.now(), message })
|
|
216
|
+
},
|
|
217
|
+
finish(next: RunLogStatus, result: string | null = null): void {
|
|
218
|
+
if (finished || closed) return
|
|
219
|
+
finished = true
|
|
220
|
+
status = next
|
|
221
|
+
endedAt = Date.now()
|
|
222
|
+
writeLine({ kind: 'end', time: endedAt, status: next, result })
|
|
223
|
+
writeMeta()
|
|
224
|
+
},
|
|
225
|
+
close(): void {
|
|
226
|
+
if (closed) return
|
|
227
|
+
closed = true
|
|
228
|
+
try {
|
|
229
|
+
stream.end()
|
|
230
|
+
} catch (error) {
|
|
231
|
+
report(error)
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
}
|