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,169 @@
|
|
|
1
|
+
import type { DeliveryCodexAppserver } from '../lib/delivery-spec.js'
|
|
2
|
+
import {
|
|
3
|
+
JsonRpcSocketClient,
|
|
4
|
+
defaultWebSocketFactory,
|
|
5
|
+
describeError,
|
|
6
|
+
resolveAuthToken,
|
|
7
|
+
safeClose,
|
|
8
|
+
type CodexWebSocketFactory,
|
|
9
|
+
type JsonRpcResponse,
|
|
10
|
+
type WebSocketLike,
|
|
11
|
+
} from './codex-appserver-rpc.js'
|
|
12
|
+
export type { WebSocketLike } from './codex-appserver-rpc.js'
|
|
13
|
+
|
|
14
|
+
type Json = unknown
|
|
15
|
+
|
|
16
|
+
export interface CodexAppserverDispatchDeps {
|
|
17
|
+
env?: NodeJS.ProcessEnv
|
|
18
|
+
webSocketFactory?: CodexWebSocketFactory
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type CodexAppserverDispatchResult =
|
|
22
|
+
| {
|
|
23
|
+
ok: true
|
|
24
|
+
transport_used: 'codex-appserver'
|
|
25
|
+
thread_id: string
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
error:
|
|
29
|
+
| 'missing_auth_token'
|
|
30
|
+
| 'codex_connect_failed'
|
|
31
|
+
| 'codex_initialize_failed'
|
|
32
|
+
| 'codex_resume_failed'
|
|
33
|
+
| 'codex_turn_start_failed'
|
|
34
|
+
detail?: unknown
|
|
35
|
+
transport_used?: 'codex-appserver'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function requestStep(
|
|
39
|
+
client: JsonRpcSocketClient,
|
|
40
|
+
method: string,
|
|
41
|
+
params: Json
|
|
42
|
+
): Promise<
|
|
43
|
+
| { ok: JsonRpcResponse }
|
|
44
|
+
| {
|
|
45
|
+
error:
|
|
46
|
+
| 'codex_initialize_failed'
|
|
47
|
+
| 'codex_resume_failed'
|
|
48
|
+
| 'codex_turn_start_failed'
|
|
49
|
+
detail: unknown
|
|
50
|
+
}
|
|
51
|
+
> {
|
|
52
|
+
try {
|
|
53
|
+
const response = await client.request(method, params)
|
|
54
|
+
if (response.error) {
|
|
55
|
+
const mappedError =
|
|
56
|
+
method === 'initialize'
|
|
57
|
+
? 'codex_initialize_failed'
|
|
58
|
+
: method === 'thread/resume'
|
|
59
|
+
? 'codex_resume_failed'
|
|
60
|
+
: 'codex_turn_start_failed'
|
|
61
|
+
return { error: mappedError, detail: response.error }
|
|
62
|
+
}
|
|
63
|
+
return { ok: response }
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const mappedError =
|
|
66
|
+
method === 'initialize'
|
|
67
|
+
? 'codex_initialize_failed'
|
|
68
|
+
: method === 'thread/resume'
|
|
69
|
+
? 'codex_resume_failed'
|
|
70
|
+
: 'codex_turn_start_failed'
|
|
71
|
+
return { error: mappedError, detail: describeError(error) }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function dispatchCodexAppserverPoke(
|
|
76
|
+
input: {
|
|
77
|
+
delivery: DeliveryCodexAppserver
|
|
78
|
+
content: string
|
|
79
|
+
},
|
|
80
|
+
deps: CodexAppserverDispatchDeps = {}
|
|
81
|
+
): Promise<CodexAppserverDispatchResult> {
|
|
82
|
+
const authToken = resolveAuthToken(
|
|
83
|
+
input.delivery.auth_token_ref,
|
|
84
|
+
deps.env ?? process.env
|
|
85
|
+
)
|
|
86
|
+
if ('error' in authToken) return authToken
|
|
87
|
+
|
|
88
|
+
const headers = authToken.ok === undefined
|
|
89
|
+
? undefined
|
|
90
|
+
: { Authorization: `Bearer ${authToken.ok}` }
|
|
91
|
+
|
|
92
|
+
let ws: WebSocketLike
|
|
93
|
+
try {
|
|
94
|
+
ws = (deps.webSocketFactory ?? defaultWebSocketFactory)({
|
|
95
|
+
url: input.delivery.ws_url,
|
|
96
|
+
headers,
|
|
97
|
+
})
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return {
|
|
100
|
+
error: 'codex_connect_failed',
|
|
101
|
+
detail: describeError(error),
|
|
102
|
+
transport_used: 'codex-appserver',
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const client = new JsonRpcSocketClient(ws)
|
|
107
|
+
try {
|
|
108
|
+
await client.waitForOpen()
|
|
109
|
+
|
|
110
|
+
const init = await requestStep(client, 'initialize', {
|
|
111
|
+
clientInfo: {
|
|
112
|
+
name: 'cross-agent-teams-mcp',
|
|
113
|
+
title: null,
|
|
114
|
+
version: '0.1.0',
|
|
115
|
+
},
|
|
116
|
+
capabilities: {
|
|
117
|
+
experimentalApi: true,
|
|
118
|
+
optOutNotificationMethods: null,
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
if ('error' in init) {
|
|
122
|
+
return {
|
|
123
|
+
error: init.error,
|
|
124
|
+
detail: init.detail,
|
|
125
|
+
transport_used: 'codex-appserver',
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
client.notify('initialized')
|
|
130
|
+
|
|
131
|
+
const resume = await requestStep(client, 'thread/resume', {
|
|
132
|
+
threadId: input.delivery.thread_id,
|
|
133
|
+
persistExtendedHistory: false,
|
|
134
|
+
})
|
|
135
|
+
if ('error' in resume) {
|
|
136
|
+
return {
|
|
137
|
+
error: resume.error,
|
|
138
|
+
detail: resume.detail,
|
|
139
|
+
transport_used: 'codex-appserver',
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const turnStart = await requestStep(client, 'turn/start', {
|
|
144
|
+
threadId: input.delivery.thread_id,
|
|
145
|
+
input: [{ type: 'text', text: input.content, text_elements: [] }],
|
|
146
|
+
})
|
|
147
|
+
if ('error' in turnStart) {
|
|
148
|
+
return {
|
|
149
|
+
error: turnStart.error,
|
|
150
|
+
detail: turnStart.detail,
|
|
151
|
+
transport_used: 'codex-appserver',
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
transport_used: 'codex-appserver',
|
|
158
|
+
thread_id: input.delivery.thread_id,
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return {
|
|
162
|
+
error: 'codex_connect_failed',
|
|
163
|
+
detail: describeError(error),
|
|
164
|
+
transport_used: 'codex-appserver',
|
|
165
|
+
}
|
|
166
|
+
} finally {
|
|
167
|
+
safeClose(ws)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
type Json = unknown
|
|
2
|
+
|
|
3
|
+
interface JsonRpcRequest {
|
|
4
|
+
jsonrpc: '2.0'
|
|
5
|
+
id: number
|
|
6
|
+
method: string
|
|
7
|
+
params: Json
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface JsonRpcNotification {
|
|
11
|
+
jsonrpc: '2.0'
|
|
12
|
+
method: string
|
|
13
|
+
params?: Json
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface JsonRpcResponse {
|
|
17
|
+
jsonrpc: '2.0'
|
|
18
|
+
id: number
|
|
19
|
+
result?: Json
|
|
20
|
+
error?: { code: number; message: string; data?: Json }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type MessageEventLike = { data: unknown }
|
|
24
|
+
type ErrorEventLike = { error?: unknown; message?: string }
|
|
25
|
+
type CloseEventLike = { code?: number; reason?: string }
|
|
26
|
+
|
|
27
|
+
export interface WebSocketLike {
|
|
28
|
+
addEventListener(
|
|
29
|
+
type: 'open' | 'message' | 'error' | 'close',
|
|
30
|
+
listener: (event: unknown) => void
|
|
31
|
+
): void
|
|
32
|
+
removeEventListener?(
|
|
33
|
+
type: 'open' | 'message' | 'error' | 'close',
|
|
34
|
+
listener: (event: unknown) => void
|
|
35
|
+
): void
|
|
36
|
+
send(data: string): void
|
|
37
|
+
close(): void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CodexWebSocketFactoryArgs {
|
|
41
|
+
url: string
|
|
42
|
+
headers?: Record<string, string>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type CodexWebSocketFactory = (
|
|
46
|
+
args: CodexWebSocketFactoryArgs
|
|
47
|
+
) => WebSocketLike
|
|
48
|
+
|
|
49
|
+
type PendingResolver = {
|
|
50
|
+
resolve: (value: JsonRpcResponse) => void
|
|
51
|
+
reject: (reason?: unknown) => void
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function defaultWebSocketFactory(
|
|
55
|
+
args: CodexWebSocketFactoryArgs
|
|
56
|
+
): WebSocketLike {
|
|
57
|
+
const ctor = globalThis.WebSocket as unknown as new (
|
|
58
|
+
url: string,
|
|
59
|
+
options?: { headers?: Record<string, string> }
|
|
60
|
+
) => WebSocketLike
|
|
61
|
+
return new ctor(
|
|
62
|
+
args.url,
|
|
63
|
+
args.headers === undefined ? undefined : { headers: args.headers }
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function describeError(error: unknown): string {
|
|
68
|
+
if (error instanceof Error && error.message.length > 0) return error.message
|
|
69
|
+
if (typeof error === 'string' && error.length > 0) return error
|
|
70
|
+
if (error && typeof error === 'object') {
|
|
71
|
+
const record = error as Record<string, unknown>
|
|
72
|
+
const message = record.message
|
|
73
|
+
if (typeof message === 'string' && message.length > 0) return message
|
|
74
|
+
const reason = record.reason
|
|
75
|
+
if (typeof reason === 'string' && reason.length > 0) return reason
|
|
76
|
+
}
|
|
77
|
+
return String(error)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function closeDetail(event: CloseEventLike): string {
|
|
81
|
+
const code = typeof event.code === 'number' ? event.code : 'unknown'
|
|
82
|
+
const reason = typeof event.reason === 'string' && event.reason.length > 0
|
|
83
|
+
? event.reason
|
|
84
|
+
: 'socket_closed'
|
|
85
|
+
return `close ${code}: ${reason}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function decodeMessageData(data: unknown): string {
|
|
89
|
+
if (typeof data === 'string') return data
|
|
90
|
+
if (data instanceof ArrayBuffer) {
|
|
91
|
+
return new TextDecoder().decode(new Uint8Array(data))
|
|
92
|
+
}
|
|
93
|
+
if (ArrayBuffer.isView(data)) {
|
|
94
|
+
return new TextDecoder().decode(data)
|
|
95
|
+
}
|
|
96
|
+
return String(data)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function safeClose(ws: WebSocketLike): void {
|
|
100
|
+
try {
|
|
101
|
+
ws.close()
|
|
102
|
+
} catch {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function resolveAuthToken(
|
|
108
|
+
authTokenRef: string | undefined,
|
|
109
|
+
env: NodeJS.ProcessEnv
|
|
110
|
+
): { ok: string | undefined } | { error: 'missing_auth_token'; detail: { ref: string } } {
|
|
111
|
+
if (authTokenRef === undefined) return { ok: undefined }
|
|
112
|
+
const token = env[authTokenRef]?.trim()
|
|
113
|
+
if (!token) {
|
|
114
|
+
return {
|
|
115
|
+
error: 'missing_auth_token',
|
|
116
|
+
detail: { ref: authTokenRef },
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { ok: token }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class JsonRpcSocketClient {
|
|
123
|
+
private nextId = 1
|
|
124
|
+
private readonly pending = new Map<number, PendingResolver>()
|
|
125
|
+
private openState:
|
|
126
|
+
| { kind: 'pending'; promise: Promise<void> }
|
|
127
|
+
| { kind: 'open' }
|
|
128
|
+
| { kind: 'failed'; error: unknown }
|
|
129
|
+
|
|
130
|
+
constructor(private readonly ws: WebSocketLike) {
|
|
131
|
+
this.openState = {
|
|
132
|
+
kind: 'pending',
|
|
133
|
+
promise: new Promise<void>((resolve, reject) => {
|
|
134
|
+
const onOpen = () => {
|
|
135
|
+
cleanup()
|
|
136
|
+
this.openState = { kind: 'open' }
|
|
137
|
+
resolve()
|
|
138
|
+
}
|
|
139
|
+
const onError = (event: unknown) => {
|
|
140
|
+
cleanup()
|
|
141
|
+
const detail = event as ErrorEventLike
|
|
142
|
+
const error = detail.error ?? detail.message ?? 'websocket_error'
|
|
143
|
+
this.openState = { kind: 'failed', error }
|
|
144
|
+
reject(error)
|
|
145
|
+
}
|
|
146
|
+
const onClose = (event: unknown) => {
|
|
147
|
+
cleanup()
|
|
148
|
+
const closeEvent = event as CloseEventLike
|
|
149
|
+
const error = closeDetail(closeEvent)
|
|
150
|
+
this.openState = { kind: 'failed', error }
|
|
151
|
+
reject(error)
|
|
152
|
+
}
|
|
153
|
+
const cleanup = () => {
|
|
154
|
+
this.ws.removeEventListener?.('open', onOpen)
|
|
155
|
+
this.ws.removeEventListener?.('error', onError)
|
|
156
|
+
this.ws.removeEventListener?.('close', onClose)
|
|
157
|
+
}
|
|
158
|
+
this.ws.addEventListener('open', onOpen)
|
|
159
|
+
this.ws.addEventListener('error', onError)
|
|
160
|
+
this.ws.addEventListener('close', onClose)
|
|
161
|
+
}),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.ws.addEventListener('message', (event) => {
|
|
165
|
+
let message: JsonRpcResponse
|
|
166
|
+
try {
|
|
167
|
+
message = JSON.parse(decodeMessageData((event as MessageEventLike).data))
|
|
168
|
+
} catch {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
if (typeof message.id !== 'number') return
|
|
172
|
+
const pending = this.pending.get(message.id)
|
|
173
|
+
if (!pending) return
|
|
174
|
+
this.pending.delete(message.id)
|
|
175
|
+
pending.resolve(message)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
this.ws.addEventListener('error', (event) => {
|
|
179
|
+
if (this.openState.kind !== 'open') return
|
|
180
|
+
const detail = event as ErrorEventLike
|
|
181
|
+
const error = detail.error ?? detail.message ?? 'websocket_error'
|
|
182
|
+
this.rejectAll(error)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
this.ws.addEventListener('close', (event) => {
|
|
186
|
+
if (this.openState.kind !== 'open') return
|
|
187
|
+
this.rejectAll(closeDetail(event as CloseEventLike))
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async waitForOpen(): Promise<void> {
|
|
192
|
+
if (this.openState.kind === 'open') return
|
|
193
|
+
if (this.openState.kind === 'failed') throw this.openState.error
|
|
194
|
+
await this.openState.promise
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
request(method: string, params: Json): Promise<JsonRpcResponse> {
|
|
198
|
+
const id = this.nextId++
|
|
199
|
+
const request: JsonRpcRequest = { jsonrpc: '2.0', id, method, params }
|
|
200
|
+
return new Promise<JsonRpcResponse>((resolve, reject) => {
|
|
201
|
+
this.pending.set(id, { resolve, reject })
|
|
202
|
+
try {
|
|
203
|
+
this.ws.send(JSON.stringify(request))
|
|
204
|
+
} catch (error) {
|
|
205
|
+
this.pending.delete(id)
|
|
206
|
+
reject(error)
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
notify(method: string, params?: Json): void {
|
|
212
|
+
const notification: JsonRpcNotification = {
|
|
213
|
+
jsonrpc: '2.0',
|
|
214
|
+
method,
|
|
215
|
+
...(params === undefined ? {} : { params }),
|
|
216
|
+
}
|
|
217
|
+
this.ws.send(JSON.stringify(notification))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private rejectAll(error: unknown): void {
|
|
221
|
+
const pending = [...this.pending.values()]
|
|
222
|
+
this.pending.clear()
|
|
223
|
+
for (const entry of pending) {
|
|
224
|
+
entry.reject(error)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3'
|
|
2
|
+
|
|
3
|
+
export interface CodexPanePreRegRow {
|
|
4
|
+
pane_id: string
|
|
5
|
+
xats_agent_id: string
|
|
6
|
+
expires_at: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UpsertInput {
|
|
10
|
+
pane_id: string
|
|
11
|
+
xats_agent_id: string
|
|
12
|
+
expires_at: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class CodexPanePreRegRepo {
|
|
16
|
+
constructor(private readonly db: Database.Database) {}
|
|
17
|
+
|
|
18
|
+
upsert(input: UpsertInput): void {
|
|
19
|
+
this.db
|
|
20
|
+
.prepare(
|
|
21
|
+
`INSERT INTO codex_pane_pre_registrations (pane_id, xats_agent_id, expires_at)
|
|
22
|
+
VALUES (?, ?, ?)
|
|
23
|
+
ON CONFLICT(pane_id) DO UPDATE SET
|
|
24
|
+
xats_agent_id = excluded.xats_agent_id,
|
|
25
|
+
expires_at = excluded.expires_at`
|
|
26
|
+
)
|
|
27
|
+
.run(input.pane_id, input.xats_agent_id, input.expires_at)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
listUnexpired(now: string): CodexPanePreRegRow[] {
|
|
31
|
+
return this.db
|
|
32
|
+
.prepare(
|
|
33
|
+
`SELECT pane_id, xats_agent_id, expires_at
|
|
34
|
+
FROM codex_pane_pre_registrations
|
|
35
|
+
WHERE expires_at > ?`
|
|
36
|
+
)
|
|
37
|
+
.all(now) as CodexPanePreRegRow[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
takeByPaneId(pane_id: string): CodexPanePreRegRow | undefined {
|
|
41
|
+
const row = this.db
|
|
42
|
+
.prepare(
|
|
43
|
+
`DELETE FROM codex_pane_pre_registrations
|
|
44
|
+
WHERE pane_id = ?
|
|
45
|
+
RETURNING pane_id, xats_agent_id, expires_at`
|
|
46
|
+
)
|
|
47
|
+
.get(pane_id) as CodexPanePreRegRow | undefined
|
|
48
|
+
return row
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
deleteExpired(now: string): number {
|
|
52
|
+
const res = this.db
|
|
53
|
+
.prepare(`DELETE FROM codex_pane_pre_registrations WHERE expires_at <= ?`)
|
|
54
|
+
.run(now)
|
|
55
|
+
return res.changes
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3'
|
|
2
|
+
import type { AutoPokeSkipReason } from './auto-poke-fanout.js'
|
|
3
|
+
|
|
4
|
+
export type WakeStatus = 'delivered' | 'retrying' | 'skipped' | 'failed'
|
|
5
|
+
export type DeliverySkipReason =
|
|
6
|
+
| AutoPokeSkipReason
|
|
7
|
+
| 'auto_poke_disabled'
|
|
8
|
+
| 'recipient_active'
|
|
9
|
+
| 'retry_exhausted'
|
|
10
|
+
|
|
11
|
+
export interface DeliveryStatusRow {
|
|
12
|
+
agent_id: string
|
|
13
|
+
wake_status: WakeStatus
|
|
14
|
+
skip_reason: DeliverySkipReason | null
|
|
15
|
+
retry_attempts: number
|
|
16
|
+
updated_at: string
|
|
17
|
+
delivered_at: string | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function recordInitialDeliveryStatuses(
|
|
21
|
+
db: Database.Database,
|
|
22
|
+
args: {
|
|
23
|
+
messageId: string
|
|
24
|
+
recipients: string[]
|
|
25
|
+
delivered: Set<string>
|
|
26
|
+
skipped: Array<{ agent_id: string; reason: AutoPokeSkipReason }>
|
|
27
|
+
autoPokeDisabled?: boolean
|
|
28
|
+
}
|
|
29
|
+
): void {
|
|
30
|
+
const now = new Date().toISOString()
|
|
31
|
+
const skipped = new Map(args.skipped.map(x => [x.agent_id, x.reason]))
|
|
32
|
+
const stmt = db.prepare(
|
|
33
|
+
`INSERT INTO message_delivery_status
|
|
34
|
+
(message_id, agent_id, wake_status, skip_reason, retry_attempts, updated_at, delivered_at)
|
|
35
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
36
|
+
ON CONFLICT(message_id, agent_id) DO UPDATE SET
|
|
37
|
+
wake_status=excluded.wake_status,
|
|
38
|
+
skip_reason=excluded.skip_reason,
|
|
39
|
+
retry_attempts=excluded.retry_attempts,
|
|
40
|
+
updated_at=excluded.updated_at,
|
|
41
|
+
delivered_at=excluded.delivered_at`
|
|
42
|
+
)
|
|
43
|
+
const tx = db.transaction(() => {
|
|
44
|
+
for (const agentId of args.recipients) {
|
|
45
|
+
const reason = args.autoPokeDisabled ? 'auto_poke_disabled' : skipped.get(agentId)
|
|
46
|
+
const delivered = args.delivered.has(agentId)
|
|
47
|
+
const status: WakeStatus = delivered
|
|
48
|
+
? 'delivered'
|
|
49
|
+
: reason === 'guard_failed' ? 'retrying' : 'skipped'
|
|
50
|
+
stmt.run(
|
|
51
|
+
args.messageId,
|
|
52
|
+
agentId,
|
|
53
|
+
status,
|
|
54
|
+
delivered ? null : reason,
|
|
55
|
+
0,
|
|
56
|
+
now,
|
|
57
|
+
delivered ? now : null
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
tx()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function updateDeliveryStatus(
|
|
65
|
+
db: Database.Database,
|
|
66
|
+
messageId: string,
|
|
67
|
+
agentId: string,
|
|
68
|
+
args: {
|
|
69
|
+
wake_status: WakeStatus
|
|
70
|
+
skip_reason?: DeliverySkipReason | null
|
|
71
|
+
retry_attempts?: number
|
|
72
|
+
delivered_at?: string | null
|
|
73
|
+
}
|
|
74
|
+
): void {
|
|
75
|
+
const now = new Date().toISOString()
|
|
76
|
+
db.prepare(
|
|
77
|
+
`UPDATE message_delivery_status
|
|
78
|
+
SET wake_status=?,
|
|
79
|
+
skip_reason=?,
|
|
80
|
+
retry_attempts=COALESCE(?, retry_attempts),
|
|
81
|
+
updated_at=?,
|
|
82
|
+
delivered_at=?
|
|
83
|
+
WHERE message_id=? AND agent_id=?`
|
|
84
|
+
).run(
|
|
85
|
+
args.wake_status,
|
|
86
|
+
args.skip_reason ?? null,
|
|
87
|
+
args.retry_attempts ?? null,
|
|
88
|
+
now,
|
|
89
|
+
args.delivered_at === undefined ? null : args.delivered_at,
|
|
90
|
+
messageId,
|
|
91
|
+
agentId
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class GetDeliveryStatusService {
|
|
96
|
+
constructor(private db: Database.Database) {}
|
|
97
|
+
|
|
98
|
+
get(args: { caller: string; message_id: string }):
|
|
99
|
+
| { message_id: string; statuses: DeliveryStatusRow[] }
|
|
100
|
+
| { error: 'unknown_message' } {
|
|
101
|
+
const owned = this.db.prepare(
|
|
102
|
+
`SELECT 1 AS ok FROM messages WHERE id=? AND from_agent_id=? LIMIT 1`
|
|
103
|
+
).get(args.message_id, args.caller) as { ok: number } | undefined
|
|
104
|
+
if (!owned) return { error: 'unknown_message' }
|
|
105
|
+
|
|
106
|
+
const rows = this.db.prepare(
|
|
107
|
+
`SELECT agent_id, wake_status, skip_reason, retry_attempts, updated_at, delivered_at
|
|
108
|
+
FROM message_delivery_status
|
|
109
|
+
WHERE message_id=?
|
|
110
|
+
ORDER BY agent_id ASC`
|
|
111
|
+
).all(args.message_id) as DeliveryStatusRow[]
|
|
112
|
+
return { message_id: args.message_id, statuses: rows }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3'
|
|
2
|
+
import type { AgentsRepo } from '../storage/agents-repo.js'
|
|
3
|
+
import { diffSchema, type ContractDiff } from '../lib/schema-diff.js'
|
|
4
|
+
|
|
5
|
+
export type DiffContractsResult =
|
|
6
|
+
| ContractDiff
|
|
7
|
+
| { error: 'unknown_contract' | 'unknown_version' | 'unknown_agent' }
|
|
8
|
+
|
|
9
|
+
export class DiffContractsService {
|
|
10
|
+
constructor(private db: Database.Database, private agents: AgentsRepo) {}
|
|
11
|
+
|
|
12
|
+
diff(args: { caller: string; name: string; from_version: number; to_version: number }): DiffContractsResult {
|
|
13
|
+
const caller = this.agents.findById(args.caller)
|
|
14
|
+
if (!caller) return { error: 'unknown_agent' }
|
|
15
|
+
const from = this.db.prepare('SELECT schema FROM contracts WHERE team=? AND name=? AND version=?')
|
|
16
|
+
.get(caller.team, args.name, args.from_version) as { schema: string } | undefined
|
|
17
|
+
const to = this.db.prepare('SELECT schema FROM contracts WHERE team=? AND name=? AND version=?')
|
|
18
|
+
.get(caller.team, args.name, args.to_version) as { schema: string } | undefined
|
|
19
|
+
if (!from || !to) {
|
|
20
|
+
const exists = this.db.prepare('SELECT 1 FROM contracts WHERE team=? AND name=? LIMIT 1').get(caller.team, args.name)
|
|
21
|
+
return exists ? { error: 'unknown_version' } : { error: 'unknown_contract' }
|
|
22
|
+
}
|
|
23
|
+
return diffSchema(JSON.parse(from.schema), JSON.parse(to.schema))
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/mcp/echo.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const echoSchema = { msg: z.string() }
|
|
4
|
+
|
|
5
|
+
export async function echoHandler(args: { msg: string }): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
|
|
6
|
+
const out = { msg: args.msg, echoed_at: new Date().toISOString() }
|
|
7
|
+
return { content: [{ type: 'text', text: JSON.stringify(out) }] }
|
|
8
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3'
|
|
2
|
+
import { fanoutAutoPoke, type AutoPokeRecipient, type AutoPokeSkipReason, type FanoutDeps } from './auto-poke-fanout.js'
|
|
3
|
+
import { RETRY_DELAYS_S } from './poke-retry.js'
|
|
4
|
+
import { recordInitialDeliveryStatuses, updateDeliveryStatus } from './delivery-status.js'
|
|
5
|
+
|
|
6
|
+
export interface FanoutResultEnvelope {
|
|
7
|
+
poked: boolean
|
|
8
|
+
poke_skip_reasons?: Array<{ agent_id: string; reason: AutoPokeSkipReason }>
|
|
9
|
+
retry_scheduled: boolean
|
|
10
|
+
retry_delays_s?: number[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Shared fan-out + retry wiring used by both BroadcastService (all-team) and BroadcastToRoleService (same-team role-scoped).
|
|
14
|
+
// Same recipient-lookup SQL (agent_id-only, team-agnostic) keeps cross-team retry behaviour consistent.
|
|
15
|
+
export async function runFanoutWithRetry(args: {
|
|
16
|
+
db: Database.Database
|
|
17
|
+
team: string
|
|
18
|
+
fromAgentId: string
|
|
19
|
+
recipients: AutoPokeRecipient[]
|
|
20
|
+
body: string
|
|
21
|
+
deps: FanoutDeps
|
|
22
|
+
messageId: string
|
|
23
|
+
sentAt: string
|
|
24
|
+
}): Promise<FanoutResultEnvelope> {
|
|
25
|
+
const { db } = args
|
|
26
|
+
const fanout = await fanoutAutoPoke({
|
|
27
|
+
team: args.team,
|
|
28
|
+
fromAgentId: args.fromAgentId,
|
|
29
|
+
recipients: args.recipients,
|
|
30
|
+
body: args.body,
|
|
31
|
+
deps: args.deps,
|
|
32
|
+
retry: {
|
|
33
|
+
messageId: args.messageId,
|
|
34
|
+
sentAt: args.sentAt,
|
|
35
|
+
lookupAgentFn: (agentId: string) => db.prepare(
|
|
36
|
+
'SELECT agent_id, tmux_pane_id, last_seen_at FROM agents WHERE agent_id=?'
|
|
37
|
+
).get(agentId) as { agent_id: string; tmux_pane_id: string | null; last_seen_at: string } | undefined,
|
|
38
|
+
updateStatusFn: (status) => {
|
|
39
|
+
updateDeliveryStatus(db, args.messageId, status.agentId, status)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
recordInitialDeliveryStatuses(db, {
|
|
44
|
+
messageId: args.messageId,
|
|
45
|
+
recipients: args.recipients.map(r => r.agent_id),
|
|
46
|
+
delivered: new Set(fanout.deliveredAgentIds),
|
|
47
|
+
skipped: fanout.skipReasons
|
|
48
|
+
})
|
|
49
|
+
const retry_scheduled = fanout.retryScheduledCount > 0
|
|
50
|
+
return {
|
|
51
|
+
poked: fanout.poked,
|
|
52
|
+
poke_skip_reasons: fanout.skipReasons,
|
|
53
|
+
retry_scheduled,
|
|
54
|
+
...(retry_scheduled ? { retry_delays_s: [...RETRY_DELAYS_S] } : {})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3'
|
|
2
|
+
import type { AgentsRepo } from '../storage/agents-repo.js'
|
|
3
|
+
|
|
4
|
+
export type GetContractResult =
|
|
5
|
+
| { name: string; version: number; schema: Record<string, unknown>; format: string; note: string | null; registered_at: string }
|
|
6
|
+
| { error: 'unknown_contract' | 'unknown_version' | 'unknown_agent' }
|
|
7
|
+
|
|
8
|
+
export class GetContractService {
|
|
9
|
+
constructor(private db: Database.Database, private agents: AgentsRepo) {}
|
|
10
|
+
|
|
11
|
+
get(args: { caller: string; name: string; version?: number }): GetContractResult {
|
|
12
|
+
const caller = this.agents.findById(args.caller)
|
|
13
|
+
if (!caller) return { error: 'unknown_agent' }
|
|
14
|
+
const row = args.version
|
|
15
|
+
? this.db.prepare('SELECT * FROM contracts WHERE team=? AND name=? AND version=?').get(caller.team, args.name, args.version)
|
|
16
|
+
: this.db.prepare('SELECT * FROM contracts WHERE team=? AND name=? ORDER BY version DESC LIMIT 1').get(caller.team, args.name)
|
|
17
|
+
if (!row) {
|
|
18
|
+
const exists = this.db.prepare('SELECT 1 FROM contracts WHERE team=? AND name=? LIMIT 1').get(caller.team, args.name)
|
|
19
|
+
return exists ? { error: 'unknown_version' } : { error: 'unknown_contract' }
|
|
20
|
+
}
|
|
21
|
+
const r = row as { name: string; version: number; schema: string; format: string; note: string | null; registered_at: string }
|
|
22
|
+
return { name: r.name, version: r.version, schema: JSON.parse(r.schema), format: r.format, note: r.note, registered_at: r.registered_at }
|
|
23
|
+
}
|
|
24
|
+
}
|