@0xmaxma/claude-gateway 1.8.2 → 1.8.3
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 +7 -2
- package/config.template.json +4 -0
- package/dist/agent/dreaming/config.d.ts +6 -0
- package/dist/agent/dreaming/config.d.ts.map +1 -1
- package/dist/agent/dreaming/config.js +17 -24
- package/dist/agent/dreaming/config.js.map +1 -1
- package/dist/agent/knowledge/config.d.ts.map +1 -1
- package/dist/agent/knowledge/config.js +7 -13
- package/dist/agent/knowledge/config.js.map +1 -1
- package/dist/agent/runner.d.ts +87 -13
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +342 -117
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/turn-stream.d.ts +202 -0
- package/dist/agent/turn-stream.d.ts.map +1 -0
- package/dist/agent/turn-stream.js +322 -0
- package/dist/agent/turn-stream.js.map +1 -0
- package/dist/api/apps-router.d.ts.map +1 -1
- package/dist/api/apps-router.js +14 -2
- package/dist/api/apps-router.js.map +1 -1
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +2 -3
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/line-webhook-router.d.ts +43 -1
- package/dist/api/line-webhook-router.d.ts.map +1 -1
- package/dist/api/line-webhook-router.js +286 -38
- package/dist/api/line-webhook-router.js.map +1 -1
- package/dist/api/router.d.ts +7 -1
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +328 -114
- package/dist/api/router.js.map +1 -1
- package/dist/apps/agent-manager.d.ts.map +1 -1
- package/dist/apps/agent-manager.js +4 -1
- package/dist/apps/agent-manager.js.map +1 -1
- package/dist/apps/installer.d.ts +75 -2
- package/dist/apps/installer.d.ts.map +1 -1
- package/dist/apps/installer.js +198 -13
- package/dist/apps/installer.js.map +1 -1
- package/dist/config/agent-env.d.ts +41 -0
- package/dist/config/agent-env.d.ts.map +1 -0
- package/dist/config/agent-env.js +154 -0
- package/dist/config/agent-env.js.map +1 -0
- package/dist/config/loader.d.ts +33 -2
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +33 -9
- package/dist/config/loader.js.map +1 -1
- package/dist/config/watcher.d.ts +1 -0
- package/dist/config/watcher.d.ts.map +1 -1
- package/dist/config/watcher.js +17 -1
- package/dist/config/watcher.js.map +1 -1
- package/dist/index.js +24 -51
- package/dist/index.js.map +1 -1
- package/dist/load-dotenv.d.ts +17 -0
- package/dist/load-dotenv.d.ts.map +1 -1
- package/dist/load-dotenv.js +32 -9
- package/dist/load-dotenv.js.map +1 -1
- package/dist/session/store.d.ts +9 -0
- package/dist/session/store.d.ts.map +1 -1
- package/dist/session/store.js +12 -0
- package/dist/session/store.js.map +1 -1
- package/dist/shell/bypass-dialog.d.ts +142 -0
- package/dist/shell/bypass-dialog.d.ts.map +1 -0
- package/dist/shell/bypass-dialog.js +207 -0
- package/dist/shell/bypass-dialog.js.map +1 -0
- package/dist/shell/claude-pty-shell.js +42 -3
- package/dist/shell/claude-pty-shell.js.map +1 -1
- package/dist/shell/screen.d.ts +7 -0
- package/dist/shell/screen.d.ts.map +1 -1
- package/dist/shell/screen.js +10 -1
- package/dist/shell/screen.js.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/config-num.d.ts +22 -0
- package/dist/utils/config-num.d.ts.map +1 -0
- package/dist/utils/config-num.js +34 -0
- package/dist/utils/config-num.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resumable turn streams (#421).
|
|
3
|
+
*
|
|
4
|
+
* A streamed turn used to be bound to exactly one HTTP request: every event was
|
|
5
|
+
* written straight to `res` behind an `if (!clientGone)` guard, so a client
|
|
6
|
+
* disconnect (or the soft timeout, which set the same flag) discarded the rest
|
|
7
|
+
* of the turn even though it kept running server-side and landed in history.
|
|
8
|
+
*
|
|
9
|
+
* A TurnStream decouples the two: the producer always records into an ordered,
|
|
10
|
+
* sequence-numbered buffer, and a *sink* — the thing that writes to a socket —
|
|
11
|
+
* is attached, detached and re-attached independently. A new SSE connection can
|
|
12
|
+
* therefore replay the tail from a cursor and then keep receiving live events,
|
|
13
|
+
* with no gap and no duplicate at the seam (attach() replays and installs in one
|
|
14
|
+
* synchronous block, so no event can slip between the two).
|
|
15
|
+
*/
|
|
16
|
+
import { ApiAttachment, StreamEvent } from '../types';
|
|
17
|
+
/** Max events retained per turn before the oldest are evicted. */
|
|
18
|
+
export declare const TURN_BUFFER_MAX_EVENTS = 2000;
|
|
19
|
+
/** Max approximate payload bytes retained per turn (heavy tool output). */
|
|
20
|
+
export declare const TURN_BUFFER_MAX_BYTES: number;
|
|
21
|
+
/** How long a completed turn stays replayable after its terminal frame. */
|
|
22
|
+
export declare const TURN_REPLAY_GRACE_MS = 120000;
|
|
23
|
+
/** A buffered event plus its per-turn sequence number. */
|
|
24
|
+
export interface SeqEvent {
|
|
25
|
+
seq: number;
|
|
26
|
+
event: StreamEvent;
|
|
27
|
+
/**
|
|
28
|
+
* Terminal `error` frames only: the original Error, so an in-process sink can
|
|
29
|
+
* hand callers the real object (with its `code`) rather than a re-wrapped
|
|
30
|
+
* message. SSE sinks ignore it and serialise `event` alone.
|
|
31
|
+
*/
|
|
32
|
+
error?: Error;
|
|
33
|
+
}
|
|
34
|
+
/** Whatever is currently writing a turn's events out (an SSE response, a test spy). */
|
|
35
|
+
export interface TurnSink {
|
|
36
|
+
/** A non-terminal event. */
|
|
37
|
+
write(e: SeqEvent): void;
|
|
38
|
+
/** The terminal `result` / `error` frame. The sink should close after this. */
|
|
39
|
+
finish(e: SeqEvent): void;
|
|
40
|
+
/**
|
|
41
|
+
* Another connection took over this turn. Optional: sinks that own a socket
|
|
42
|
+
* should close it, otherwise the displaced connection hangs open with no
|
|
43
|
+
* terminal frame ever coming — it is no longer the one being written to.
|
|
44
|
+
*/
|
|
45
|
+
displaced?(): void;
|
|
46
|
+
}
|
|
47
|
+
export type AttachFailure = 'truncated' | 'ahead';
|
|
48
|
+
/**
|
|
49
|
+
* One in-flight (or recently completed) turn: its ordered event buffer, its
|
|
50
|
+
* terminal frame once known, and the sink currently draining it.
|
|
51
|
+
*/
|
|
52
|
+
export declare class TurnStream {
|
|
53
|
+
/** The registry key this turn is filed under — see turnStreamKey(). */
|
|
54
|
+
readonly key: string;
|
|
55
|
+
readonly requestId: string;
|
|
56
|
+
readonly startedAt: number;
|
|
57
|
+
private readonly events;
|
|
58
|
+
private nextSeq;
|
|
59
|
+
/**
|
|
60
|
+
* seq of the NEWEST event evicted so far — eviction walks the head forward, so
|
|
61
|
+
* this advances with each drop. Anything at or below it is unreplayable, which
|
|
62
|
+
* is exactly the comparison attach() makes.
|
|
63
|
+
*/
|
|
64
|
+
private evictedThroughSeq;
|
|
65
|
+
private bytes;
|
|
66
|
+
private terminalEvent;
|
|
67
|
+
private sink;
|
|
68
|
+
constructor(
|
|
69
|
+
/** The registry key this turn is filed under — see turnStreamKey(). */
|
|
70
|
+
key: string, requestId: string);
|
|
71
|
+
get isComplete(): boolean;
|
|
72
|
+
get lastSeq(): number;
|
|
73
|
+
/** Record a non-terminal event and, if a sink is attached, write it out. */
|
|
74
|
+
emit(event: StreamEvent): void;
|
|
75
|
+
/**
|
|
76
|
+
* Record the terminal frame (`result` or `error`), flush it to the sink and
|
|
77
|
+
* detach — the turn is over, and anything that attaches later replays instead.
|
|
78
|
+
*/
|
|
79
|
+
complete(event: StreamEvent, error?: Error): void;
|
|
80
|
+
/**
|
|
81
|
+
* Replay everything after `afterSeq`, then install `sink` as the live one.
|
|
82
|
+
* Deliberately synchronous end to end: an event produced during an `await`
|
|
83
|
+
* here would land in the buffer but miss the sink (gap) or arrive twice (dup).
|
|
84
|
+
*
|
|
85
|
+
* Returns `'truncated'` — installing nothing — when `afterSeq` names an event
|
|
86
|
+
* the buffer has already evicted: the caller asked to continue seamlessly from
|
|
87
|
+
* a point that no longer exists, and that promise cannot be honoured.
|
|
88
|
+
*
|
|
89
|
+
* `afterSeq === 0` is exempt. It claims to have seen nothing, so there is no
|
|
90
|
+
* seam to break, and refusing it is what turned the headline case — a reload
|
|
91
|
+
* during a long turn — into a dead end: history holds no assistant row while
|
|
92
|
+
* the turn is still running, so the client was sent somewhere with nothing in
|
|
93
|
+
* it. It gets whatever the buffer still holds instead; the first replayed
|
|
94
|
+
* frame's `seq` is > 1 exactly when older events were dropped, and the terminal
|
|
95
|
+
* `result` carries the turn's full text regardless.
|
|
96
|
+
*
|
|
97
|
+
* Returns `'ahead'` when `afterSeq` runs past the last event this turn has
|
|
98
|
+
* produced. Seq numbering restarts at 1 for every turn, so a client that
|
|
99
|
+
* reloads and replays a cursor held over from an earlier turn would otherwise
|
|
100
|
+
* attach successfully, match no event, and still be handed the terminal frame
|
|
101
|
+
* — the final answer with every delta silently missing.
|
|
102
|
+
*
|
|
103
|
+
* This catches a stale cursor only when the new turn is *shorter* than the old
|
|
104
|
+
* one; a cursor from turn N sits inside turn N+1 the moment N+1 has emitted
|
|
105
|
+
* that many events, and no seq comparison can tell that apart from a genuine
|
|
106
|
+
* resume. Identity is what separates them, which is why the resume endpoint
|
|
107
|
+
* requires `request_id` whenever `after_seq > 0`.
|
|
108
|
+
*/
|
|
109
|
+
attach(sink: TurnSink, afterSeq: number): AttachFailure | null;
|
|
110
|
+
/**
|
|
111
|
+
* Detach `sink` if it is still the live one. Passing the sink (rather than
|
|
112
|
+
* clearing unconditionally) keeps a stale `res.on('close')` from a superseded
|
|
113
|
+
* connection from silencing the sink that replaced it.
|
|
114
|
+
*/
|
|
115
|
+
detach(sink: TurnSink): void;
|
|
116
|
+
private evictIfOverCap;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Registry key for a turn. Every producer namespaces its session ids, because
|
|
120
|
+
* one registry serves them all: the API path files under `api`, a channel turn
|
|
121
|
+
* under its channel name. Without the prefix the two id spaces shared one
|
|
122
|
+
* keyspace and a collision would let one producer's turn evict or terminate the
|
|
123
|
+
* other's — a claim the old comments here asserted could not happen while
|
|
124
|
+
* complete()'s guard below existed precisely because it could.
|
|
125
|
+
*
|
|
126
|
+
* The chat id is deliberately NOT part of the key: an API session id is minted
|
|
127
|
+
* by the gateway inside a single `api-{chatId}` index and cannot be presented
|
|
128
|
+
* for another chat (see apiSessionExists), so the namespace prefix already makes
|
|
129
|
+
* every key unique.
|
|
130
|
+
*/
|
|
131
|
+
export declare function turnStreamKey(namespace: string, sessionId: string): string;
|
|
132
|
+
/**
|
|
133
|
+
* Per-session registry of turn streams, with the completed-turn grace window.
|
|
134
|
+
*
|
|
135
|
+
* Keyed by turnStreamKey(); each record carries its request id, so a client that
|
|
136
|
+
* re-attaches can assert it is resuming the turn it thinks it is.
|
|
137
|
+
*/
|
|
138
|
+
export declare class TurnStreamRegistry {
|
|
139
|
+
private readonly graceMs;
|
|
140
|
+
private readonly turns;
|
|
141
|
+
private readonly releaseTimers;
|
|
142
|
+
constructor(graceMs?: number);
|
|
143
|
+
/**
|
|
144
|
+
* Begin a new turn, replacing (and un-scheduling) any previous record for the
|
|
145
|
+
* key so a fresh turn never inherits the last one's buffer — the same reason
|
|
146
|
+
* `pendingApiAttachments` is cleared at the top of every turn.
|
|
147
|
+
*
|
|
148
|
+
* This also ends the previous turn's replay grace window early: a session
|
|
149
|
+
* holds at most one turn, so nothing of turn N survives once turn N+1 starts.
|
|
150
|
+
* A client that resumes turn N by name (`request_id`) therefore gets
|
|
151
|
+
* TURN_MISMATCH — not a replay of N, and not a replay of N+1 either. A client
|
|
152
|
+
* that resumes by cursor alone cannot be told apart from one legitimately
|
|
153
|
+
* following N+1, which is why the resume endpoint requires the request id
|
|
154
|
+
* alongside a non-zero cursor. Documented in API.md under "Resuming an
|
|
155
|
+
* interrupted stream"; history is the fallback.
|
|
156
|
+
*/
|
|
157
|
+
start(key: string, requestId: string): TurnStream;
|
|
158
|
+
get(key: string): TurnStream | undefined;
|
|
159
|
+
/**
|
|
160
|
+
* Record the terminal frame and keep the turn replayable for the grace window.
|
|
161
|
+
*
|
|
162
|
+
* Takes the TurnStream itself, not a key: a turn superseded by the next one on
|
|
163
|
+
* the same session finishes late often enough, and it must terminate its OWN
|
|
164
|
+
* record, never the one that replaced it.
|
|
165
|
+
*/
|
|
166
|
+
complete(turn: TurnStream, event: StreamEvent, error?: Error): void;
|
|
167
|
+
/**
|
|
168
|
+
* Terminal frame + immediate release, for a producer whose turns nothing can
|
|
169
|
+
* re-attach to: the cross-channel live view files under its channel namespace,
|
|
170
|
+
* and the resume endpoint only ever looks under `api`. Holding that buffer for
|
|
171
|
+
* the grace window would retain up to the full per-turn cap (2,000 events /
|
|
172
|
+
* ~4 MB) for two minutes with no code path able to replay a byte of it.
|
|
173
|
+
*
|
|
174
|
+
* Same superseded-turn guard as complete(): a late finisher releases its OWN
|
|
175
|
+
* record, never the one that replaced it.
|
|
176
|
+
*/
|
|
177
|
+
completeAndRelease(turn: TurnStream, event: StreamEvent, error?: Error): void;
|
|
178
|
+
/** Drop the record and its grace timer. */
|
|
179
|
+
release(key: string): void;
|
|
180
|
+
clear(): void;
|
|
181
|
+
}
|
|
182
|
+
/** Terminal frame for a turn that finished normally. */
|
|
183
|
+
export declare function resultEvent(text: string, attachments: ApiAttachment[]): StreamEvent;
|
|
184
|
+
/** Terminal frame for a turn that failed, carrying the Error's `code` when it has one. */
|
|
185
|
+
export declare function errorEvent(err: Error): StreamEvent;
|
|
186
|
+
/** The `code` property producers attach to their Errors, when it is a string. */
|
|
187
|
+
export declare function errorCode(err: unknown): string | undefined;
|
|
188
|
+
/**
|
|
189
|
+
* The callback shape every streaming producer in AgentRunner emits into. `seq`
|
|
190
|
+
* is the event's per-turn sequence number — the cursor a client passes back to
|
|
191
|
+
* `GET …/sessions/:sessionId/stream?after_seq=` to resume without a gap.
|
|
192
|
+
*/
|
|
193
|
+
export interface ApiStreamCallbacks {
|
|
194
|
+
onChunk: (event: StreamEvent, seq?: number) => void;
|
|
195
|
+
onDone: (fullText: string, attachments: ApiAttachment[], seq?: number) => void;
|
|
196
|
+
onError: (err: Error, seq?: number) => void;
|
|
197
|
+
/** Another connection resumed this turn; this one is no longer being written to. */
|
|
198
|
+
onDisplaced?: () => void;
|
|
199
|
+
}
|
|
200
|
+
/** Adapt a callback trio into a sink a TurnStream can drive. */
|
|
201
|
+
export declare function callbackSink(callbacks: ApiStreamCallbacks): TurnSink;
|
|
202
|
+
//# sourceMappingURL=turn-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-stream.d.ts","sourceRoot":"","sources":["../../src/agent/turn-stream.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TurnStreamRegistry = exports.TurnStream = exports.TURN_REPLAY_GRACE_MS = exports.TURN_BUFFER_MAX_BYTES = exports.TURN_BUFFER_MAX_EVENTS = void 0;
|
|
4
|
+
exports.turnStreamKey = turnStreamKey;
|
|
5
|
+
exports.resultEvent = resultEvent;
|
|
6
|
+
exports.errorEvent = errorEvent;
|
|
7
|
+
exports.errorCode = errorCode;
|
|
8
|
+
exports.callbackSink = callbackSink;
|
|
9
|
+
/** Max events retained per turn before the oldest are evicted. */
|
|
10
|
+
exports.TURN_BUFFER_MAX_EVENTS = 2000;
|
|
11
|
+
/** Max approximate payload bytes retained per turn (heavy tool output). */
|
|
12
|
+
exports.TURN_BUFFER_MAX_BYTES = 4 * 1024 * 1024;
|
|
13
|
+
/** How long a completed turn stays replayable after its terminal frame. */
|
|
14
|
+
exports.TURN_REPLAY_GRACE_MS = 120000;
|
|
15
|
+
/** Fraction of the cap evicted at once, so eviction is amortised O(1) per event. */
|
|
16
|
+
const EVICT_BATCH_RATIO = 0.1;
|
|
17
|
+
/**
|
|
18
|
+
* One in-flight (or recently completed) turn: its ordered event buffer, its
|
|
19
|
+
* terminal frame once known, and the sink currently draining it.
|
|
20
|
+
*/
|
|
21
|
+
class TurnStream {
|
|
22
|
+
constructor(
|
|
23
|
+
/** The registry key this turn is filed under — see turnStreamKey(). */
|
|
24
|
+
key, requestId) {
|
|
25
|
+
this.key = key;
|
|
26
|
+
this.requestId = requestId;
|
|
27
|
+
this.startedAt = Date.now();
|
|
28
|
+
this.events = [];
|
|
29
|
+
this.nextSeq = 1;
|
|
30
|
+
/**
|
|
31
|
+
* seq of the NEWEST event evicted so far — eviction walks the head forward, so
|
|
32
|
+
* this advances with each drop. Anything at or below it is unreplayable, which
|
|
33
|
+
* is exactly the comparison attach() makes.
|
|
34
|
+
*/
|
|
35
|
+
this.evictedThroughSeq = 0;
|
|
36
|
+
this.bytes = 0;
|
|
37
|
+
this.terminalEvent = null;
|
|
38
|
+
this.sink = null;
|
|
39
|
+
}
|
|
40
|
+
get isComplete() {
|
|
41
|
+
return this.terminalEvent !== null;
|
|
42
|
+
}
|
|
43
|
+
get lastSeq() {
|
|
44
|
+
return this.nextSeq - 1;
|
|
45
|
+
}
|
|
46
|
+
/** Record a non-terminal event and, if a sink is attached, write it out. */
|
|
47
|
+
emit(event) {
|
|
48
|
+
if (this.terminalEvent)
|
|
49
|
+
return; // nothing may follow the terminal frame
|
|
50
|
+
const e = { seq: this.nextSeq++, event, bytes: approxSize(event) };
|
|
51
|
+
this.events.push(e);
|
|
52
|
+
this.bytes += e.bytes;
|
|
53
|
+
this.evictIfOverCap();
|
|
54
|
+
this.sink?.write(e);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Record the terminal frame (`result` or `error`), flush it to the sink and
|
|
58
|
+
* detach — the turn is over, and anything that attaches later replays instead.
|
|
59
|
+
*/
|
|
60
|
+
complete(event, error) {
|
|
61
|
+
if (this.terminalEvent)
|
|
62
|
+
return;
|
|
63
|
+
const e = { seq: this.nextSeq++, event, ...(error ? { error } : {}) };
|
|
64
|
+
this.terminalEvent = e;
|
|
65
|
+
const sink = this.sink;
|
|
66
|
+
this.sink = null;
|
|
67
|
+
sink?.finish(e);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Replay everything after `afterSeq`, then install `sink` as the live one.
|
|
71
|
+
* Deliberately synchronous end to end: an event produced during an `await`
|
|
72
|
+
* here would land in the buffer but miss the sink (gap) or arrive twice (dup).
|
|
73
|
+
*
|
|
74
|
+
* Returns `'truncated'` — installing nothing — when `afterSeq` names an event
|
|
75
|
+
* the buffer has already evicted: the caller asked to continue seamlessly from
|
|
76
|
+
* a point that no longer exists, and that promise cannot be honoured.
|
|
77
|
+
*
|
|
78
|
+
* `afterSeq === 0` is exempt. It claims to have seen nothing, so there is no
|
|
79
|
+
* seam to break, and refusing it is what turned the headline case — a reload
|
|
80
|
+
* during a long turn — into a dead end: history holds no assistant row while
|
|
81
|
+
* the turn is still running, so the client was sent somewhere with nothing in
|
|
82
|
+
* it. It gets whatever the buffer still holds instead; the first replayed
|
|
83
|
+
* frame's `seq` is > 1 exactly when older events were dropped, and the terminal
|
|
84
|
+
* `result` carries the turn's full text regardless.
|
|
85
|
+
*
|
|
86
|
+
* Returns `'ahead'` when `afterSeq` runs past the last event this turn has
|
|
87
|
+
* produced. Seq numbering restarts at 1 for every turn, so a client that
|
|
88
|
+
* reloads and replays a cursor held over from an earlier turn would otherwise
|
|
89
|
+
* attach successfully, match no event, and still be handed the terminal frame
|
|
90
|
+
* — the final answer with every delta silently missing.
|
|
91
|
+
*
|
|
92
|
+
* This catches a stale cursor only when the new turn is *shorter* than the old
|
|
93
|
+
* one; a cursor from turn N sits inside turn N+1 the moment N+1 has emitted
|
|
94
|
+
* that many events, and no seq comparison can tell that apart from a genuine
|
|
95
|
+
* resume. Identity is what separates them, which is why the resume endpoint
|
|
96
|
+
* requires `request_id` whenever `after_seq > 0`.
|
|
97
|
+
*/
|
|
98
|
+
attach(sink, afterSeq) {
|
|
99
|
+
if (afterSeq > this.lastSeq)
|
|
100
|
+
return 'ahead';
|
|
101
|
+
if (afterSeq > 0 && afterSeq < this.evictedThroughSeq)
|
|
102
|
+
return 'truncated';
|
|
103
|
+
// Retire whoever held the slot before the replay, so the displaced socket
|
|
104
|
+
// closes instead of waiting forever for a terminal frame it will not get.
|
|
105
|
+
const previous = this.sink;
|
|
106
|
+
if (previous && previous !== sink) {
|
|
107
|
+
this.sink = null;
|
|
108
|
+
try {
|
|
109
|
+
previous.displaced?.();
|
|
110
|
+
}
|
|
111
|
+
catch { /* already gone */ }
|
|
112
|
+
}
|
|
113
|
+
for (const e of this.events) {
|
|
114
|
+
if (e.seq > afterSeq)
|
|
115
|
+
sink.write(e);
|
|
116
|
+
}
|
|
117
|
+
if (this.terminalEvent) {
|
|
118
|
+
sink.finish(this.terminalEvent);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
this.sink = sink;
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Detach `sink` if it is still the live one. Passing the sink (rather than
|
|
126
|
+
* clearing unconditionally) keeps a stale `res.on('close')` from a superseded
|
|
127
|
+
* connection from silencing the sink that replaced it.
|
|
128
|
+
*/
|
|
129
|
+
detach(sink) {
|
|
130
|
+
if (this.sink === sink)
|
|
131
|
+
this.sink = null;
|
|
132
|
+
}
|
|
133
|
+
evictIfOverCap() {
|
|
134
|
+
if (this.events.length <= exports.TURN_BUFFER_MAX_EVENTS && this.bytes <= exports.TURN_BUFFER_MAX_BYTES)
|
|
135
|
+
return;
|
|
136
|
+
const batch = Math.max(1, Math.floor(exports.TURN_BUFFER_MAX_EVENTS * EVICT_BATCH_RATIO));
|
|
137
|
+
while (this.events.length > 0 &&
|
|
138
|
+
(this.events.length > exports.TURN_BUFFER_MAX_EVENTS - batch || this.bytes > exports.TURN_BUFFER_MAX_BYTES)) {
|
|
139
|
+
const dropped = this.events.shift();
|
|
140
|
+
this.bytes -= dropped.bytes;
|
|
141
|
+
this.evictedThroughSeq = dropped.seq;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
exports.TurnStream = TurnStream;
|
|
146
|
+
/**
|
|
147
|
+
* Registry key for a turn. Every producer namespaces its session ids, because
|
|
148
|
+
* one registry serves them all: the API path files under `api`, a channel turn
|
|
149
|
+
* under its channel name. Without the prefix the two id spaces shared one
|
|
150
|
+
* keyspace and a collision would let one producer's turn evict or terminate the
|
|
151
|
+
* other's — a claim the old comments here asserted could not happen while
|
|
152
|
+
* complete()'s guard below existed precisely because it could.
|
|
153
|
+
*
|
|
154
|
+
* The chat id is deliberately NOT part of the key: an API session id is minted
|
|
155
|
+
* by the gateway inside a single `api-{chatId}` index and cannot be presented
|
|
156
|
+
* for another chat (see apiSessionExists), so the namespace prefix already makes
|
|
157
|
+
* every key unique.
|
|
158
|
+
*/
|
|
159
|
+
function turnStreamKey(namespace, sessionId) {
|
|
160
|
+
return `${namespace}:${sessionId}`;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Per-session registry of turn streams, with the completed-turn grace window.
|
|
164
|
+
*
|
|
165
|
+
* Keyed by turnStreamKey(); each record carries its request id, so a client that
|
|
166
|
+
* re-attaches can assert it is resuming the turn it thinks it is.
|
|
167
|
+
*/
|
|
168
|
+
class TurnStreamRegistry {
|
|
169
|
+
constructor(graceMs = exports.TURN_REPLAY_GRACE_MS) {
|
|
170
|
+
this.graceMs = graceMs;
|
|
171
|
+
this.turns = new Map();
|
|
172
|
+
this.releaseTimers = new Map();
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Begin a new turn, replacing (and un-scheduling) any previous record for the
|
|
176
|
+
* key so a fresh turn never inherits the last one's buffer — the same reason
|
|
177
|
+
* `pendingApiAttachments` is cleared at the top of every turn.
|
|
178
|
+
*
|
|
179
|
+
* This also ends the previous turn's replay grace window early: a session
|
|
180
|
+
* holds at most one turn, so nothing of turn N survives once turn N+1 starts.
|
|
181
|
+
* A client that resumes turn N by name (`request_id`) therefore gets
|
|
182
|
+
* TURN_MISMATCH — not a replay of N, and not a replay of N+1 either. A client
|
|
183
|
+
* that resumes by cursor alone cannot be told apart from one legitimately
|
|
184
|
+
* following N+1, which is why the resume endpoint requires the request id
|
|
185
|
+
* alongside a non-zero cursor. Documented in API.md under "Resuming an
|
|
186
|
+
* interrupted stream"; history is the fallback.
|
|
187
|
+
*/
|
|
188
|
+
start(key, requestId) {
|
|
189
|
+
this.release(key);
|
|
190
|
+
const turn = new TurnStream(key, requestId);
|
|
191
|
+
this.turns.set(key, turn);
|
|
192
|
+
return turn;
|
|
193
|
+
}
|
|
194
|
+
get(key) {
|
|
195
|
+
return this.turns.get(key);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Record the terminal frame and keep the turn replayable for the grace window.
|
|
199
|
+
*
|
|
200
|
+
* Takes the TurnStream itself, not a key: a turn superseded by the next one on
|
|
201
|
+
* the same session finishes late often enough, and it must terminate its OWN
|
|
202
|
+
* record, never the one that replaced it.
|
|
203
|
+
*/
|
|
204
|
+
complete(turn, event, error) {
|
|
205
|
+
turn.complete(event, error);
|
|
206
|
+
// Only the current record earns a grace timer; a superseded one is already
|
|
207
|
+
// unreachable and would otherwise schedule a release for its successor.
|
|
208
|
+
if (this.turns.get(turn.key) !== turn)
|
|
209
|
+
return;
|
|
210
|
+
const timer = setTimeout(() => this.release(turn.key), this.graceMs);
|
|
211
|
+
timer.unref?.();
|
|
212
|
+
this.releaseTimers.set(turn.key, timer);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Terminal frame + immediate release, for a producer whose turns nothing can
|
|
216
|
+
* re-attach to: the cross-channel live view files under its channel namespace,
|
|
217
|
+
* and the resume endpoint only ever looks under `api`. Holding that buffer for
|
|
218
|
+
* the grace window would retain up to the full per-turn cap (2,000 events /
|
|
219
|
+
* ~4 MB) for two minutes with no code path able to replay a byte of it.
|
|
220
|
+
*
|
|
221
|
+
* Same superseded-turn guard as complete(): a late finisher releases its OWN
|
|
222
|
+
* record, never the one that replaced it.
|
|
223
|
+
*/
|
|
224
|
+
completeAndRelease(turn, event, error) {
|
|
225
|
+
turn.complete(event, error);
|
|
226
|
+
if (this.turns.get(turn.key) === turn)
|
|
227
|
+
this.release(turn.key);
|
|
228
|
+
}
|
|
229
|
+
/** Drop the record and its grace timer. */
|
|
230
|
+
release(key) {
|
|
231
|
+
const timer = this.releaseTimers.get(key);
|
|
232
|
+
if (timer) {
|
|
233
|
+
clearTimeout(timer);
|
|
234
|
+
this.releaseTimers.delete(key);
|
|
235
|
+
}
|
|
236
|
+
this.turns.delete(key);
|
|
237
|
+
}
|
|
238
|
+
clear() {
|
|
239
|
+
for (const timer of this.releaseTimers.values())
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
this.releaseTimers.clear();
|
|
242
|
+
this.turns.clear();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
exports.TurnStreamRegistry = TurnStreamRegistry;
|
|
246
|
+
/** Cheap stand-in for the serialised size — exact bytes aren't worth the hot-path cost. */
|
|
247
|
+
function approxSize(event) {
|
|
248
|
+
switch (event.type) {
|
|
249
|
+
case 'text_delta':
|
|
250
|
+
case 'thinking':
|
|
251
|
+
return event.text.length + 32;
|
|
252
|
+
case 'result':
|
|
253
|
+
return event.text.length + 32 + (event.attachments?.length ?? 0) * 128;
|
|
254
|
+
case 'error':
|
|
255
|
+
case 'timeout':
|
|
256
|
+
return event.message.length + 32;
|
|
257
|
+
case 'tool_use':
|
|
258
|
+
return event.name.length + event.id.length + (event.input ? JSON.stringify(event.input).length : 0) + 32;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Terminal frame for a turn that finished normally. */
|
|
262
|
+
function resultEvent(text, attachments) {
|
|
263
|
+
return attachments.length ? { type: 'result', text, attachments } : { type: 'result', text };
|
|
264
|
+
}
|
|
265
|
+
/** Terminal frame for a turn that failed, carrying the Error's `code` when it has one. */
|
|
266
|
+
function errorEvent(err) {
|
|
267
|
+
const code = errorCode(err);
|
|
268
|
+
return code ? { type: 'error', message: err.message, code } : { type: 'error', message: err.message };
|
|
269
|
+
}
|
|
270
|
+
/** The `code` property producers attach to their Errors, when it is a string. */
|
|
271
|
+
function errorCode(err) {
|
|
272
|
+
const code = err?.code;
|
|
273
|
+
return typeof code === 'string' ? code : undefined;
|
|
274
|
+
}
|
|
275
|
+
/** Best-effort message for a terminal frame that is not a `result`. */
|
|
276
|
+
function terminalMessage(event) {
|
|
277
|
+
return 'message' in event ? event.message : `Turn ended (${event.type})`;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Rebuild an Error for a terminal frame whose original Error is not available —
|
|
281
|
+
* a replayed `error` frame reaches a sink as data, not as the live throw. The
|
|
282
|
+
* frame's `code` is copied back onto it so a resumed client learns the same
|
|
283
|
+
* thing the original connection did.
|
|
284
|
+
*/
|
|
285
|
+
function terminalError(event) {
|
|
286
|
+
const err = new Error(terminalMessage(event));
|
|
287
|
+
if (event.type === 'error' && event.code)
|
|
288
|
+
Object.assign(err, { code: event.code });
|
|
289
|
+
return err;
|
|
290
|
+
}
|
|
291
|
+
/** Adapt a callback trio into a sink a TurnStream can drive. */
|
|
292
|
+
function callbackSink(callbacks) {
|
|
293
|
+
return {
|
|
294
|
+
write: (e) => {
|
|
295
|
+
try {
|
|
296
|
+
callbacks.onChunk(e.event, e.seq);
|
|
297
|
+
}
|
|
298
|
+
catch { /* sink gone */ }
|
|
299
|
+
},
|
|
300
|
+
finish: (e) => {
|
|
301
|
+
try {
|
|
302
|
+
if (e.event.type === 'result') {
|
|
303
|
+
callbacks.onDone(e.event.text, e.event.attachments ?? [], e.seq);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
// `error` — and anything else that ever ends up terminal. A sink that
|
|
307
|
+
// is handed a terminal frame it does not recognise must still be told
|
|
308
|
+
// the turn is over, or an SSE response stays open forever.
|
|
309
|
+
callbacks.onError(e.error ?? terminalError(e.event), e.seq);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch { /* sink gone */ }
|
|
313
|
+
},
|
|
314
|
+
displaced: () => {
|
|
315
|
+
try {
|
|
316
|
+
callbacks.onDisplaced?.();
|
|
317
|
+
}
|
|
318
|
+
catch { /* sink gone */ }
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
//# sourceMappingURL=turn-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-stream.js","sourceRoot":"","sources":["../../src/agent/turn-stream.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apps-router.d.ts","sourceRoot":"","sources":["../../src/api/apps-router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,OAAO,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"apps-router.d.ts","sourceRoot":"","sources":["../../src/api/apps-router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,OAAO,EAAE,YAAY,EAAY,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AA+DzD,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,YAAY,EACtB,SAAS,EAAE,YAAY,EACvB,cAAc,EAAE,cAAc,EAC9B,OAAO,EAAE,MAAM,EAAE,GAChB,MAAM,CAwbR"}
|
package/dist/api/apps-router.js
CHANGED
|
@@ -4,6 +4,18 @@ exports.createAppsRouter = createAppsRouter;
|
|
|
4
4
|
const express_1 = require("express");
|
|
5
5
|
const auth_1 = require("./auth");
|
|
6
6
|
const compose_generator_1 = require("../apps/compose-generator");
|
|
7
|
+
/**
|
|
8
|
+
* Attach this boot's restore failure to an app entry, when one is recorded, so
|
|
9
|
+
* an app the gateway tried and failed to start is distinguishable from one an
|
|
10
|
+
* operator deliberately stopped (issue #425). Absent on every healthy app, so
|
|
11
|
+
* the response shape is unchanged for existing consumers.
|
|
12
|
+
*/
|
|
13
|
+
function withRestoreFailure(installer, entry) {
|
|
14
|
+
const failure = installer.getRestoreFailure(entry.name);
|
|
15
|
+
if (!failure)
|
|
16
|
+
return entry;
|
|
17
|
+
return { ...entry, restoreError: failure.error, restoreFailedAt: failure.at };
|
|
18
|
+
}
|
|
7
19
|
/**
|
|
8
20
|
* Parse a request-body `ports` field into a validated host-port override map.
|
|
9
21
|
* Returns undefined when absent. Throws (→ 400) on a non-object, a non-integer
|
|
@@ -85,7 +97,7 @@ function createAppsRouter(registry, installer, registryClient, apiKeys) {
|
|
|
85
97
|
// Reconcile each stored status against the live Docker runtime so a
|
|
86
98
|
// container that crashed/was killed externally no longer reports running.
|
|
87
99
|
const reconciled = await installer.reconcileStatuses(apps);
|
|
88
|
-
res.json({ apps: reconciled });
|
|
100
|
+
res.json({ apps: reconciled.map((e) => withRestoreFailure(installer, e)) });
|
|
89
101
|
}
|
|
90
102
|
catch (err) {
|
|
91
103
|
res.status(500).json({ error: err.message });
|
|
@@ -186,7 +198,7 @@ function createAppsRouter(registry, installer, registryClient, apiKeys) {
|
|
|
186
198
|
}
|
|
187
199
|
// Reconcile the stored status against the live Docker runtime before return.
|
|
188
200
|
const reconciled = await installer.reconcileStatus(entry);
|
|
189
|
-
res.json(reconciled);
|
|
201
|
+
res.json(withRestoreFailure(installer, reconciled));
|
|
190
202
|
}
|
|
191
203
|
catch (err) {
|
|
192
204
|
res.status(500).json({ error: err.message });
|