cross-agent-teams-mcp 0.2.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/LICENSE +21 -0
- package/README.md +296 -0
- package/README.zh-CN.md +306 -0
- package/dist/channel-cli.d.ts +18 -0
- package/dist/channel-cli.js +358 -0
- package/dist/channel-cli.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +4585 -0
- package/dist/cli.js.map +1 -0
- package/package.json +62 -0
- package/src/channel/auto-daemon.ts +130 -0
- package/src/channel/daemon-client.ts +155 -0
- package/src/channel/proxy.ts +28 -0
- package/src/channel-cli.ts +122 -0
- package/src/cli.ts +136 -0
- package/src/daemon/auth.ts +17 -0
- package/src/daemon/channel-wake-fanout.ts +39 -0
- package/src/daemon/channel-wake-send.ts +38 -0
- package/src/daemon/cleanup.ts +38 -0
- package/src/daemon/errors.ts +18 -0
- package/src/daemon/pid.ts +33 -0
- package/src/daemon/port.ts +16 -0
- package/src/daemon/runtime-identity.ts +238 -0
- package/src/daemon/server.ts +64 -0
- package/src/daemon/shutdown.ts +12 -0
- package/src/daemon/sse-fanout.ts +96 -0
- package/src/daemon/tmux-cli.ts +61 -0
- package/src/daemon/tmux-pane-detect.ts +276 -0
- package/src/lib/client-kind.ts +1 -0
- package/src/lib/default-team.ts +18 -0
- package/src/lib/delivery-spec.ts +172 -0
- package/src/lib/schema-diff.ts +79 -0
- package/src/mcp/agent-public-row.ts +52 -0
- package/src/mcp/auto-bind-channel.ts +106 -0
- package/src/mcp/auto-bind-codex-pane.ts +170 -0
- package/src/mcp/auto-poke-fanout.ts +129 -0
- package/src/mcp/bind-channel.ts +39 -0
- package/src/mcp/bind-runtime-identity.ts +43 -0
- package/src/mcp/broadcast-to-role.ts +127 -0
- package/src/mcp/broadcast.ts +115 -0
- package/src/mcp/codex-appserver-dispatch.ts +169 -0
- package/src/mcp/codex-appserver-rpc.ts +227 -0
- package/src/mcp/codex-pane-pre-register-repo.ts +57 -0
- package/src/mcp/delivery-status.ts +114 -0
- package/src/mcp/diff-contracts.ts +25 -0
- package/src/mcp/echo.ts +8 -0
- package/src/mcp/fanout-with-retry.ts +56 -0
- package/src/mcp/get-contract.ts +24 -0
- package/src/mcp/get-inbox.ts +57 -0
- package/src/mcp/identity.ts +8 -0
- package/src/mcp/pending-contract-events.ts +36 -0
- package/src/mcp/poke-guard.ts +32 -0
- package/src/mcp/poke-retry.ts +159 -0
- package/src/mcp/poke.ts +190 -0
- package/src/mcp/pre-register-codex-pane.ts +65 -0
- package/src/mcp/register-agent.ts +84 -0
- package/src/mcp/register-codex-self.ts +276 -0
- package/src/mcp/register-contract.ts +60 -0
- package/src/mcp/send-message.ts +159 -0
- package/src/mcp/subscribe-channel-wake.ts +31 -0
- package/src/mcp/subscribe-contract.ts +24 -0
- package/src/mcp/task-add.ts +37 -0
- package/src/mcp/task-claim.ts +54 -0
- package/src/mcp/task-complete.ts +36 -0
- package/src/mcp/task-list.ts +33 -0
- package/src/mcp/tools.ts +1240 -0
- package/src/mcp/transport-dispatch.ts +171 -0
- package/src/mcp/transport.ts +204 -0
- package/src/mcp/unregister-self.ts +46 -0
- package/src/storage/agents-repo.ts +328 -0
- package/src/storage/db.ts +13 -0
- package/src/storage/events-outbox.ts +44 -0
- package/src/storage/schema.ts +180 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import {
|
|
4
|
+
parseDeliveryRow,
|
|
5
|
+
serializeDelivery,
|
|
6
|
+
type DeliverySpec,
|
|
7
|
+
type DeliveryRow,
|
|
8
|
+
} from '../lib/delivery-spec.js'
|
|
9
|
+
import type { ClientKind } from '../lib/client-kind.js'
|
|
10
|
+
|
|
11
|
+
export interface RegisterInput {
|
|
12
|
+
client?: ClientKind
|
|
13
|
+
client_name?: string
|
|
14
|
+
model: string
|
|
15
|
+
name: string
|
|
16
|
+
role?: string
|
|
17
|
+
team?: string
|
|
18
|
+
tmux_pane_id?: string
|
|
19
|
+
delivery?: DeliverySpec
|
|
20
|
+
claude_ui_pid?: number
|
|
21
|
+
runtime_ui_pid?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AgentRow {
|
|
25
|
+
agent_id: string
|
|
26
|
+
client: ClientKind | null
|
|
27
|
+
client_name: string | null
|
|
28
|
+
team: string
|
|
29
|
+
role: string
|
|
30
|
+
name: string
|
|
31
|
+
model: string | null
|
|
32
|
+
tmux_pane_id: string | null
|
|
33
|
+
delivery: DeliverySpec
|
|
34
|
+
channel_session_id: string | null
|
|
35
|
+
last_seen_at: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AgentListRow extends AgentRow {
|
|
39
|
+
online: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const ONLINE_MS = 5 * 60 * 1000
|
|
43
|
+
|
|
44
|
+
type DbAgentRow = {
|
|
45
|
+
agent_id: string
|
|
46
|
+
client: ClientKind | null
|
|
47
|
+
client_name: string | null
|
|
48
|
+
team: string
|
|
49
|
+
role: string
|
|
50
|
+
name: string
|
|
51
|
+
model: string | null
|
|
52
|
+
tmux_pane_id: string | null
|
|
53
|
+
last_seen_at: string
|
|
54
|
+
} & DeliveryRow
|
|
55
|
+
|
|
56
|
+
function toAgentRow(row: DbAgentRow): AgentRow {
|
|
57
|
+
const delivery = parseDeliveryRow(row)
|
|
58
|
+
return {
|
|
59
|
+
agent_id: row.agent_id,
|
|
60
|
+
client: row.client,
|
|
61
|
+
client_name: row.client_name,
|
|
62
|
+
team: row.team,
|
|
63
|
+
role: row.role,
|
|
64
|
+
name: row.name,
|
|
65
|
+
model: row.model,
|
|
66
|
+
tmux_pane_id: row.tmux_pane_id,
|
|
67
|
+
delivery,
|
|
68
|
+
channel_session_id:
|
|
69
|
+
delivery.kind === 'claude-channel' ? delivery.channel_session_id : null,
|
|
70
|
+
last_seen_at: row.last_seen_at,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class AgentsRepo {
|
|
75
|
+
constructor(private db: Database.Database) {}
|
|
76
|
+
|
|
77
|
+
findByIdentity(args: { team: string; name: string }): { agent_id: string } | undefined {
|
|
78
|
+
return this.db.prepare(
|
|
79
|
+
`SELECT agent_id FROM agents WHERE team=? AND name=?`
|
|
80
|
+
).get(args.team, args.name) as { agent_id: string } | undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
register(input: RegisterInput): {
|
|
84
|
+
agent_id: string
|
|
85
|
+
team: string
|
|
86
|
+
} {
|
|
87
|
+
const team = input.team ?? 'default'
|
|
88
|
+
const role = input.role ?? 'default'
|
|
89
|
+
const name = input.name
|
|
90
|
+
const now = new Date().toISOString()
|
|
91
|
+
const newId = randomUUID()
|
|
92
|
+
const delivery = input.delivery ?? { kind: 'none' }
|
|
93
|
+
const serialized = serializeDelivery(delivery)
|
|
94
|
+
const preserveExistingDelivery = input.delivery === undefined ? 1 : 0
|
|
95
|
+
const tx = this.db.transaction(() => {
|
|
96
|
+
this.writeAgentRow({
|
|
97
|
+
newId,
|
|
98
|
+
input,
|
|
99
|
+
team,
|
|
100
|
+
role,
|
|
101
|
+
name,
|
|
102
|
+
now,
|
|
103
|
+
serialized,
|
|
104
|
+
preserveExistingDelivery,
|
|
105
|
+
})
|
|
106
|
+
const rebindCsid =
|
|
107
|
+
role === '__channel_proxy__' &&
|
|
108
|
+
input.claude_ui_pid !== undefined &&
|
|
109
|
+
delivery.kind === 'claude-channel'
|
|
110
|
+
? delivery.channel_session_id
|
|
111
|
+
: undefined
|
|
112
|
+
if (rebindCsid !== undefined) {
|
|
113
|
+
this.reactiveRebindHosts({
|
|
114
|
+
team,
|
|
115
|
+
claude_ui_pid: input.claude_ui_pid!,
|
|
116
|
+
new_csid: rebindCsid,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
tx()
|
|
121
|
+
const row = this.db.prepare(`SELECT agent_id FROM agents WHERE team=? AND name=?`).get(team, name) as { agent_id: string }
|
|
122
|
+
return { agent_id: row.agent_id, team }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private writeAgentRow(args: {
|
|
126
|
+
newId: string
|
|
127
|
+
input: RegisterInput
|
|
128
|
+
team: string
|
|
129
|
+
role: string
|
|
130
|
+
name: string
|
|
131
|
+
now: string
|
|
132
|
+
serialized: ReturnType<typeof serializeDelivery>
|
|
133
|
+
preserveExistingDelivery: number
|
|
134
|
+
}): void {
|
|
135
|
+
const { newId, input, team, role, name, now, serialized, preserveExistingDelivery } = args
|
|
136
|
+
this.db.prepare(
|
|
137
|
+
`INSERT INTO agents (
|
|
138
|
+
agent_id, client, client_name, team, role, name, model, registered_at, last_seen_at,
|
|
139
|
+
tmux_pane_id, claude_ui_pid, runtime_ui_pid, delivery_kind, delivery_payload
|
|
140
|
+
)
|
|
141
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
142
|
+
ON CONFLICT (team, name) DO UPDATE SET
|
|
143
|
+
client = excluded.client,
|
|
144
|
+
client_name = excluded.client_name,
|
|
145
|
+
role = excluded.role,
|
|
146
|
+
model = excluded.model,
|
|
147
|
+
last_seen_at = excluded.last_seen_at,
|
|
148
|
+
tmux_pane_id = COALESCE(excluded.tmux_pane_id, tmux_pane_id),
|
|
149
|
+
claude_ui_pid = COALESCE(excluded.claude_ui_pid, claude_ui_pid),
|
|
150
|
+
runtime_ui_pid = COALESCE(excluded.runtime_ui_pid, runtime_ui_pid),
|
|
151
|
+
delivery_kind = CASE
|
|
152
|
+
WHEN ? THEN delivery_kind
|
|
153
|
+
ELSE excluded.delivery_kind
|
|
154
|
+
END,
|
|
155
|
+
delivery_payload = CASE
|
|
156
|
+
WHEN ? THEN delivery_payload
|
|
157
|
+
ELSE excluded.delivery_payload
|
|
158
|
+
END`
|
|
159
|
+
).run(
|
|
160
|
+
newId,
|
|
161
|
+
input.client ?? null,
|
|
162
|
+
input.client_name ?? null,
|
|
163
|
+
team,
|
|
164
|
+
role,
|
|
165
|
+
name,
|
|
166
|
+
input.model,
|
|
167
|
+
now,
|
|
168
|
+
now,
|
|
169
|
+
input.tmux_pane_id ?? null,
|
|
170
|
+
input.claude_ui_pid ?? null,
|
|
171
|
+
input.runtime_ui_pid ?? null,
|
|
172
|
+
serialized.delivery_kind,
|
|
173
|
+
serialized.delivery_payload,
|
|
174
|
+
preserveExistingDelivery,
|
|
175
|
+
preserveExistingDelivery,
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private reactiveRebindHosts(args: {
|
|
180
|
+
team: string
|
|
181
|
+
claude_ui_pid: number
|
|
182
|
+
new_csid: string
|
|
183
|
+
}): void {
|
|
184
|
+
this.db.prepare(
|
|
185
|
+
`UPDATE agents
|
|
186
|
+
SET delivery_kind = 'claude-channel',
|
|
187
|
+
delivery_payload = json_object('channel_session_id', ?)
|
|
188
|
+
WHERE role != '__channel_proxy__'
|
|
189
|
+
AND runtime_ui_pid IS NOT NULL
|
|
190
|
+
AND runtime_ui_pid = ?
|
|
191
|
+
AND team = ?
|
|
192
|
+
AND (
|
|
193
|
+
delivery_kind = 'none'
|
|
194
|
+
OR (delivery_kind = 'claude-channel'
|
|
195
|
+
AND json_extract(delivery_payload,'$.channel_session_id') != ?)
|
|
196
|
+
)`
|
|
197
|
+
).run(args.new_csid, args.claude_ui_pid, args.team, args.new_csid)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
setDelivery(agent_id: string, spec: DeliverySpec): void {
|
|
201
|
+
const serialized = serializeDelivery(spec)
|
|
202
|
+
this.db.prepare(
|
|
203
|
+
`UPDATE agents
|
|
204
|
+
SET delivery_kind=?, delivery_payload=?
|
|
205
|
+
WHERE agent_id=?`
|
|
206
|
+
).run(serialized.delivery_kind, serialized.delivery_payload, agent_id)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
setClient(agent_id: string, client: ClientKind, client_name?: string | null): void {
|
|
210
|
+
this.db.prepare(
|
|
211
|
+
`UPDATE agents
|
|
212
|
+
SET client=?,
|
|
213
|
+
client_name=?
|
|
214
|
+
WHERE agent_id=?`
|
|
215
|
+
).run(client, client_name ?? null, agent_id)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
setRuntimeBinding(
|
|
219
|
+
agent_id: string,
|
|
220
|
+
args: {
|
|
221
|
+
tmux_pane_id: string
|
|
222
|
+
runtime_ui_pid: number | null
|
|
223
|
+
runtime_tty: string
|
|
224
|
+
runtime_verification_mode: string
|
|
225
|
+
runtime_bound_at?: string
|
|
226
|
+
}
|
|
227
|
+
): void {
|
|
228
|
+
this.db.prepare(
|
|
229
|
+
`UPDATE agents
|
|
230
|
+
SET tmux_pane_id=?,
|
|
231
|
+
runtime_ui_pid=?,
|
|
232
|
+
runtime_tty=?,
|
|
233
|
+
runtime_verification_mode=?,
|
|
234
|
+
runtime_bound_at=?
|
|
235
|
+
WHERE agent_id=?`
|
|
236
|
+
).run(
|
|
237
|
+
args.tmux_pane_id,
|
|
238
|
+
args.runtime_ui_pid,
|
|
239
|
+
args.runtime_tty,
|
|
240
|
+
args.runtime_verification_mode,
|
|
241
|
+
args.runtime_bound_at ?? new Date().toISOString(),
|
|
242
|
+
agent_id
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
list(args: { team: string }): AgentListRow[] {
|
|
247
|
+
const rows = this.db.prepare(
|
|
248
|
+
`SELECT
|
|
249
|
+
agent_id,
|
|
250
|
+
client,
|
|
251
|
+
client_name,
|
|
252
|
+
team,
|
|
253
|
+
role,
|
|
254
|
+
name,
|
|
255
|
+
model,
|
|
256
|
+
tmux_pane_id,
|
|
257
|
+
delivery_kind,
|
|
258
|
+
delivery_payload,
|
|
259
|
+
last_seen_at
|
|
260
|
+
FROM agents
|
|
261
|
+
WHERE team=?
|
|
262
|
+
ORDER BY registered_at ASC`
|
|
263
|
+
).all(args.team) as DbAgentRow[]
|
|
264
|
+
const nowMs = Date.now()
|
|
265
|
+
return rows.map((row) => {
|
|
266
|
+
const agent = toAgentRow(row)
|
|
267
|
+
return {
|
|
268
|
+
...agent,
|
|
269
|
+
online: nowMs - new Date(agent.last_seen_at).getTime() < ONLINE_MS,
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
touch(agent_id: string): void {
|
|
275
|
+
this.db.prepare(`UPDATE agents SET last_seen_at=? WHERE agent_id=?`).run(new Date().toISOString(), agent_id)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
listClaimedInProgressTaskIds(args: { agent_id: string; team: string }): string[] {
|
|
279
|
+
const rows = this.db.prepare(
|
|
280
|
+
`SELECT id
|
|
281
|
+
FROM tasks
|
|
282
|
+
WHERE team=? AND claimed_by=? AND status='in_progress'
|
|
283
|
+
ORDER BY id ASC`
|
|
284
|
+
).all(args.team, args.agent_id) as Array<{ id: string }>
|
|
285
|
+
return rows.map((row) => row.id)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
deleteContractSubscriptions(args: { agent_id: string; team: string }): number {
|
|
289
|
+
const result = this.db.prepare(
|
|
290
|
+
`DELETE FROM contract_subscriptions
|
|
291
|
+
WHERE agent_id=? AND team=?`
|
|
292
|
+
).run(args.agent_id, args.team)
|
|
293
|
+
return result.changes
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
deleteById(agent_id: string): boolean {
|
|
297
|
+
const result = this.db.prepare(
|
|
298
|
+
`DELETE FROM agents
|
|
299
|
+
WHERE agent_id=?`
|
|
300
|
+
).run(agent_id)
|
|
301
|
+
return result.changes === 1
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
getById(agent_id: string): AgentRow | undefined {
|
|
305
|
+
const row = this.db.prepare(
|
|
306
|
+
`SELECT
|
|
307
|
+
agent_id,
|
|
308
|
+
client,
|
|
309
|
+
client_name,
|
|
310
|
+
team,
|
|
311
|
+
role,
|
|
312
|
+
name,
|
|
313
|
+
model,
|
|
314
|
+
tmux_pane_id,
|
|
315
|
+
delivery_kind,
|
|
316
|
+
delivery_payload,
|
|
317
|
+
last_seen_at
|
|
318
|
+
FROM agents
|
|
319
|
+
WHERE agent_id=?`
|
|
320
|
+
).get(agent_id) as DbAgentRow | undefined
|
|
321
|
+
if (!row) return undefined
|
|
322
|
+
return toAgentRow(row)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
findById(agent_id: string): AgentRow | undefined {
|
|
326
|
+
return this.getById(agent_id)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Database from 'better-sqlite3'
|
|
2
|
+
import { mkdirSync } from 'node:fs'
|
|
3
|
+
import { dirname } from 'node:path'
|
|
4
|
+
|
|
5
|
+
export function openDb(path: string): Database.Database {
|
|
6
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
7
|
+
const db = new Database(path)
|
|
8
|
+
db.pragma('journal_mode = WAL')
|
|
9
|
+
db.pragma('busy_timeout = 5000')
|
|
10
|
+
db.pragma('synchronous = NORMAL')
|
|
11
|
+
db.pragma('foreign_keys = ON')
|
|
12
|
+
return db
|
|
13
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3'
|
|
2
|
+
|
|
3
|
+
export interface EventRow {
|
|
4
|
+
event_id: number
|
|
5
|
+
from_team: string
|
|
6
|
+
to_team: string
|
|
7
|
+
event_type: string
|
|
8
|
+
actor_agent_id: string | null
|
|
9
|
+
payload: string
|
|
10
|
+
created_at: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class EventsOutbox {
|
|
14
|
+
constructor(private db: Database.Database) {}
|
|
15
|
+
|
|
16
|
+
append(args: {
|
|
17
|
+
from_team: string
|
|
18
|
+
to_team: string
|
|
19
|
+
event_type: string
|
|
20
|
+
actor_agent_id?: string | null
|
|
21
|
+
payload: unknown
|
|
22
|
+
}): number {
|
|
23
|
+
const stmt = this.db.prepare(
|
|
24
|
+
`INSERT INTO events (from_team, to_team, event_type, actor_agent_id, payload, created_at)
|
|
25
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
26
|
+
)
|
|
27
|
+
const info = stmt.run(
|
|
28
|
+
args.from_team,
|
|
29
|
+
args.to_team,
|
|
30
|
+
args.event_type,
|
|
31
|
+
args.actor_agent_id ?? null,
|
|
32
|
+
JSON.stringify(args.payload),
|
|
33
|
+
new Date().toISOString()
|
|
34
|
+
)
|
|
35
|
+
return Number(info.lastInsertRowid)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
since(args: { team: string; since_event_id: number; limit?: number }): EventRow[] {
|
|
39
|
+
const limit = Math.min(args.limit ?? 100, 500)
|
|
40
|
+
return this.db.prepare(
|
|
41
|
+
`SELECT * FROM events WHERE to_team = ? AND event_id > ? ORDER BY event_id ASC LIMIT ?`
|
|
42
|
+
).all(args.team, args.since_event_id, limit) as EventRow[]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3'
|
|
2
|
+
|
|
3
|
+
const DDL = [
|
|
4
|
+
`CREATE TABLE IF NOT EXISTS events (
|
|
5
|
+
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6
|
+
from_team TEXT NOT NULL,
|
|
7
|
+
to_team TEXT NOT NULL,
|
|
8
|
+
event_type TEXT NOT NULL,
|
|
9
|
+
actor_agent_id TEXT,
|
|
10
|
+
payload TEXT NOT NULL,
|
|
11
|
+
created_at TEXT NOT NULL
|
|
12
|
+
)`,
|
|
13
|
+
`CREATE INDEX IF NOT EXISTS idx_events_from_team_eventid ON events(from_team, event_id)`,
|
|
14
|
+
`CREATE INDEX IF NOT EXISTS idx_events_to_team_eventid ON events(to_team, event_id)`,
|
|
15
|
+
`CREATE TABLE IF NOT EXISTS agents (
|
|
16
|
+
agent_id TEXT PRIMARY KEY,
|
|
17
|
+
client TEXT,
|
|
18
|
+
client_name TEXT,
|
|
19
|
+
team TEXT NOT NULL,
|
|
20
|
+
role TEXT NOT NULL,
|
|
21
|
+
name TEXT NOT NULL,
|
|
22
|
+
model TEXT,
|
|
23
|
+
registered_at TEXT NOT NULL,
|
|
24
|
+
last_seen_at TEXT NOT NULL,
|
|
25
|
+
last_processed_event_id INTEGER NOT NULL DEFAULT 0,
|
|
26
|
+
tmux_pane_id TEXT,
|
|
27
|
+
claude_ui_pid INTEGER,
|
|
28
|
+
runtime_ui_pid INTEGER,
|
|
29
|
+
runtime_tty TEXT,
|
|
30
|
+
runtime_verification_mode TEXT,
|
|
31
|
+
runtime_bound_at TEXT,
|
|
32
|
+
channel_session_id TEXT,
|
|
33
|
+
delivery_kind TEXT NOT NULL DEFAULT 'none',
|
|
34
|
+
delivery_payload TEXT
|
|
35
|
+
)`,
|
|
36
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS agents_identity_idx ON agents(team, name)`,
|
|
37
|
+
`CREATE TABLE IF NOT EXISTS messages (
|
|
38
|
+
id TEXT PRIMARY KEY,
|
|
39
|
+
event_id INTEGER NOT NULL REFERENCES events(event_id),
|
|
40
|
+
from_team TEXT NOT NULL,
|
|
41
|
+
to_team TEXT NOT NULL,
|
|
42
|
+
from_agent_id TEXT NOT NULL,
|
|
43
|
+
to_agent_id TEXT,
|
|
44
|
+
to_role TEXT,
|
|
45
|
+
subject TEXT,
|
|
46
|
+
body TEXT NOT NULL,
|
|
47
|
+
need_reply INTEGER NOT NULL DEFAULT 1,
|
|
48
|
+
sent_at TEXT NOT NULL
|
|
49
|
+
)`,
|
|
50
|
+
`CREATE TABLE IF NOT EXISTS message_delivery_status (
|
|
51
|
+
message_id TEXT NOT NULL,
|
|
52
|
+
agent_id TEXT NOT NULL,
|
|
53
|
+
wake_status TEXT NOT NULL CHECK(wake_status IN ('delivered','retrying','skipped','failed')),
|
|
54
|
+
skip_reason TEXT,
|
|
55
|
+
retry_attempts INTEGER NOT NULL DEFAULT 0,
|
|
56
|
+
updated_at TEXT NOT NULL,
|
|
57
|
+
delivered_at TEXT,
|
|
58
|
+
PRIMARY KEY (message_id, agent_id)
|
|
59
|
+
)`,
|
|
60
|
+
`CREATE INDEX IF NOT EXISTS idx_message_delivery_status_message ON message_delivery_status(message_id)`,
|
|
61
|
+
`CREATE TABLE IF NOT EXISTS tasks (
|
|
62
|
+
id TEXT PRIMARY KEY,
|
|
63
|
+
team TEXT NOT NULL,
|
|
64
|
+
title TEXT NOT NULL,
|
|
65
|
+
description TEXT,
|
|
66
|
+
status TEXT NOT NULL CHECK(status IN ('pending','in_progress','completed')),
|
|
67
|
+
depends_on TEXT NOT NULL,
|
|
68
|
+
claimed_by TEXT,
|
|
69
|
+
claimed_at TEXT,
|
|
70
|
+
completed_at TEXT,
|
|
71
|
+
result TEXT,
|
|
72
|
+
created_at TEXT NOT NULL
|
|
73
|
+
)`,
|
|
74
|
+
`CREATE TABLE IF NOT EXISTS contracts (
|
|
75
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
76
|
+
team TEXT NOT NULL,
|
|
77
|
+
name TEXT NOT NULL,
|
|
78
|
+
version INTEGER NOT NULL,
|
|
79
|
+
format TEXT NOT NULL CHECK(format='jsonschema'),
|
|
80
|
+
schema TEXT NOT NULL,
|
|
81
|
+
note TEXT,
|
|
82
|
+
registered_by TEXT NOT NULL,
|
|
83
|
+
registered_at TEXT NOT NULL,
|
|
84
|
+
UNIQUE(team, name, version)
|
|
85
|
+
)`,
|
|
86
|
+
`CREATE TABLE IF NOT EXISTS contract_subscriptions (
|
|
87
|
+
agent_id TEXT NOT NULL,
|
|
88
|
+
team TEXT NOT NULL,
|
|
89
|
+
contract_name TEXT NOT NULL,
|
|
90
|
+
subscribed_at TEXT NOT NULL,
|
|
91
|
+
PRIMARY KEY (agent_id, team, contract_name)
|
|
92
|
+
)`,
|
|
93
|
+
`CREATE TABLE IF NOT EXISTS codex_pane_pre_registrations (
|
|
94
|
+
pane_id TEXT PRIMARY KEY,
|
|
95
|
+
xats_agent_id TEXT NOT NULL,
|
|
96
|
+
expires_at TEXT NOT NULL
|
|
97
|
+
)`
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
function migrateAgentsDeliveryColumns(db: Database.Database): void {
|
|
101
|
+
const tableExists = db
|
|
102
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='agents'`)
|
|
103
|
+
.get() as { name: string } | undefined
|
|
104
|
+
if (!tableExists) return
|
|
105
|
+
const cols = db.pragma('table_info(agents)') as Array<{ name: string }>
|
|
106
|
+
const existing = new Set(cols.map(c => c.name))
|
|
107
|
+
const needClient = !existing.has('client')
|
|
108
|
+
const needClientName = !existing.has('client_name')
|
|
109
|
+
const needKind = !existing.has('delivery_kind')
|
|
110
|
+
const needPayload = !existing.has('delivery_payload')
|
|
111
|
+
const needRuntimeUiPid = !existing.has('runtime_ui_pid')
|
|
112
|
+
const needRuntimeTty = !existing.has('runtime_tty')
|
|
113
|
+
const needRuntimeVerificationMode = !existing.has('runtime_verification_mode')
|
|
114
|
+
const needRuntimeBoundAt = !existing.has('runtime_bound_at')
|
|
115
|
+
const needClaudeUiPid = !existing.has('claude_ui_pid')
|
|
116
|
+
if (
|
|
117
|
+
!needClient &&
|
|
118
|
+
!needClientName &&
|
|
119
|
+
!needKind &&
|
|
120
|
+
!needPayload &&
|
|
121
|
+
!needRuntimeUiPid &&
|
|
122
|
+
!needRuntimeTty &&
|
|
123
|
+
!needRuntimeVerificationMode &&
|
|
124
|
+
!needRuntimeBoundAt &&
|
|
125
|
+
!needClaudeUiPid
|
|
126
|
+
) return
|
|
127
|
+
const tx = db.transaction(() => {
|
|
128
|
+
if (needClient) {
|
|
129
|
+
db.exec(`ALTER TABLE agents ADD COLUMN client TEXT`)
|
|
130
|
+
}
|
|
131
|
+
if (needClientName) {
|
|
132
|
+
db.exec(`ALTER TABLE agents ADD COLUMN client_name TEXT`)
|
|
133
|
+
}
|
|
134
|
+
if (needKind) {
|
|
135
|
+
db.exec(`ALTER TABLE agents ADD COLUMN delivery_kind TEXT NOT NULL DEFAULT 'none'`)
|
|
136
|
+
}
|
|
137
|
+
if (needPayload) {
|
|
138
|
+
db.exec(`ALTER TABLE agents ADD COLUMN delivery_payload TEXT`)
|
|
139
|
+
}
|
|
140
|
+
if (needRuntimeUiPid) {
|
|
141
|
+
db.exec(`ALTER TABLE agents ADD COLUMN runtime_ui_pid INTEGER`)
|
|
142
|
+
}
|
|
143
|
+
if (needRuntimeTty) {
|
|
144
|
+
db.exec(`ALTER TABLE agents ADD COLUMN runtime_tty TEXT`)
|
|
145
|
+
}
|
|
146
|
+
if (needRuntimeVerificationMode) {
|
|
147
|
+
db.exec(`ALTER TABLE agents ADD COLUMN runtime_verification_mode TEXT`)
|
|
148
|
+
}
|
|
149
|
+
if (needRuntimeBoundAt) {
|
|
150
|
+
db.exec(`ALTER TABLE agents ADD COLUMN runtime_bound_at TEXT`)
|
|
151
|
+
}
|
|
152
|
+
if (needClaudeUiPid) {
|
|
153
|
+
db.exec(`ALTER TABLE agents ADD COLUMN claude_ui_pid INTEGER`)
|
|
154
|
+
}
|
|
155
|
+
if (needKind || needPayload) {
|
|
156
|
+
db.exec(`UPDATE agents
|
|
157
|
+
SET delivery_kind = 'claude-channel',
|
|
158
|
+
delivery_payload = json_object('channel_session_id', channel_session_id)
|
|
159
|
+
WHERE channel_session_id IS NOT NULL AND delivery_kind = 'none'`)
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
tx()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function migrateMessagesNeedReplyColumn(db: Database.Database): void {
|
|
166
|
+
const tableExists = db
|
|
167
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='messages'`)
|
|
168
|
+
.get() as { name: string } | undefined
|
|
169
|
+
if (!tableExists) return
|
|
170
|
+
const cols = db.pragma('table_info(messages)') as Array<{ name: string }>
|
|
171
|
+
const existing = new Set(cols.map(c => c.name))
|
|
172
|
+
if (existing.has('need_reply')) return
|
|
173
|
+
db.exec(`ALTER TABLE messages ADD COLUMN need_reply INTEGER NOT NULL DEFAULT 1`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function applySchema(db: Database.Database): void {
|
|
177
|
+
for (const sql of DDL) db.exec(sql)
|
|
178
|
+
migrateAgentsDeliveryColumns(db)
|
|
179
|
+
migrateMessagesNeedReplyColumn(db)
|
|
180
|
+
}
|