freddie 0.0.116 → 0.0.117

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freddie",
3
- "version": "0.0.116",
3
+ "version": "0.0.117",
4
4
  "type": "module",
5
5
  "description": "Open JS agent harness built on pi-mono, floosie, xstate, and anentrypoint-design",
6
6
  "bin": {
@@ -62,9 +62,15 @@ export default {
62
62
  let model = opts.model || undefined
63
63
  if (!provider && model && /^[a-z][a-z0-9-]*\//.test(model)) { provider = model.split('/')[0]; model = model.slice(provider.length + 1) }
64
64
  const out = await runTurn({ prompt: opts.prompt, provider, model, skill: opts.skill || undefined, cwd: opts.cwd || undefined, timeoutMs: Number(opts.timeout), witnessPath: opts.witness || undefined })
65
- if (out.error) { console.error('error:', out.error); process.exit(1) }
66
- console.log(out.result || out.messages?.at(-1)?.content || '')
67
- process.exit(0)
65
+ console.log(out.error ? '' : (out.result || out.messages?.at(-1)?.content || ''))
66
+ if (out.error) console.error('error:', out.error)
67
+ // Tear down cleanly instead of process.exit(): force-closing the
68
+ // process while undici keep-alive sockets and a pending setImmediate
69
+ // are still live makes libuv double-close its async handle and assert
70
+ // UV_HANDLE_CLOSING on Windows. Close the HTTP dispatcher's sockets,
71
+ // then set exitCode and let the event loop drain on its own.
72
+ try { const u = await import('undici'); await u.getGlobalDispatcher()?.close?.() } catch {}
73
+ process.exitCode = out.error ? 1 : 0
68
74
  } })
69
75
  C({ name: 'cron', description: 'Manage cron jobs', args: [{ name: 'action', default: 'list' }, { name: 'a1' }, { name: 'a2' }], action: async (action, a1, a2) => {
70
76
  const { listJobs, createJob, cancelJob, deleteJob, tick } = await import('../../src/cron/scheduler.js')
@@ -2,6 +2,32 @@ import { logger } from '../observability/log.js'
2
2
 
3
3
  const log = logger('acptoapi')
4
4
 
5
+ // acptoapi may take minutes to answer when it serially walks dead providers
6
+ // (each ~90s timeout) before a live one responds. Node's default undici
7
+ // headersTimeout/bodyTimeout (~300s) would throw UND_ERR_HEADERS_TIMEOUT
8
+ // mid-walk, so we raise them to 0 (disabled) on the global dispatcher and let
9
+ // our AbortController be the single source of truth for the overall deadline
10
+ // (default 240s, override via FREDDIE_LLM_TIMEOUT_MS). Use setGlobalDispatcher
11
+ // once rather than a per-request Agent so we don't leak a dispatcher per call.
12
+ const ACPTOAPI_TIMEOUT_MS = Number(process.env.FREDDIE_LLM_TIMEOUT_MS) || 240000
13
+ let _dispatcherSet = false
14
+ async function ensureLongTimeoutDispatcher() {
15
+ if (_dispatcherSet) return
16
+ _dispatcherSet = true
17
+ try {
18
+ const undici = await import('undici')
19
+ // headersTimeout/bodyTimeout 0: tolerate acptoapi's minutes-long walk.
20
+ // keepAlive*Timeout 1ms: close the socket right after the response so no
21
+ // keep-alive socket lingers between calls.
22
+ undici.setGlobalDispatcher(new undici.Agent({
23
+ headersTimeout: 0,
24
+ bodyTimeout: 0,
25
+ keepAliveTimeout: 1,
26
+ keepAliveMaxTimeout: 1,
27
+ }))
28
+ } catch { /* undici not available — rely on AbortController + defaults */ }
29
+ }
30
+
5
31
  export function getAcptoapiUrl() {
6
32
  return process.env.FREDDIE_LLM_URL || 'http://127.0.0.1:4800/v1'
7
33
  }
@@ -36,8 +62,9 @@ export async function callLLM({ messages, tools = [], model } = {}) {
36
62
  // Network Access headers so cross-origin loopback (gh-pages → localhost)
37
63
  // succeeds when acptoapi is running. The earlier preemptive loopback
38
64
  // refusal caused false negatives on reachable endpoints.
65
+ await ensureLongTimeoutDispatcher()
39
66
  const _ac = new AbortController()
40
- const _tid = setTimeout(() => _ac.abort(new Error('acptoapi fetch timeout')), 60000)
67
+ const _tid = setTimeout(() => _ac.abort(new Error('acptoapi fetch timeout')), ACPTOAPI_TIMEOUT_MS)
41
68
  let res
42
69
  try {
43
70
  res = await fetch(base.replace(/\/$/, '') + '/chat/completions', {
@@ -172,16 +172,22 @@ export async function runTurn({ prompt, messages = [], model, provider, callLLM,
172
172
  const machine = createAgentMachine({ model, provider, callLLM, enabledToolsets, disabledToolsets, maxIterations, events })
173
173
  const actor = createActor(machine, { input: { messages: initMessages } }); actor.start(); actor.send({ type: 'SUBMIT', prompt })
174
174
  return await new Promise((resolve, reject) => {
175
- const t = setTimeout(() => { try { actor.stop() } catch {} reject(new Error('agent turn timeout')) }, timeoutMs)
176
- actor.subscribe(snap => { if (snap.status !== 'done') return; clearTimeout(t)
175
+ let sub
176
+ const cleanup = () => { try { sub?.unsubscribe() } catch {} try { actor.stop() } catch {} }
177
+ const t = setTimeout(() => { cleanup(); reject(new Error('agent turn timeout')) }, timeoutMs)
178
+ sub = actor.subscribe(snap => { if (snap.status !== 'done') return; clearTimeout(t)
177
179
  ;(async () => {
178
180
  const out = snap.output
179
181
  const outbound = await h.hooks.invoke('onMessageOutbound', { content: out?.result || '' })
180
182
  if (outbound?.systemMessage || outbound?.additionalContext) out.messages = mergeHookExtras(out.messages || [], outbound, 'onMessageOutbound')
181
183
  await h.hooks.invoke('onSessionEnd', { reason: out?.error ? 'error' : 'ok', iterations: out?.iterations })
182
184
  const errorStack = out?.error ? (events.find(e => e.type === 'llm_call' && !e.ok)?.stack || null) : null
183
- await writeTrajectory(out, { prompt, provider, model, skill, cwd, events, errorStack, witnessPath }); resolve(out)
184
- })().catch(reject)
185
+ await writeTrajectory(out, { prompt, provider, model, skill, cwd, events, errorStack, witnessPath })
186
+ // Unsubscribe + stop the actor once the turn is done — a finished
187
+ // actor should not be left running with live subscriptions/handles.
188
+ cleanup()
189
+ resolve(out)
190
+ })().catch(e => { cleanup(); reject(e) })
185
191
  })
186
192
  })
187
193
  }