zooid 0.7.2 → 0.7.4

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.
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <title>Zoon</title>
8
- <script type="module" crossorigin src="/assets/index-1pU3tgkr.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-BqOX0Pv4.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-C-ZtBp7U.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zooid",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Open-source tool for defining and running AI agents alongside you and your team. Any model, any CLI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -37,13 +37,13 @@
37
37
  "marked": "^18.0.4",
38
38
  "sanitize-html": "^2.17.4",
39
39
  "yaml": "^2.5.0",
40
- "@zooid/acp-client": "^0.7.2",
41
- "@zooid/context-mcp": "^0.7.2",
42
- "@zooid/runtime-docker": "^0.7.2",
43
- "@zooid/core": "^0.7.2",
44
- "@zooid/transport-http": "^0.7.2",
45
- "@zooid/runtime-local": "^0.7.2",
46
- "@zooid/transport-matrix": "^0.7.2"
40
+ "@zooid/context-mcp": "^0.7.4",
41
+ "@zooid/acp-client": "^0.7.4",
42
+ "@zooid/transport-http": "^0.7.4",
43
+ "@zooid/core": "^0.7.4",
44
+ "@zooid/runtime-docker": "^0.7.4",
45
+ "@zooid/runtime-local": "^0.7.4",
46
+ "@zooid/transport-matrix": "^0.7.4"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@agentclientprotocol/sdk": "^0.21.0",
@@ -161,7 +161,11 @@ export async function runDev(flags: DevFlags): Promise<DevHandle> {
161
161
  title: 'Wait for Tuwunel /_matrix/client/versions',
162
162
  task: async () => {
163
163
  if (!ctx.svc) throw new Error('service not started')
164
- await ctx.svc.waitHealthy({ url: homeserver, timeoutMs: 60_000 })
164
+ // 180s covers Docker Desktop's first-run bind-mount setup on macOS
165
+ // (which can sit in `created` for 30–90s before transitioning to
166
+ // `running`) plus Tuwunel's RocksDB init. Repeated runs against an
167
+ // existing DB are far quicker.
168
+ await ctx.svc.waitHealthy({ url: homeserver, timeoutMs: 180_000 })
165
169
  },
166
170
  },
167
171
  {
@@ -1,6 +1,10 @@
1
1
  import { spawn, type ChildProcess } from 'node:child_process'
2
+ import { execFile } from 'node:child_process'
3
+ import { promisify } from 'node:util'
2
4
  import type { Paths } from '../bootstrap/paths.js'
3
5
 
6
+ const execFileAsync = promisify(execFile)
7
+
4
8
  export interface TuwunelOpts {
5
9
  name: string
6
10
  image?: string
@@ -61,7 +65,32 @@ export class TuwunelService {
61
65
  }
62
66
 
63
67
  async waitHealthy(opts: { url: string; timeoutMs: number }): Promise<void> {
68
+ // Two-phase wait: first the container has to transition from `created` to
69
+ // `running` (Docker Desktop on macOS can take 30–90s on first-run bind-mount
70
+ // setup), then Tuwunel has to open its DB and start serving HTTP. Polling
71
+ // only HTTP loses both signals: a long create looks identical to a long
72
+ // boot, and a crashed container looks identical to a slow one.
64
73
  const deadline = Date.now() + opts.timeoutMs
74
+
75
+ // Phase 1: wait for the container to be running. Surface the actual
76
+ // docker error if it transitions to `exited` instead — that's the
77
+ // case where today we just hang for 60s and say "did not become healthy"
78
+ // with no clue what really broke.
79
+ while (Date.now() < deadline) {
80
+ const state = await this.inspectState().catch(() => null)
81
+ if (state?.status === 'running') break
82
+ if (state?.status === 'exited') {
83
+ throw new Error(
84
+ `Tuwunel container exited before serving HTTP ` +
85
+ `(exit=${state.exitCode}${state.error ? `, error=${state.error}` : ''}). ` +
86
+ `Check the engine logs (\`${this.opts.engine} logs ${this.opts.name}\`).`,
87
+ )
88
+ }
89
+ await new Promise((resolve) => setTimeout(resolve, 500))
90
+ }
91
+
92
+ // Phase 2: container is running (or we ran out of time). Poll the HTTP
93
+ // endpoint until either it answers or the deadline passes.
65
94
  while (Date.now() < deadline) {
66
95
  try {
67
96
  const r = await fetch(`${opts.url}/_matrix/client/versions`)
@@ -71,10 +100,48 @@ export class TuwunelService {
71
100
  }
72
101
  await new Promise((resolve) => setTimeout(resolve, 500))
73
102
  }
103
+
104
+ // Last-ditch diagnostic before throwing the generic timeout — surface the
105
+ // current container state so the user knows whether to blame slow start,
106
+ // slow boot, or a misconfiguration.
107
+ const finalState = await this.inspectState().catch(() => null)
108
+ const detail = finalState
109
+ ? ` (container status=${finalState.status}${
110
+ finalState.exitCode !== undefined ? `, exit=${finalState.exitCode}` : ''
111
+ })`
112
+ : ''
74
113
  throw new Error(
75
- `Tuwunel did not become healthy at ${opts.url} in ${opts.timeoutMs}ms`,
114
+ `Tuwunel did not become healthy at ${opts.url} in ${opts.timeoutMs}ms${detail}`,
76
115
  )
77
116
  }
117
+
118
+ /**
119
+ * Read the engine's view of the container. Returns null if the container
120
+ * isn't known to the engine (e.g. before spawn completed or after `--rm`).
121
+ */
122
+ private async inspectState(): Promise<{
123
+ status: string
124
+ exitCode?: number
125
+ error?: string
126
+ } | null> {
127
+ try {
128
+ const { stdout } = await execFileAsync(this.opts.engine, [
129
+ 'inspect',
130
+ this.opts.name,
131
+ '--format',
132
+ '{{.State.Status}}|{{.State.ExitCode}}|{{.State.Error}}',
133
+ ])
134
+ const [status, exitCodeRaw, error] = stdout.trim().split('|')
135
+ const exitCode = exitCodeRaw && exitCodeRaw !== '<no value>' ? Number(exitCodeRaw) : undefined
136
+ return {
137
+ status: status ?? 'unknown',
138
+ ...(Number.isFinite(exitCode) ? { exitCode: exitCode as number } : {}),
139
+ ...(error ? { error } : {}),
140
+ }
141
+ } catch {
142
+ return null
143
+ }
144
+ }
78
145
  }
79
146
 
80
147
  function execEngine(engine: string, args: string[]): Promise<void> {