ocpipe 0.5.14 → 0.5.15

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 (2) hide show
  1. package/package.json +2 -2
  2. package/src/claude-code.ts +39 -39
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocpipe",
3
- "version": "0.5.14",
3
+ "version": "0.5.15",
4
4
  "description": "SDK for LLM pipelines with OpenCode and Zod",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -31,7 +31,7 @@
31
31
  "dependencies": {},
32
32
  "peerDependencies": {
33
33
  "zod": "4.3.6",
34
- "@anthropic-ai/claude-agent-sdk": "0.2.31"
34
+ "@anthropic-ai/claude-agent-sdk": "0.2.44"
35
35
  },
36
36
  "peerDependenciesMeta": {
37
37
  "@anthropic-ai/claude-agent-sdk": {
@@ -1,7 +1,13 @@
1
1
  /**
2
2
  * ocpipe Claude Code agent integration.
3
3
  *
4
- * Uses the Claude Agent SDK v2 for running LLM agents with session management.
4
+ * Uses the Claude Agent SDK v1 query() API for running LLM agents with session management.
5
+ * The v1 API properly supports session persistence — the subprocess exits naturally when
6
+ * the query completes, giving it time to save session data to disk. The v2 API's close()
7
+ * sends SIGTERM immediately, which kills the subprocess before it can persist.
8
+ *
9
+ * See: https://github.com/s4wave/ocpipe/issues/10
10
+ * See: https://github.com/anthropics/anthropic-sdk-typescript/issues/911
5
11
  */
6
12
 
7
13
  import { execSync } from 'child_process'
@@ -9,12 +15,11 @@ import { existsSync, readFileSync } from 'fs'
9
15
  import { join } from 'path'
10
16
  import { homedir } from 'os'
11
17
  import {
12
- unstable_v2_createSession,
13
- unstable_v2_resumeSession,
18
+ query,
14
19
  type HookCallback,
20
+ type Options,
15
21
  type PreToolUseHookInput,
16
22
  type SDKMessage,
17
- type SDKSessionOptions,
18
23
  } from '@anthropic-ai/claude-agent-sdk'
19
24
  import type { RunAgentOptions, RunAgentResult } from './types.js'
20
25
 
@@ -161,22 +166,35 @@ export async function runClaudeCodeAgent(
161
166
  const sessionInfo = sessionId ? `[session:${sessionId}]` : '[new session]'
162
167
  const promptPreview = prompt.slice(0, 50).replace(/\n/g, ' ')
163
168
 
164
- // Build session options with configurable permission mode (default: acceptEdits)
169
+ // Build query options with configurable permission mode (default: acceptEdits)
165
170
  const permissionMode = claudeCode?.permissionMode ?? 'acceptEdits'
166
171
 
167
172
  // Resolve system prompt: explicit option > agent definition file > none
168
173
  const systemPrompt = claudeCode?.systemPrompt ?? loadAgentDefinition(agent, workdir)
169
174
 
170
- const sessionOptions: SDKSessionOptions = {
175
+ // Bridge external abort signal to an AbortController for the SDK
176
+ const abortController = new AbortController()
177
+ if (signal) {
178
+ signal.addEventListener(
179
+ 'abort',
180
+ () => {
181
+ console.error(`\n[abort] Aborting Claude Code query...`)
182
+ abortController.abort()
183
+ },
184
+ { once: true },
185
+ )
186
+ }
187
+
188
+ const queryOptions: Options = {
171
189
  model: modelStr,
172
190
  permissionMode,
191
+ abortController,
192
+ // v1 persistSession defaults to true, but set explicitly for clarity
193
+ persistSession: true,
173
194
  ...(workdir && { cwd: workdir }),
174
195
  ...(systemPrompt && { systemPrompt }),
175
- // Enable session persistence so close+resume works.
176
- // The v2 SDK defaults persistSession to false, unlike v1 which defaults to true.
177
- // Without this, session.close() destroys the session and resumeSession() fails
178
- // with "No conversation found with session ID".
179
- ...({ persistSession: true }),
196
+ // Resume from previous session if sessionId provided
197
+ ...(sessionId && { resume: sessionId }),
180
198
  hooks: {
181
199
  PreToolUse: [{ hooks: [logToolCall] }],
182
200
  },
@@ -196,36 +214,22 @@ export async function runClaudeCodeAgent(
196
214
  `\n>>> Claude Code [${modelStr}] [${permissionMode}] ${sessionInfo}: ${promptPreview}...`,
197
215
  )
198
216
 
199
- // Create or resume session
200
- const session =
201
- sessionId ?
202
- unstable_v2_resumeSession(sessionId, sessionOptions)
203
- : unstable_v2_createSession(sessionOptions)
217
+ // v1 query() returns an AsyncGenerator — subprocess exits naturally when done,
218
+ // allowing session data to be persisted to disk before the process ends.
219
+ const q = query({ prompt, options: queryOptions })
204
220
 
205
- // Handle abort signal
206
- const abortHandler = () => {
207
- console.error(`\n[abort] Closing Claude Code session...`)
208
- session.close()
209
- }
210
- signal?.addEventListener('abort', abortHandler, { once: true })
211
-
212
- // Declare outside try block so finally can access it
213
221
  let timeoutId: ReturnType<typeof setTimeout> | null = null
214
222
 
215
223
  try {
216
- // Send the prompt
217
- await session.send(prompt)
218
-
219
- // Collect the response
220
224
  const textParts: string[] = []
221
225
  let newSessionId = sessionId || ''
222
226
 
223
- // Set up timeout (store ID so we can clear it later)
227
+ // Set up timeout
224
228
  const timeoutPromise =
225
229
  timeoutSec > 0 ?
226
230
  new Promise<never>((_, reject) => {
227
231
  timeoutId = setTimeout(() => {
228
- session.close()
232
+ q.close()
229
233
  reject(new Error(`Timeout after ${timeoutSec}s`))
230
234
  }, timeoutSec * 1000)
231
235
  })
@@ -237,18 +241,15 @@ export async function runClaudeCodeAgent(
237
241
  new Promise<never>((_, reject) => {
238
242
  signal.addEventListener(
239
243
  'abort',
240
- () => {
241
- reject(new Error('Request aborted'))
242
- },
244
+ () => reject(new Error('Request aborted')),
243
245
  { once: true },
244
246
  )
245
247
  })
246
248
  : null
247
249
 
248
- // Stream the response
250
+ // Stream the response — iterate the AsyncGenerator
249
251
  const streamPromise = (async () => {
250
- for await (const msg of session.stream()) {
251
- // Capture session ID from any message
252
+ for await (const msg of q) {
252
253
  if (msg.session_id) {
253
254
  newSessionId = msg.session_id
254
255
  }
@@ -268,7 +269,6 @@ export async function runClaudeCodeAgent(
268
269
  if (abortPromise) promises.push(abortPromise)
269
270
  await Promise.race(promises)
270
271
 
271
- // Clear the timeout to prevent it from keeping the event loop alive
272
272
  if (timeoutId) clearTimeout(timeoutId)
273
273
 
274
274
  const response = textParts.join('')
@@ -287,8 +287,8 @@ export async function runClaudeCodeAgent(
287
287
  sessionId: newSessionId,
288
288
  }
289
289
  } finally {
290
- signal?.removeEventListener('abort', abortHandler)
291
290
  if (timeoutId) clearTimeout(timeoutId)
292
- session.close()
291
+ // v1 subprocess exits naturally when the generator completes — no close() needed.
292
+ // close() is only called on timeout (above) to force-terminate.
293
293
  }
294
294
  }