typeclaw 0.36.8 → 0.37.1
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 +3 -3
- 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 +30 -3
- 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 +166 -18
- package/src/cli/inspect.ts +11 -5
- package/src/cli/model.ts +115 -36
- package/src/cli/provider.ts +5 -3
- 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 +65 -8
- 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,
|
|
@@ -412,7 +439,30 @@ export async function startAgent({
|
|
|
412
439
|
: {}),
|
|
413
440
|
}
|
|
414
441
|
}
|
|
415
|
-
|
|
442
|
+
// Non-plugin (built-in) subagents — general/explore/scout/memory-logger/
|
|
443
|
+
// dreaming and anything spawned through the generic task path. They used to
|
|
444
|
+
// run with NO plugin tool.before/tool.after coverage, so their bash skipped
|
|
445
|
+
// the security guards AND the github-cli-auth GitHub-token injection — a
|
|
446
|
+
// generic subagent's `git push` got no minted token and died with "could
|
|
447
|
+
// not read Username" even when a GitHub App was configured. Thread the same
|
|
448
|
+
// hook bus the plugin-subagent branch uses, against a freshly allocated
|
|
449
|
+
// subagent session id (never the parent's, so hooks/audit/permission
|
|
450
|
+
// attribution stay per-session).
|
|
451
|
+
const sessionManager = SessionManager.create(cwd, sessionFactory.sessionDir())
|
|
452
|
+
return defaultCreateSessionForSubagent(subagent, {
|
|
453
|
+
...subagentOptions,
|
|
454
|
+
plugins: {
|
|
455
|
+
registry: snap.registry,
|
|
456
|
+
hooks: snap.hooks,
|
|
457
|
+
sessionId: sessionManager.getSessionId(),
|
|
458
|
+
agentDir: cwd,
|
|
459
|
+
},
|
|
460
|
+
// Pass permissions alongside plugins (same as the plugin-subagent branch
|
|
461
|
+
// at line 384): without it the builtin-bash sandbox (applyBashSandbox /
|
|
462
|
+
// applyTmpPathRedirect) stays off and the subagent would get the injected
|
|
463
|
+
// token but no role-derived sandboxing.
|
|
464
|
+
permissions: pluginsLoaded.permissions,
|
|
465
|
+
})
|
|
416
466
|
}
|
|
417
467
|
|
|
418
468
|
const subagentConsumer = createSubagentConsumer({
|
|
@@ -485,6 +535,7 @@ export async function startAgent({
|
|
|
485
535
|
containerName: containerNameOpt.containerName,
|
|
486
536
|
sessionFactory,
|
|
487
537
|
channelRouter: channelManager.router,
|
|
538
|
+
suppressSystemMemory,
|
|
488
539
|
...mcpManagerOpt,
|
|
489
540
|
}),
|
|
490
541
|
subagent: (subName: string, payload?: unknown) =>
|
|
@@ -525,6 +576,7 @@ export async function startAgent({
|
|
|
525
576
|
channelRouter: channelManager.router,
|
|
526
577
|
origin: cronOrigin,
|
|
527
578
|
permissions: pluginsLoaded.permissions,
|
|
579
|
+
suppressSystemMemory,
|
|
528
580
|
...(refOverride !== undefined ? { refOverride } : {}),
|
|
529
581
|
...(snap.hasAnyPluginContent
|
|
530
582
|
? {
|
|
@@ -543,7 +595,12 @@ export async function startAgent({
|
|
|
543
595
|
...runtimeVersionOpt,
|
|
544
596
|
...mcpManagerOpt,
|
|
545
597
|
})
|
|
546
|
-
liveSessionRegistry.register({
|
|
598
|
+
liveSessionRegistry.register({
|
|
599
|
+
sessionId,
|
|
600
|
+
session,
|
|
601
|
+
origin: sessionMetaPayload(cronOrigin).origin,
|
|
602
|
+
registeredAtMs: Date.now(),
|
|
603
|
+
})
|
|
547
604
|
return {
|
|
548
605
|
prompt: (text) => session.prompt(text),
|
|
549
606
|
dispose: () => {
|
|
@@ -729,11 +786,10 @@ export async function startAgent({
|
|
|
729
786
|
payload: { kind: 'portbroker-log', event },
|
|
730
787
|
})
|
|
731
788
|
},
|
|
732
|
-
// Re-publish to
|
|
733
|
-
//
|
|
734
|
-
// without holding a reference to the broker. See src/portbroker/
|
|
735
|
-
// forward-result-bus.ts for the contract.
|
|
789
|
+
// Re-publish to in-process buses so plugin code can talk to the
|
|
790
|
+
// broker without holding a ContainerBroker reference.
|
|
736
791
|
onForwardResult: (event) => publishForwardResult(event),
|
|
792
|
+
onForwardRequestSubscribe: (cb) => subscribeForwardRequest(cb),
|
|
737
793
|
})
|
|
738
794
|
: undefined
|
|
739
795
|
const containerBrokerOpt = containerBroker ? { containerBroker } : {}
|
|
@@ -749,6 +805,7 @@ export async function startAgent({
|
|
|
749
805
|
outbound,
|
|
750
806
|
sessionFactory,
|
|
751
807
|
channelRouter: channelManager.router,
|
|
808
|
+
suppressSystemMemory,
|
|
752
809
|
...mcpManagerOpt,
|
|
753
810
|
})
|
|
754
811
|
|