typeclaw 0.36.8 → 0.37.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/README.md +2 -2
- package/package.json +3 -2
- package/src/agent/index.ts +31 -11
- package/src/agent/live-sessions.ts +12 -0
- package/src/agent/model-fallback.ts +17 -15
- package/src/agent/model-overrides.ts +2 -2
- package/src/agent/session-meta.ts +10 -0
- package/src/agent/subagents.ts +11 -2
- package/src/agent/system-prompt.ts +9 -3
- package/src/agent/todo/continuation-policy.ts +6 -3
- package/src/agent/todo/continuation-wiring.ts +4 -2
- package/src/agent/todo/continuation.ts +3 -3
- package/src/agent/tools/todo/index.ts +27 -4
- package/src/bundled-plugins/agent-browser/index.ts +33 -108
- package/src/bundled-plugins/agent-browser/shim.ts +3 -94
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +8 -33
- package/src/bundled-plugins/doc-render/skills/typeclaw-render-pdf/SKILL.md +2 -2
- package/src/bundled-plugins/guard/policies/memory-retrieval-cache-write.ts +7 -1
- package/src/bundled-plugins/memory/README.md +80 -23
- package/src/bundled-plugins/memory/append-tool.ts +74 -53
- package/src/bundled-plugins/memory/citation-superset.ts +4 -0
- package/src/bundled-plugins/memory/citations.ts +54 -0
- package/src/bundled-plugins/memory/dreaming-metrics.ts +30 -0
- package/src/bundled-plugins/memory/dreaming.ts +444 -21
- package/src/bundled-plugins/memory/index.ts +544 -400
- package/src/bundled-plugins/memory/load-memory.ts +87 -10
- package/src/bundled-plugins/memory/load-shards.ts +48 -22
- package/src/bundled-plugins/memory/memory-logger.ts +95 -106
- package/src/bundled-plugins/memory/memory-retrieval.ts +3 -3
- package/src/bundled-plugins/memory/parent-link.ts +33 -0
- package/src/bundled-plugins/memory/paths.ts +12 -0
- package/src/bundled-plugins/memory/references/frontmatter.ts +197 -0
- package/src/bundled-plugins/memory/references/load-references.ts +212 -0
- package/src/bundled-plugins/memory/references/store-reference-tool.ts +59 -0
- package/src/bundled-plugins/memory/search-tool.ts +282 -45
- package/src/bundled-plugins/memory/stream-events.ts +1 -0
- package/src/bundled-plugins/memory/stream-io.ts +28 -3
- package/src/bundled-plugins/memory/turn-dedup.ts +40 -0
- package/src/bundled-plugins/memory/vector/cache-write.ts +19 -0
- package/src/bundled-plugins/memory/vector/config.ts +28 -0
- package/src/bundled-plugins/memory/vector/doctor.ts +124 -0
- package/src/bundled-plugins/memory/vector/embedder.ts +246 -0
- package/src/bundled-plugins/memory/vector/hybrid.ts +439 -0
- package/src/bundled-plugins/memory/vector/index-on-write.ts +34 -0
- package/src/bundled-plugins/memory/vector/inspect.ts +111 -0
- package/src/bundled-plugins/memory/vector/passages.ts +125 -0
- package/src/bundled-plugins/memory/vector/reference-index-on-write.ts +50 -0
- package/src/bundled-plugins/memory/vector/relevance-gate.ts +93 -0
- package/src/bundled-plugins/memory/vector/startup.ts +71 -0
- package/src/bundled-plugins/memory/vector/store.ts +203 -0
- package/src/bundled-plugins/memory/vector/truncation.ts +124 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +2 -0
- package/src/channels/router.ts +239 -40
- package/src/cli/incomplete-init.ts +57 -0
- package/src/cli/init.ts +143 -12
- package/src/cli/inspect.ts +11 -5
- package/src/cli/model.ts +112 -34
- package/src/cli/restart.ts +24 -0
- package/src/cli/start.ts +24 -0
- package/src/cli/tunnel.ts +53 -8
- package/src/config/config.ts +110 -19
- package/src/config/index.ts +5 -1
- package/src/config/models-mutation.ts +29 -11
- package/src/config/providers-mutation.ts +2 -2
- package/src/config/providers.ts +146 -12
- package/src/container/shared.ts +9 -0
- package/src/container/start.ts +87 -4
- package/src/cron/consumer.ts +13 -7
- package/src/hostd/models.ts +64 -0
- package/src/hostd/paths.ts +6 -0
- package/src/hostd/portbroker-manager.ts +2 -2
- package/src/init/checkpoint.ts +201 -0
- package/src/init/dockerfile.ts +121 -34
- package/src/init/gitignore.ts +7 -7
- package/src/init/index.ts +41 -9
- package/src/init/models-dev.ts +96 -21
- package/src/init/oauth-login.ts +3 -3
- package/src/init/progress.ts +29 -0
- package/src/init/validate-api-key.ts +4 -0
- package/src/inspect/index.ts +13 -6
- package/src/inspect/item-list.ts +11 -2
- package/src/inspect/live-list.ts +65 -0
- package/src/inspect/open-item.ts +22 -1
- package/src/inspect/session-list.ts +29 -0
- package/src/models/embedding-model.ts +114 -0
- package/src/models/transformers-version.ts +55 -0
- package/src/plugin/types.ts +3 -0
- package/src/portbroker/container-server.ts +23 -0
- package/src/portbroker/forward-request-bus.ts +35 -0
- package/src/portbroker/forward-result-bus.ts +2 -3
- package/src/portbroker/hostd-client.ts +182 -36
- package/src/portbroker/index.ts +6 -1
- package/src/portbroker/protocol.ts +9 -2
- package/src/run/channel-session-factory.ts +11 -1
- package/src/run/index.ts +41 -7
- package/src/server/command-runner.ts +24 -1
- package/src/server/index.ts +42 -8
- package/src/shared/index.ts +2 -0
- package/src/shared/protocol.ts +31 -0
- package/src/skills/typeclaw-channels/SKILL.md +4 -4
- package/src/skills/typeclaw-config/SKILL.md +2 -2
- package/src/skills/typeclaw-memory/SKILL.md +3 -1
- package/src/skills/typeclaw-permissions/SKILL.md +3 -3
- package/src/skills/typeclaw-skills/SKILL.md +1 -1
- package/src/skills/typeclaw-tunnels/SKILL.md +22 -1
- package/src/tunnels/providers/cloudflare-quick.ts +65 -7
- package/src/tunnels/upstream-probe.ts +25 -0
- package/typeclaw.schema.json +156 -67
- package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +0 -170
- package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +0 -421
- package/src/portbroker/bind-with-forward.ts +0 -102
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
2
2
|
|
|
3
3
|
import type { ServerWebSocket } from 'bun'
|
|
4
4
|
|
|
5
|
+
import type { ForwardRequestEvent } from './forward-request-bus'
|
|
5
6
|
import { parseProcNetTcp } from './proc-net-tcp'
|
|
6
7
|
import { decodeBytes, encodeBytes, type ContainerToHostd, type HostdToContainer, type StreamId } from './protocol'
|
|
7
8
|
|
|
@@ -23,6 +24,7 @@ export type ContainerBrokerOptions = {
|
|
|
23
24
|
// to the host side. Without it, code that picks an in-container port has no
|
|
24
25
|
// way to detect host-side EADDRINUSE collisions across containers.
|
|
25
26
|
onForwardResult?: (event: ForwardResultEvent) => void
|
|
27
|
+
onForwardRequestSubscribe?: (cb: (event: ForwardRequestEvent) => void) => () => void
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export type ForwardResultEvent =
|
|
@@ -72,6 +74,8 @@ export function createContainerBroker(opts: ContainerBrokerOptions): ContainerBr
|
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
const sessions = new WeakMap<BrokerSocket, SessionState>()
|
|
77
|
+
const sockets = new Set<BrokerSocket>()
|
|
78
|
+
const reserved = new Map<number, ForwardRequestEvent>()
|
|
75
79
|
|
|
76
80
|
const send = (ws: BrokerSocket, msg: ContainerToHostd): void => {
|
|
77
81
|
try {
|
|
@@ -98,6 +102,22 @@ export function createContainerBroker(opts: ContainerBrokerOptions): ContainerBr
|
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
104
|
|
|
105
|
+
const sendReservedRequest = (ws: BrokerSocket, request: ForwardRequestEvent): void => {
|
|
106
|
+
send(ws, {
|
|
107
|
+
type: 'port-forward-request',
|
|
108
|
+
targetPort: request.targetPort,
|
|
109
|
+
hostCandidates: request.hostCandidates,
|
|
110
|
+
...(request.reason !== undefined ? { reason: request.reason } : {}),
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
opts.onForwardRequestSubscribe?.((event) => {
|
|
115
|
+
reserved.set(event.targetPort, event)
|
|
116
|
+
for (const ws of sockets) {
|
|
117
|
+
if (ws.data.authed) sendReservedRequest(ws, event)
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
101
121
|
const tickWatcher = async (ws: BrokerSocket, state: SessionState): Promise<void> => {
|
|
102
122
|
const next = await snapshotPorts()
|
|
103
123
|
for (const [port, bindAddr] of next) {
|
|
@@ -158,6 +178,7 @@ export function createContainerBroker(opts: ContainerBrokerOptions): ContainerBr
|
|
|
158
178
|
return {
|
|
159
179
|
open(ws) {
|
|
160
180
|
sessions.set(ws, { pollTimer: null, lastSnapshot: new Map(), upstreams: new Map() })
|
|
181
|
+
sockets.add(ws)
|
|
161
182
|
},
|
|
162
183
|
|
|
163
184
|
async message(ws, raw) {
|
|
@@ -187,6 +208,7 @@ export function createContainerBroker(opts: ContainerBrokerOptions): ContainerBr
|
|
|
187
208
|
ws.data.authed = true
|
|
188
209
|
log({ kind: 'authed' })
|
|
189
210
|
send(ws, { type: 'broker-hello-ack' })
|
|
211
|
+
for (const request of reserved.values()) sendReservedRequest(ws, request)
|
|
190
212
|
return
|
|
191
213
|
}
|
|
192
214
|
|
|
@@ -252,6 +274,7 @@ export function createContainerBroker(opts: ContainerBrokerOptions): ContainerBr
|
|
|
252
274
|
} catch {}
|
|
253
275
|
}
|
|
254
276
|
state.upstreams.clear()
|
|
277
|
+
sockets.delete(ws)
|
|
255
278
|
sessions.delete(ws)
|
|
256
279
|
},
|
|
257
280
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// In-process event bus for explicit container→host forward requests. The
|
|
2
|
+
// agent-browser plugin runs in the same process as the container broker but
|
|
3
|
+
// does not hold a broker handle, so it publishes here and run/index wires the
|
|
4
|
+
// bus into createContainerBroker.
|
|
5
|
+
|
|
6
|
+
export type ForwardRequestEvent = {
|
|
7
|
+
targetPort: number
|
|
8
|
+
hostCandidates: number[]
|
|
9
|
+
reason?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type Subscriber = (event: ForwardRequestEvent) => void
|
|
13
|
+
|
|
14
|
+
const subscribers = new Set<Subscriber>()
|
|
15
|
+
|
|
16
|
+
export function publishForwardRequest(event: ForwardRequestEvent): void {
|
|
17
|
+
for (const sub of subscribers) {
|
|
18
|
+
try {
|
|
19
|
+
sub(event)
|
|
20
|
+
} catch {
|
|
21
|
+
// Subscriber failures must not block peer subscribers.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function subscribeForwardRequest(cb: Subscriber): () => void {
|
|
27
|
+
subscribers.add(cb)
|
|
28
|
+
return () => {
|
|
29
|
+
subscribers.delete(cb)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function __resetForwardRequestBus(): void {
|
|
34
|
+
subscribers.clear()
|
|
35
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// In-process event bus for `port-forward-result` events emitted by the
|
|
2
2
|
// host-side broker over the WS to the container side. Lives as a module-level
|
|
3
3
|
// singleton so the run-loop wiring (src/run/index.ts) can publish events from
|
|
4
|
-
// the broker callback while consumers
|
|
5
|
-
//
|
|
6
|
-
// needing a reference to the ContainerBroker itself.
|
|
4
|
+
// the broker callback while consumers subscribe by importing this module
|
|
5
|
+
// without needing a reference to the ContainerBroker itself.
|
|
7
6
|
//
|
|
8
7
|
// Tests should call `__resetForwardResultBus()` in afterEach so subscriptions
|
|
9
8
|
// from a previous test don't leak.
|
|
@@ -85,13 +85,21 @@ export function createBroker(opts: BrokerOptions): Broker {
|
|
|
85
85
|
const listenHost = opts.listenHost ?? defaultListenHost
|
|
86
86
|
|
|
87
87
|
type ForwarderState = {
|
|
88
|
-
|
|
88
|
+
targetPort: number
|
|
89
|
+
hostPort: number
|
|
89
90
|
bindAddr: BindAddr
|
|
91
|
+
reserved: boolean
|
|
90
92
|
listener: HostListener
|
|
91
93
|
streams: Map<StreamId, { sock: HostSocket; opened: boolean; pending: Uint8Array[] }>
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
const forwarders = new Map<number, ForwarderState>()
|
|
97
|
+
const reservedTargets = new Map<number, number>()
|
|
98
|
+
// Targets claimed by a reserved forward whose host bind is still in flight.
|
|
99
|
+
// Marked synchronously BEFORE awaiting listenHost() so a concurrent
|
|
100
|
+
// port-listen snapshot/opened for the same target cannot slip past the
|
|
101
|
+
// reserved guard and install a competing auto-forward during the await.
|
|
102
|
+
const pendingReservedTargets = new Set<number>()
|
|
95
103
|
let ws: WsClient | null = null
|
|
96
104
|
let nextStreamId: StreamId = 1
|
|
97
105
|
let stopped = false
|
|
@@ -112,8 +120,8 @@ export function createBroker(opts: BrokerOptions): Broker {
|
|
|
112
120
|
}
|
|
113
121
|
}
|
|
114
122
|
|
|
115
|
-
const closeStream = (
|
|
116
|
-
const fwd = forwarders.get(
|
|
123
|
+
const closeStream = (hostPort: number, streamId: StreamId, sendClose: boolean): void => {
|
|
124
|
+
const fwd = forwarders.get(hostPort)
|
|
117
125
|
if (!fwd) return
|
|
118
126
|
const stream = fwd.streams.get(streamId)
|
|
119
127
|
if (!stream) return
|
|
@@ -124,7 +132,7 @@ export function createBroker(opts: BrokerOptions): Broker {
|
|
|
124
132
|
if (sendClose && ws) ws.send({ type: 'relay-close', streamId, side: 'downstream' })
|
|
125
133
|
}
|
|
126
134
|
|
|
127
|
-
const handleHostConnection = (
|
|
135
|
+
const handleHostConnection = (hostPort: number, sock: HostSocket): void => {
|
|
128
136
|
if (!ws) {
|
|
129
137
|
try {
|
|
130
138
|
sock.end()
|
|
@@ -132,7 +140,7 @@ export function createBroker(opts: BrokerOptions): Broker {
|
|
|
132
140
|
return
|
|
133
141
|
}
|
|
134
142
|
const streamId = allocStreamId()
|
|
135
|
-
const fwd = forwarders.get(
|
|
143
|
+
const fwd = forwarders.get(hostPort)
|
|
136
144
|
if (!fwd) {
|
|
137
145
|
try {
|
|
138
146
|
sock.end()
|
|
@@ -153,53 +161,174 @@ export function createBroker(opts: BrokerOptions): Broker {
|
|
|
153
161
|
}
|
|
154
162
|
})
|
|
155
163
|
sock.onClose(() => {
|
|
156
|
-
closeStream(
|
|
164
|
+
closeStream(hostPort, streamId, true)
|
|
157
165
|
})
|
|
158
166
|
|
|
159
|
-
if (ws) ws.send({ type: 'relay-open', streamId, port })
|
|
167
|
+
if (ws) ws.send({ type: 'relay-open', streamId, port: fwd.targetPort })
|
|
160
168
|
}
|
|
161
169
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
const isReservedTarget = (targetPort: number): boolean =>
|
|
171
|
+
reservedTargets.has(targetPort) || pendingReservedTargets.has(targetPort)
|
|
172
|
+
|
|
173
|
+
const hasAutoForwarderForTarget = (targetPort: number): boolean => {
|
|
174
|
+
for (const fwd of forwarders.values()) {
|
|
175
|
+
if (!fwd.reserved && fwd.targetPort === targetPort) return true
|
|
176
|
+
}
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const installForwarder = async (targetPort: number, bindAddr: BindAddr): Promise<void> => {
|
|
181
|
+
if (isReservedTarget(targetPort)) return
|
|
182
|
+
// Dedup by targetPort, not the map key: the map is keyed by the bound host
|
|
183
|
+
// port, which diverges from targetPort on an ephemeral bind, so a
|
|
184
|
+
// `forwarders.has(targetPort)` guard would miss an existing forward and bind
|
|
185
|
+
// a second host listener for the same container port.
|
|
186
|
+
if (hasAutoForwarderForTarget(targetPort)) return
|
|
187
|
+
if (!shouldForward({ policy: opts.policy, port: targetPort })) {
|
|
188
|
+
// Policy excluded the port. Tell the container so consumers waiting on
|
|
189
|
+
// port-forward-result can surface a diagnostic instead of hanging.
|
|
190
|
+
if (ws) ws.send({ type: 'port-forward-result', port: targetPort, ok: false, reason: 'policy excluded' })
|
|
170
191
|
return
|
|
171
192
|
}
|
|
172
193
|
try {
|
|
173
|
-
|
|
174
|
-
|
|
194
|
+
// The host listen port — not targetPort — is the forwarders map key, so
|
|
195
|
+
// the connection callback must look up by it. They coincide for ordinary
|
|
196
|
+
// 1:1 auto-forwards but diverge whenever the OS reassigns the bind (e.g. a
|
|
197
|
+
// port-0 ephemeral bind), at which point routing by targetPort misses the
|
|
198
|
+
// map and silently drops the connection. Captured after the await; a
|
|
199
|
+
// connection can only arrive once the listener is bound.
|
|
200
|
+
let boundHostPort: number | undefined
|
|
201
|
+
const listener = await listenHost(hostBind, targetPort, {
|
|
202
|
+
onConnection: (sock) => {
|
|
203
|
+
if (boundHostPort === undefined) {
|
|
204
|
+
try {
|
|
205
|
+
sock.end()
|
|
206
|
+
} catch {}
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
handleHostConnection(boundHostPort, sock)
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
boundHostPort = listener.port
|
|
213
|
+
forwarders.set(boundHostPort, {
|
|
214
|
+
targetPort,
|
|
215
|
+
hostPort: boundHostPort,
|
|
216
|
+
bindAddr,
|
|
217
|
+
reserved: false,
|
|
218
|
+
listener,
|
|
219
|
+
streams: new Map(),
|
|
175
220
|
})
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
221
|
+
emit({
|
|
222
|
+
kind: 'port-forward-opened',
|
|
223
|
+
containerName: opts.containerName,
|
|
224
|
+
port: targetPort,
|
|
225
|
+
hostPort: boundHostPort,
|
|
226
|
+
bindAddr,
|
|
227
|
+
})
|
|
228
|
+
if (ws) ws.send({ type: 'port-forward-result', port: targetPort, ok: true, hostPort: boundHostPort })
|
|
179
229
|
} catch (err) {
|
|
180
230
|
const reason = err instanceof Error ? err.message : String(err)
|
|
181
|
-
log(`forward bind ${
|
|
182
|
-
emit({ kind: 'port-forward-failed', containerName: opts.containerName, port, reason })
|
|
183
|
-
if (ws) ws.send({ type: 'port-forward-result', port, ok: false, reason })
|
|
231
|
+
log(`forward bind ${targetPort}: ${reason}`)
|
|
232
|
+
emit({ kind: 'port-forward-failed', containerName: opts.containerName, port: targetPort, reason })
|
|
233
|
+
if (ws) ws.send({ type: 'port-forward-result', port: targetPort, ok: false, reason })
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const installReservedForwarder = async (targetPort: number, hostCandidates: number[]): Promise<void> => {
|
|
238
|
+
const existingHostPort = reservedTargets.get(targetPort)
|
|
239
|
+
if (existingHostPort !== undefined) {
|
|
240
|
+
if (ws) ws.send({ type: 'port-forward-result', port: targetPort, ok: true, hostPort: existingHostPort })
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
if (!shouldForward({ policy: opts.policy, port: targetPort })) {
|
|
244
|
+
if (ws) ws.send({ type: 'port-forward-result', port: targetPort, ok: false, reason: 'policy excluded' })
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Claim the target and evict any auto-forward that already won the race
|
|
249
|
+
// before the reserved bind below yields control to the event loop.
|
|
250
|
+
pendingReservedTargets.add(targetPort)
|
|
251
|
+
removeAutoForwarderForTarget(targetPort, 'container-released')
|
|
252
|
+
|
|
253
|
+
let lastReason = 'no host candidates'
|
|
254
|
+
for (const hostPort of hostCandidates) {
|
|
255
|
+
try {
|
|
256
|
+
// `port` stays the in-container target; the host listen port is the
|
|
257
|
+
// forwarders map key the connection callback must route by. Reserved
|
|
258
|
+
// forwards deliberately allow the two to differ, so route by the actual
|
|
259
|
+
// bound port, captured after the await.
|
|
260
|
+
let boundHostPort: number | undefined
|
|
261
|
+
const listener = await listenHost(hostBind, hostPort, {
|
|
262
|
+
onConnection: (sock) => {
|
|
263
|
+
if (boundHostPort === undefined) {
|
|
264
|
+
try {
|
|
265
|
+
sock.end()
|
|
266
|
+
} catch {}
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
handleHostConnection(boundHostPort, sock)
|
|
270
|
+
},
|
|
271
|
+
})
|
|
272
|
+
boundHostPort = listener.port
|
|
273
|
+
forwarders.set(boundHostPort, {
|
|
274
|
+
targetPort,
|
|
275
|
+
hostPort: boundHostPort,
|
|
276
|
+
bindAddr: '127.0.0.1',
|
|
277
|
+
reserved: true,
|
|
278
|
+
listener,
|
|
279
|
+
streams: new Map(),
|
|
280
|
+
})
|
|
281
|
+
reservedTargets.set(targetPort, boundHostPort)
|
|
282
|
+
pendingReservedTargets.delete(targetPort)
|
|
283
|
+
emit({
|
|
284
|
+
kind: 'port-forward-opened',
|
|
285
|
+
containerName: opts.containerName,
|
|
286
|
+
port: targetPort,
|
|
287
|
+
hostPort: boundHostPort,
|
|
288
|
+
bindAddr: '127.0.0.1',
|
|
289
|
+
})
|
|
290
|
+
if (ws) ws.send({ type: 'port-forward-result', port: targetPort, ok: true, hostPort: boundHostPort })
|
|
291
|
+
return
|
|
292
|
+
} catch (err) {
|
|
293
|
+
lastReason = err instanceof Error ? err.message : String(err)
|
|
294
|
+
log(`reserved forward bind ${hostPort} for ${targetPort}: ${lastReason}`)
|
|
295
|
+
}
|
|
184
296
|
}
|
|
297
|
+
pendingReservedTargets.delete(targetPort)
|
|
298
|
+
emit({ kind: 'port-forward-failed', containerName: opts.containerName, port: targetPort, reason: lastReason })
|
|
299
|
+
if (ws) ws.send({ type: 'port-forward-result', port: targetPort, ok: false, reason: lastReason })
|
|
185
300
|
}
|
|
186
301
|
|
|
187
|
-
const removeForwarder = (
|
|
188
|
-
const fwd = forwarders.get(
|
|
302
|
+
const removeForwarder = (hostPort: number, reason: 'container-released' | 'host-error'): void => {
|
|
303
|
+
const fwd = forwarders.get(hostPort)
|
|
189
304
|
if (!fwd) return
|
|
190
|
-
forwarders.delete(
|
|
305
|
+
forwarders.delete(hostPort)
|
|
306
|
+
if (fwd.reserved) reservedTargets.delete(fwd.targetPort)
|
|
191
307
|
try {
|
|
192
308
|
fwd.listener.stop()
|
|
193
309
|
} catch {}
|
|
194
|
-
for (const [streamId] of fwd.streams) closeStream(
|
|
195
|
-
emit({
|
|
310
|
+
for (const [streamId] of fwd.streams) closeStream(hostPort, streamId, false)
|
|
311
|
+
emit({
|
|
312
|
+
kind: 'port-forward-closed',
|
|
313
|
+
containerName: opts.containerName,
|
|
314
|
+
port: fwd.targetPort,
|
|
315
|
+
hostPort: fwd.hostPort,
|
|
316
|
+
reason,
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const removeAutoForwarderForTarget = (targetPort: number, reason: 'container-released' | 'host-error'): void => {
|
|
321
|
+
for (const [hostPort, fwd] of Array.from(forwarders)) {
|
|
322
|
+
if (!fwd.reserved && fwd.targetPort === targetPort) removeForwarder(hostPort, reason)
|
|
323
|
+
}
|
|
196
324
|
}
|
|
197
325
|
|
|
198
326
|
const teardownAllForwarders = (reason: 'broker-stopped' | 'deregistered' | 'host-error'): void => {
|
|
199
|
-
for (const
|
|
200
|
-
const fwd = forwarders.get(
|
|
327
|
+
for (const hostPort of Array.from(forwarders.keys())) {
|
|
328
|
+
const fwd = forwarders.get(hostPort)
|
|
201
329
|
if (!fwd) continue
|
|
202
|
-
forwarders.delete(
|
|
330
|
+
forwarders.delete(hostPort)
|
|
331
|
+
if (fwd.reserved) reservedTargets.delete(fwd.targetPort)
|
|
203
332
|
try {
|
|
204
333
|
fwd.listener.stop()
|
|
205
334
|
} catch {}
|
|
@@ -209,7 +338,19 @@ export function createBroker(opts: BrokerOptions): Broker {
|
|
|
209
338
|
stream?.sock.end()
|
|
210
339
|
} catch {}
|
|
211
340
|
}
|
|
212
|
-
emit({
|
|
341
|
+
emit({
|
|
342
|
+
kind: 'port-forward-closed',
|
|
343
|
+
containerName: opts.containerName,
|
|
344
|
+
port: fwd.targetPort,
|
|
345
|
+
hostPort: fwd.hostPort,
|
|
346
|
+
reason,
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const teardownAutoForwarders = (reason: 'host-error'): void => {
|
|
352
|
+
for (const [hostPort, fwd] of Array.from(forwarders)) {
|
|
353
|
+
if (!fwd.reserved) removeForwarder(hostPort, reason)
|
|
213
354
|
}
|
|
214
355
|
}
|
|
215
356
|
|
|
@@ -228,14 +369,19 @@ export function createBroker(opts: BrokerOptions): Broker {
|
|
|
228
369
|
return
|
|
229
370
|
case 'port-listen-snapshot':
|
|
230
371
|
for (const { port, bindAddr } of msg.ports) {
|
|
231
|
-
void installForwarder(port, bindAddr)
|
|
372
|
+
if (!isReservedTarget(port)) void installForwarder(port, bindAddr)
|
|
232
373
|
}
|
|
233
374
|
return
|
|
234
375
|
case 'port-listen-opened':
|
|
376
|
+
if (isReservedTarget(msg.port)) return
|
|
235
377
|
void installForwarder(msg.port, msg.bindAddr)
|
|
236
378
|
return
|
|
237
379
|
case 'port-listen-closed':
|
|
238
|
-
|
|
380
|
+
if (isReservedTarget(msg.port)) return
|
|
381
|
+
removeAutoForwarderForTarget(msg.port, 'container-released')
|
|
382
|
+
return
|
|
383
|
+
case 'port-forward-request':
|
|
384
|
+
void installReservedForwarder(msg.targetPort, msg.hostCandidates)
|
|
239
385
|
return
|
|
240
386
|
case 'relay-open-ack': {
|
|
241
387
|
const port = findStreamPort(msg.streamId)
|
|
@@ -303,7 +449,7 @@ export function createBroker(opts: BrokerOptions): Broker {
|
|
|
303
449
|
client.onMessage(handleContainerMessage)
|
|
304
450
|
client.onClose(() => {
|
|
305
451
|
ws = null
|
|
306
|
-
|
|
452
|
+
teardownAutoForwarders('host-error')
|
|
307
453
|
scheduleReconnect()
|
|
308
454
|
})
|
|
309
455
|
|
|
@@ -404,7 +550,7 @@ async function defaultConnectWs(url: string): Promise<WsClient> {
|
|
|
404
550
|
})
|
|
405
551
|
}
|
|
406
552
|
|
|
407
|
-
function defaultListenHost(
|
|
553
|
+
export function defaultListenHost(
|
|
408
554
|
host: string,
|
|
409
555
|
port: number,
|
|
410
556
|
handlers: { onConnection: (sock: HostSocket) => void },
|
package/src/portbroker/index.ts
CHANGED
|
@@ -20,8 +20,13 @@ export {
|
|
|
20
20
|
type UpstreamConnection,
|
|
21
21
|
type UpstreamHandlers,
|
|
22
22
|
} from './container-server'
|
|
23
|
+
export {
|
|
24
|
+
__resetForwardRequestBus,
|
|
25
|
+
publishForwardRequest,
|
|
26
|
+
subscribeForwardRequest,
|
|
27
|
+
type ForwardRequestEvent,
|
|
28
|
+
} from './forward-request-bus'
|
|
23
29
|
export { __resetForwardResultBus, publishForwardResult, subscribeForwardResult } from './forward-result-bus'
|
|
24
|
-
export { bindWithForward, type BindFactory, type BindResult, type BindWithForwardOptions } from './bind-with-forward'
|
|
25
30
|
export {
|
|
26
31
|
createBroker,
|
|
27
32
|
type Broker,
|
|
@@ -15,6 +15,7 @@ export type HostdToContainer =
|
|
|
15
15
|
export type ContainerToHostd =
|
|
16
16
|
| { type: 'broker-hello-ack' }
|
|
17
17
|
| { type: 'broker-hello-nack'; reason: string }
|
|
18
|
+
| { type: 'port-forward-request'; targetPort: number; hostCandidates: number[]; reason?: string }
|
|
18
19
|
| { type: 'port-listen-snapshot'; ports: Array<{ port: number; bindAddr: BindAddr }> }
|
|
19
20
|
| { type: 'port-listen-opened'; port: number; bindAddr: BindAddr }
|
|
20
21
|
| { type: 'port-listen-closed'; port: number }
|
|
@@ -24,8 +25,14 @@ export type ContainerToHostd =
|
|
|
24
25
|
| { type: 'relay-close'; streamId: StreamId; side: 'upstream' | 'downstream' }
|
|
25
26
|
|
|
26
27
|
export type PortForwardEvent =
|
|
27
|
-
| { kind: 'port-forward-opened'; containerName: string; port: number; bindAddr: BindAddr }
|
|
28
|
-
| {
|
|
28
|
+
| { kind: 'port-forward-opened'; containerName: string; port: number; hostPort: number; bindAddr: BindAddr }
|
|
29
|
+
| {
|
|
30
|
+
kind: 'port-forward-closed'
|
|
31
|
+
containerName: string
|
|
32
|
+
port: number
|
|
33
|
+
hostPort: number
|
|
34
|
+
reason: PortForwardCloseReason
|
|
35
|
+
}
|
|
29
36
|
| { kind: 'port-forward-failed'; containerName: string; port: number; reason: string }
|
|
30
37
|
|
|
31
38
|
export type PortForwardCloseReason = 'container-released' | 'host-error' | 'deregistered' | 'broker-stopped'
|
|
@@ -3,6 +3,7 @@ import { SessionManager } from '@mariozechner/pi-coding-agent'
|
|
|
3
3
|
import { createSession as defaultCreateSession } from '@/agent'
|
|
4
4
|
import type { LiveSessionRegistry } from '@/agent/live-sessions'
|
|
5
5
|
import type { LiveSubagentRegistry } from '@/agent/live-subagents'
|
|
6
|
+
import { sessionMetaPayload } from '@/agent/session-meta'
|
|
6
7
|
import type { CreateSessionForSubagent, SubagentRegistry } from '@/agent/subagents'
|
|
7
8
|
import { capJsonlFileInPlace } from '@/bundled-plugins/tool-result-cap/cap-jsonl'
|
|
8
9
|
import type { CapOptions } from '@/bundled-plugins/tool-result-cap/cap-result'
|
|
@@ -70,6 +71,9 @@ export type BuildChannelSessionFactoryDeps = {
|
|
|
70
71
|
subagentRegistry?: SubagentRegistry
|
|
71
72
|
getCreateSessionForSubagent?: () => CreateSessionForSubagent
|
|
72
73
|
liveSessionRegistry?: LiveSessionRegistry
|
|
74
|
+
// Forwarded to createSession: when true the `# Memory` section is omitted
|
|
75
|
+
// from the system prompt (vector agents inject memory per-turn instead).
|
|
76
|
+
suppressSystemMemory?: boolean
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
// Tight basename validation so a tampered or corrupt channels/sessions.json
|
|
@@ -137,10 +141,16 @@ export function buildChannelSessionFactory(deps: BuildChannelSessionFactoryDeps)
|
|
|
137
141
|
...(deps.getCreateSessionForSubagent !== undefined
|
|
138
142
|
? { createSessionForSubagent: deps.getCreateSessionForSubagent() }
|
|
139
143
|
: {}),
|
|
144
|
+
...(deps.suppressSystemMemory !== undefined ? { suppressSystemMemory: deps.suppressSystemMemory } : {}),
|
|
140
145
|
})
|
|
141
146
|
|
|
142
147
|
const sessionId = sessionManager.getSessionId()
|
|
143
|
-
deps.liveSessionRegistry?.register({
|
|
148
|
+
deps.liveSessionRegistry?.register({
|
|
149
|
+
sessionId,
|
|
150
|
+
session,
|
|
151
|
+
origin: sessionMetaPayload(origin).origin,
|
|
152
|
+
registeredAtMs: Date.now(),
|
|
153
|
+
})
|
|
144
154
|
|
|
145
155
|
return {
|
|
146
156
|
session,
|
package/src/run/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { LiveSessionRegistry } from '@/agent/live-sessions'
|
|
|
5
5
|
import { LiveSubagentRegistry } from '@/agent/live-subagents'
|
|
6
6
|
import { requestContainerRestart } from '@/agent/restart'
|
|
7
7
|
import { consumeRestartHandoff } from '@/agent/restart-handoff'
|
|
8
|
+
import { sessionMetaPayload } from '@/agent/session-meta'
|
|
8
9
|
import type { SessionOrigin } from '@/agent/session-origin'
|
|
9
10
|
import {
|
|
10
11
|
awaitWithSubagentTimeout,
|
|
@@ -18,6 +19,9 @@ import {
|
|
|
18
19
|
type SubagentShared,
|
|
19
20
|
} from '@/agent/subagents'
|
|
20
21
|
import { clearTodosForOrigin } from '@/agent/todo/continuation-wiring'
|
|
22
|
+
import { vectorEnabledFromMemoryConfig } from '@/bundled-plugins/memory/vector/config'
|
|
23
|
+
import { embed, warmEmbedder } from '@/bundled-plugins/memory/vector/embedder'
|
|
24
|
+
import { buildStartupVectorIndex } from '@/bundled-plugins/memory/vector/startup'
|
|
21
25
|
import { resolveCapOptionsFromConfig } from '@/bundled-plugins/tool-result-cap'
|
|
22
26
|
import {
|
|
23
27
|
createChannelManager,
|
|
@@ -55,7 +59,7 @@ import { runStartupMigrations } from '@/migrations'
|
|
|
55
59
|
import { loadPlugins, type LoadPluginsResult, pluginCronJobs, type PluginRegistry, summarizeLoaded } from '@/plugin'
|
|
56
60
|
import { createPluginLogger } from '@/plugin/context'
|
|
57
61
|
import type { CronHandlerContext } from '@/plugin/types'
|
|
58
|
-
import { createContainerBroker, publishForwardResult } from '@/portbroker'
|
|
62
|
+
import { createContainerBroker, publishForwardResult, subscribeForwardRequest } from '@/portbroker'
|
|
59
63
|
import { formatChannelReloadSummary, ReloadRegistry } from '@/reload'
|
|
60
64
|
import { createClaimController } from '@/role-claim'
|
|
61
65
|
import {
|
|
@@ -157,6 +161,10 @@ export async function startAgent({
|
|
|
157
161
|
const tuiTokenOpt = tuiToken !== undefined && tuiToken !== '' ? { tuiToken } : {}
|
|
158
162
|
|
|
159
163
|
const pluginConfigsByName = loadPluginConfigsSync(cwd)
|
|
164
|
+
// Vector agents omit the system-prompt `# Memory` section and inject memory
|
|
165
|
+
// per-turn instead. Derived once here: `memory.vector.enabled` is
|
|
166
|
+
// restart-required, so a single boot read is coherent for the process.
|
|
167
|
+
const suppressSystemMemory = vectorEnabledFromMemoryConfig(pluginConfigsByName.memory)
|
|
160
168
|
const cwdConfig = loadConfigSync(cwd)
|
|
161
169
|
const githubTokenBridge = createGithubTokenBridge()
|
|
162
170
|
const mcpManager =
|
|
@@ -215,6 +223,19 @@ export async function startAgent({
|
|
|
215
223
|
// v2-only parser rejects the file and hydrate below sees no channels. Runs
|
|
216
224
|
// exactly once per folder; a folder already at v2 is a no-op.
|
|
217
225
|
runStartupMigrations(cwd)
|
|
226
|
+
if (suppressSystemMemory) {
|
|
227
|
+
await buildStartupVectorIndex(cwd, embed).catch((err) => {
|
|
228
|
+
console.warn(`[vector] startup index build failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// Warm the embedder now (even when the index needed no rebuild above, which
|
|
232
|
+
// skips embed() entirely) so the first channel turn's query embed doesn't
|
|
233
|
+
// pay the one-time ONNX init on its critical path. Non-fatal: a failure here
|
|
234
|
+
// degrades to the per-turn lazy load, same as before this step existed.
|
|
235
|
+
await warmEmbedder().catch((err) => {
|
|
236
|
+
console.warn(`[vector] embedder warm-up failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
237
|
+
})
|
|
238
|
+
}
|
|
218
239
|
|
|
219
240
|
// Channel adapters read `process.env[TOKEN_ENV]` (see channels/manager.ts).
|
|
220
241
|
// Hydrate fills any unset env var from secrets.json#channels via env-wins:
|
|
@@ -280,6 +301,7 @@ export async function startAgent({
|
|
|
280
301
|
stream,
|
|
281
302
|
reloadRegistry,
|
|
282
303
|
pluginRuntime,
|
|
304
|
+
suppressSystemMemory,
|
|
283
305
|
getChannelRouter: () => channelManager.router,
|
|
284
306
|
rehydrateCapOptions: resolveCapOptionsFromConfig(pluginConfigsByName['tool-result-cap']),
|
|
285
307
|
permissions: pluginsLoaded.permissions,
|
|
@@ -394,7 +416,12 @@ export async function startAgent({
|
|
|
394
416
|
...(entry.pluginSubagent.bashPolicy !== undefined ? { bashPolicy: entry.pluginSubagent.bashPolicy } : {}),
|
|
395
417
|
...runtimeVersionOpt,
|
|
396
418
|
})
|
|
397
|
-
liveSessionRegistry.register({
|
|
419
|
+
liveSessionRegistry.register({
|
|
420
|
+
sessionId,
|
|
421
|
+
session: created.session,
|
|
422
|
+
origin: sessionMetaPayload(origin).origin,
|
|
423
|
+
registeredAtMs: Date.now(),
|
|
424
|
+
})
|
|
398
425
|
const originalDispose = created.dispose
|
|
399
426
|
return {
|
|
400
427
|
...created,
|
|
@@ -485,6 +512,7 @@ export async function startAgent({
|
|
|
485
512
|
containerName: containerNameOpt.containerName,
|
|
486
513
|
sessionFactory,
|
|
487
514
|
channelRouter: channelManager.router,
|
|
515
|
+
suppressSystemMemory,
|
|
488
516
|
...mcpManagerOpt,
|
|
489
517
|
}),
|
|
490
518
|
subagent: (subName: string, payload?: unknown) =>
|
|
@@ -525,6 +553,7 @@ export async function startAgent({
|
|
|
525
553
|
channelRouter: channelManager.router,
|
|
526
554
|
origin: cronOrigin,
|
|
527
555
|
permissions: pluginsLoaded.permissions,
|
|
556
|
+
suppressSystemMemory,
|
|
528
557
|
...(refOverride !== undefined ? { refOverride } : {}),
|
|
529
558
|
...(snap.hasAnyPluginContent
|
|
530
559
|
? {
|
|
@@ -543,7 +572,12 @@ export async function startAgent({
|
|
|
543
572
|
...runtimeVersionOpt,
|
|
544
573
|
...mcpManagerOpt,
|
|
545
574
|
})
|
|
546
|
-
liveSessionRegistry.register({
|
|
575
|
+
liveSessionRegistry.register({
|
|
576
|
+
sessionId,
|
|
577
|
+
session,
|
|
578
|
+
origin: sessionMetaPayload(cronOrigin).origin,
|
|
579
|
+
registeredAtMs: Date.now(),
|
|
580
|
+
})
|
|
547
581
|
return {
|
|
548
582
|
prompt: (text) => session.prompt(text),
|
|
549
583
|
dispose: () => {
|
|
@@ -729,11 +763,10 @@ export async function startAgent({
|
|
|
729
763
|
payload: { kind: 'portbroker-log', event },
|
|
730
764
|
})
|
|
731
765
|
},
|
|
732
|
-
// Re-publish to
|
|
733
|
-
//
|
|
734
|
-
// without holding a reference to the broker. See src/portbroker/
|
|
735
|
-
// forward-result-bus.ts for the contract.
|
|
766
|
+
// Re-publish to in-process buses so plugin code can talk to the
|
|
767
|
+
// broker without holding a ContainerBroker reference.
|
|
736
768
|
onForwardResult: (event) => publishForwardResult(event),
|
|
769
|
+
onForwardRequestSubscribe: (cb) => subscribeForwardRequest(cb),
|
|
737
770
|
})
|
|
738
771
|
: undefined
|
|
739
772
|
const containerBrokerOpt = containerBroker ? { containerBroker } : {}
|
|
@@ -749,6 +782,7 @@ export async function startAgent({
|
|
|
749
782
|
outbound,
|
|
750
783
|
sessionFactory,
|
|
751
784
|
channelRouter: channelManager.router,
|
|
785
|
+
suppressSystemMemory,
|
|
752
786
|
...mcpManagerOpt,
|
|
753
787
|
})
|
|
754
788
|
|