@0xmaxma/claude-gateway 1.5.8 → 1.6.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 +22 -0
- package/dist/agent/runner.d.ts +30 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +223 -16
- package/dist/agent/runner.js.map +1 -1
- package/dist/api/apps-router.d.ts.map +1 -1
- package/dist/api/apps-router.js +7 -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 +27 -0
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/line-webhook-router.d.ts.map +1 -1
- package/dist/api/line-webhook-router.js +68 -0
- package/dist/api/line-webhook-router.js.map +1 -1
- package/dist/api/router.d.ts +2 -0
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +59 -3
- package/dist/api/router.js.map +1 -1
- package/dist/api/share-router.d.ts +11 -0
- package/dist/api/share-router.d.ts.map +1 -0
- package/dist/api/share-router.js +402 -0
- package/dist/api/share-router.js.map +1 -0
- package/dist/apps/installer.d.ts +65 -0
- package/dist/apps/installer.d.ts.map +1 -1
- package/dist/apps/installer.js +171 -4
- package/dist/apps/installer.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +9 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/public-url.d.ts +11 -0
- package/dist/config/public-url.d.ts.map +1 -0
- package/dist/config/public-url.js +41 -0
- package/dist/config/public-url.js.map +1 -0
- package/dist/config/watcher.d.ts.map +1 -1
- package/dist/config/watcher.js +1 -0
- package/dist/config/watcher.js.map +1 -1
- package/dist/history/db.d.ts +14 -0
- package/dist/history/db.d.ts.map +1 -1
- package/dist/history/db.js +48 -4
- package/dist/history/db.js.map +1 -1
- package/dist/history/types.d.ts +5 -0
- package/dist/history/types.d.ts.map +1 -1
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +17 -1
- package/dist/session/process.js.map +1 -1
- package/dist/session/store.d.ts.map +1 -1
- package/dist/session/store.js +9 -1
- package/dist/session/store.js.map +1 -1
- package/dist/share/session-image-catalog.d.ts +22 -0
- package/dist/share/session-image-catalog.d.ts.map +1 -0
- package/dist/share/session-image-catalog.js +151 -0
- package/dist/share/session-image-catalog.js.map +1 -0
- package/dist/share/share-store.d.ts +137 -0
- package/dist/share/share-store.d.ts.map +1 -0
- package/dist/share/share-store.js +340 -0
- package/dist/share/share-store.js.map +1 -0
- package/dist/types.d.ts +17 -7
- package/dist/types.d.ts.map +1 -1
- package/mcp/instructions.ts +1 -0
- package/mcp/server.ts +2 -0
- package/mcp/tools/image/module.ts +240 -26
- package/mcp/tools/line/module.ts +186 -0
- package/mcp/tools/share-image/module.ts +120 -0
- package/mcp/tools/shared/share-client.ts +197 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -214,6 +214,28 @@ Config lives at `~/.claude-gateway/config.json` (or set `GATEWAY_CONFIG` env var
|
|
|
214
214
|
}
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
+
### `gateway.publicUrl` (optional)
|
|
218
|
+
|
|
219
|
+
The externally reachable gateway base URL. Set it manually to enable short-lived
|
|
220
|
+
public image shares used by `generate_image` reference edits and `share_image`.
|
|
221
|
+
The URL must end in `/gateway`; changing it requires a gateway restart.
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{
|
|
225
|
+
"gateway": {
|
|
226
|
+
"publicUrl": "https://vm.example.com/gateway"
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Minted share URLs have the stable form
|
|
232
|
+
`https://vm.example.com/gateway/shared/TOKEN`. When `publicUrl` is set the mint
|
|
233
|
+
response includes this ready-built `url`; when it is unset the response still
|
|
234
|
+
returns the `token` (the share endpoint stays enabled) and callers with their own
|
|
235
|
+
public base — e.g. LINE, which derives its host from the inbound webhook — build
|
|
236
|
+
`<base>/shared/<token>` themselves. HTTP is accepted only for local development
|
|
237
|
+
hosts such as `http://host.docker.internal:10850/gateway`.
|
|
238
|
+
|
|
217
239
|
### `session`
|
|
218
240
|
|
|
219
241
|
| Field | Default | Description |
|
package/dist/agent/runner.d.ts
CHANGED
|
@@ -149,6 +149,23 @@ export declare class AgentRunner extends EventEmitter {
|
|
|
149
149
|
* options are present.
|
|
150
150
|
*/
|
|
151
151
|
private static buildImageParamsNote;
|
|
152
|
+
/**
|
|
153
|
+
* The durable slice of the composer image options — everything except
|
|
154
|
+
* `image_refs`, which is a per-turn explicit selection (#73) and must never be
|
|
155
|
+
* restored as sticky session config. Returns undefined when nothing is durable.
|
|
156
|
+
*/
|
|
157
|
+
private static durableImageConfig;
|
|
158
|
+
/**
|
|
159
|
+
* The web builds image_ref/image_refs from the paths the upload endpoint
|
|
160
|
+
* returned — the ui-upload STAGING paths. promoteUiUploads then MOVES those
|
|
161
|
+
* files into per-session storage, so a note or history row built from the
|
|
162
|
+
* raw params would hand out dead paths (the agent's generate_image call
|
|
163
|
+
* fails image_ref_not_found and has to guess its way to the real file).
|
|
164
|
+
* Substitute every ref that matches a promoted staging path with its new
|
|
165
|
+
* session path; refs that were never staged (catalog refs, artifact:<id>)
|
|
166
|
+
* pass through untouched. (#74)
|
|
167
|
+
*/
|
|
168
|
+
static remapImageParamsRefs(p: ImageParams | undefined, stagedPaths: readonly string[] | undefined, promotedPaths: readonly string[] | undefined): ImageParams | undefined;
|
|
152
169
|
private static buildChannelXml;
|
|
153
170
|
private getOrSpawnSession;
|
|
154
171
|
private spawnSession;
|
|
@@ -281,6 +298,10 @@ export declare class AgentRunner extends EventEmitter {
|
|
|
281
298
|
startLineReply(): void;
|
|
282
299
|
stopLineReply(): void;
|
|
283
300
|
getAgentConfig(): AgentConfig;
|
|
301
|
+
/** Configured gateway.publicUrl, or undefined. The trusted source for the
|
|
302
|
+
* gateway's own public origin — preferred over request-derived hosts, which
|
|
303
|
+
* a client can spoof via X-Forwarded-Host. */
|
|
304
|
+
getGatewayPublicUrl(): string | undefined;
|
|
284
305
|
startTelegramReceiver(): void;
|
|
285
306
|
stopTelegramReceiver(): void;
|
|
286
307
|
startDiscordReceiver(): void;
|
|
@@ -357,6 +378,15 @@ export declare class AgentRunner extends EventEmitter {
|
|
|
357
378
|
* Pop and return buffered attachment paths for a session, then clear the buffer.
|
|
358
379
|
* Converts absolute paths to relative media URLs for the API response.
|
|
359
380
|
*/
|
|
381
|
+
/**
|
|
382
|
+
* Close a turn that died without a result (#75): drain any attachments the
|
|
383
|
+
* turn already registered (the files exist on disk — deliver them rather
|
|
384
|
+
* than leak them into the NEXT turn's reply) and record a terminal
|
|
385
|
+
* assistant row so history never ends on a dangling user message.
|
|
386
|
+
* System-initiated turns (skipUserMessage) have nothing dangling to close —
|
|
387
|
+
* only the buffer is drained.
|
|
388
|
+
*/
|
|
389
|
+
private persistFailedApiTurn;
|
|
360
390
|
popApiAttachments(sessionId: string): ApiAttachment[];
|
|
361
391
|
private evictApiSessionMapping;
|
|
362
392
|
getAgentsBaseDir(): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/agent/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAMtC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,EAAW,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAS7H,OAAO,EAA0C,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAOvF,OAAO,EAAiC,KAAK,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAG7F,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAqB1C,eAAO,MAAM,oBAAoB,QAAmB,CAAC;AAErD,eAAO,MAAM,cAAc,EAAE,WAAW,EAYvC,CAAC;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/agent/runner.ts"],"names":[],"mappings":"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"}
|
package/dist/agent/runner.js
CHANGED
|
@@ -94,6 +94,11 @@ const PROTECTED_WORKSPACE_FILES = [
|
|
|
94
94
|
'IDENTITY.md', 'USER.md', 'HEARTBEAT.md',
|
|
95
95
|
];
|
|
96
96
|
const MAX_API_IMAGES = 5;
|
|
97
|
+
/** Extra time an api turn may keep running AFTER its soft timeout already
|
|
98
|
+
* answered the caller (#75). The soft timeout only abandons the WAIT — the
|
|
99
|
+
* CLI turn is still producing a result that must reach history, exactly like
|
|
100
|
+
* the client-disconnect path. This cap bounds a genuinely hung turn. */
|
|
101
|
+
const API_TIMEOUT_HARD_CAP_EXTRA_MS = 600000;
|
|
97
102
|
// Hard timeout for the one-shot local `claude -p` triage during recovery
|
|
98
103
|
// (Epic #195, Phase 3b). A slow/hung triage collapses to a safe notify-only.
|
|
99
104
|
const RECOVERY_TRIAGE_TIMEOUT_MS = 15000;
|
|
@@ -954,13 +959,72 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
954
959
|
typeof p.n === 'number' ? `n="${p.n}"` : '',
|
|
955
960
|
p.image_ref ? `image_ref="${AgentRunner.escapeXmlAttr(p.image_ref)}"` : '',
|
|
956
961
|
].filter(Boolean);
|
|
957
|
-
|
|
962
|
+
const refs = (p.image_refs ?? []).filter((r) => typeof r === 'string' && r.trim().length > 0);
|
|
963
|
+
if (!attrs.length && !refs.length)
|
|
958
964
|
return '';
|
|
959
|
-
|
|
965
|
+
if (!refs.length) {
|
|
966
|
+
return (`<image-params ${attrs.join(' ')} />\n` +
|
|
967
|
+
`The user selected the image-generation options above in the composer. When the request ` +
|
|
968
|
+
`involves creating or editing an image, call the generate_image tool (action="generate") ` +
|
|
969
|
+
`using these values (pass image_ref as the "image" argument for image-to-image), then ` +
|
|
970
|
+
`deliver the returned image with your reply tool.\n`);
|
|
971
|
+
}
|
|
972
|
+
// Explicitly selected reference images (#73). Rendered as nested elements so a
|
|
973
|
+
// ref containing separators (commas, quotes) stays unambiguous.
|
|
974
|
+
const openTag = attrs.length ? `<image-params ${attrs.join(' ')}>` : '<image-params>';
|
|
975
|
+
const refLines = refs
|
|
976
|
+
.map((r, i) => ` <ref index="${i + 1}">${AgentRunner.escapeXmlAttr(r.trim())}</ref>`)
|
|
977
|
+
.join('\n');
|
|
978
|
+
const passInstruction = refs.length === 1
|
|
979
|
+
? `Pass that single ref as the "image" argument of generate_image.`
|
|
980
|
+
: `Pass all ${refs.length} refs as the "images" argument of generate_image, in the same order.`;
|
|
981
|
+
return (`${openTag}\n${refLines}\n</image-params>\n` +
|
|
960
982
|
`The user selected the image-generation options above in the composer. When the request ` +
|
|
961
983
|
`involves creating or editing an image, call the generate_image tool (action="generate") ` +
|
|
962
|
-
`using these values
|
|
963
|
-
`
|
|
984
|
+
`using these values, then deliver the returned image with your reply tool.\n` +
|
|
985
|
+
`The user explicitly SELECTED the reference image(s) listed above in the composer, in this order. ` +
|
|
986
|
+
`${passInstruction} Do NOT reinterpret which images they are, do NOT call list_refs to ` +
|
|
987
|
+
`second-guess an explicit selection, and do not drop any of them. ` +
|
|
988
|
+
`Do NOT open or Read the referenced files first — the image model receives the actual files; ` +
|
|
989
|
+
`reading them wastes minutes and can push the request past its timeout. Go straight to generate_image.\n`);
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* The durable slice of the composer image options — everything except
|
|
993
|
+
* `image_refs`, which is a per-turn explicit selection (#73) and must never be
|
|
994
|
+
* restored as sticky session config. Returns undefined when nothing is durable.
|
|
995
|
+
*/
|
|
996
|
+
static durableImageConfig(p) {
|
|
997
|
+
const { image_refs: _refs, ...durable } = p;
|
|
998
|
+
return Object.keys(durable).length ? durable : undefined;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* The web builds image_ref/image_refs from the paths the upload endpoint
|
|
1002
|
+
* returned — the ui-upload STAGING paths. promoteUiUploads then MOVES those
|
|
1003
|
+
* files into per-session storage, so a note or history row built from the
|
|
1004
|
+
* raw params would hand out dead paths (the agent's generate_image call
|
|
1005
|
+
* fails image_ref_not_found and has to guess its way to the real file).
|
|
1006
|
+
* Substitute every ref that matches a promoted staging path with its new
|
|
1007
|
+
* session path; refs that were never staged (catalog refs, artifact:<id>)
|
|
1008
|
+
* pass through untouched. (#74)
|
|
1009
|
+
*/
|
|
1010
|
+
static remapImageParamsRefs(p, stagedPaths, promotedPaths) {
|
|
1011
|
+
if (!p || !stagedPaths?.length || !promotedPaths?.length)
|
|
1012
|
+
return p;
|
|
1013
|
+
const map = new Map();
|
|
1014
|
+
for (let i = 0; i < Math.min(stagedPaths.length, promotedPaths.length); i++) {
|
|
1015
|
+
const from = stagedPaths[i];
|
|
1016
|
+
const to = promotedPaths[i];
|
|
1017
|
+
if (from !== to)
|
|
1018
|
+
map.set(from, to);
|
|
1019
|
+
}
|
|
1020
|
+
if (!map.size)
|
|
1021
|
+
return p;
|
|
1022
|
+
const remap = (r) => map.get(r) ?? r;
|
|
1023
|
+
return {
|
|
1024
|
+
...p,
|
|
1025
|
+
...(p.image_ref ? { image_ref: remap(p.image_ref) } : {}),
|
|
1026
|
+
...(p.image_refs?.length ? { image_refs: p.image_refs.map(remap) } : {}),
|
|
1027
|
+
};
|
|
964
1028
|
}
|
|
965
1029
|
static buildChannelXml(params) {
|
|
966
1030
|
const meta = params.meta ?? {};
|
|
@@ -1117,6 +1181,11 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1117
1181
|
// Auto-forward result text to channel if agent didn't call reply tool.
|
|
1118
1182
|
let replyCalled = false;
|
|
1119
1183
|
let replyToolUseId = null; // track id to detect failed tool calls
|
|
1184
|
+
// line_image tool_use blocks re-appear across cumulative `assistant` stream
|
|
1185
|
+
// snapshots (--include-partial-messages), so dedupe history inserts by the
|
|
1186
|
+
// block id — otherwise one sent image lands in the transcript N times and
|
|
1187
|
+
// skews the session image catalog's "image N" ordinal.
|
|
1188
|
+
const seenLineImageIds = new Set();
|
|
1120
1189
|
// Set when an interactive-menu prompt was rendered to the channel this turn,
|
|
1121
1190
|
// so the result's plain-text auto-forward is skipped (no duplicate message).
|
|
1122
1191
|
let menuSentThisTurn = false;
|
|
@@ -1141,6 +1210,34 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1141
1210
|
const msg = obj['message'];
|
|
1142
1211
|
if (Array.isArray(msg?.content)) {
|
|
1143
1212
|
for (const block of msg.content) {
|
|
1213
|
+
// LINE sends images through its own line_image tool (not the reply
|
|
1214
|
+
// tool's `files`), so capture those sends into history too — the
|
|
1215
|
+
// web transcript and the session image catalog key off mediaFiles.
|
|
1216
|
+
if (block.type === 'tool_use' && block.name === 'mcp__gateway__line_image') {
|
|
1217
|
+
const lineBlockId = block['id'];
|
|
1218
|
+
const dedupeKey = typeof lineBlockId === 'string' ? lineBlockId : '';
|
|
1219
|
+
// Skip if this tool_use block was already persisted this turn
|
|
1220
|
+
// (cumulative assistant snapshots re-emit completed blocks).
|
|
1221
|
+
if (dedupeKey && seenLineImageIds.has(dedupeKey))
|
|
1222
|
+
continue;
|
|
1223
|
+
const imgPath = typeof block.input?.['image'] === 'string' ? block.input['image'] : '';
|
|
1224
|
+
const mediaRootLine = path.join(this.agentsBaseDir, this.agentConfig.id, 'media') + path.sep;
|
|
1225
|
+
const lineMedia = toRelMediaFiles(imgPath ? [imgPath] : [], mediaRootLine);
|
|
1226
|
+
if (lineMedia.length) {
|
|
1227
|
+
if (dedupeKey)
|
|
1228
|
+
seenLineImageIds.add(dedupeKey);
|
|
1229
|
+
const channelSrcLine = this.channelSourceMap.get(mapKey) ?? 'line';
|
|
1230
|
+
this.historyDb.insertMessage({
|
|
1231
|
+
chatId: `${channelSrcLine}-${mapKey}`,
|
|
1232
|
+
sessionId: actualSessionId,
|
|
1233
|
+
source: channelSrcLine,
|
|
1234
|
+
role: 'assistant',
|
|
1235
|
+
content: '',
|
|
1236
|
+
mediaFiles: lineMedia,
|
|
1237
|
+
ts: Date.now(),
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1144
1241
|
if (block.type === 'tool_use' && block.name === replyToolName && !replyCalled) {
|
|
1145
1242
|
replyCalled = true;
|
|
1146
1243
|
replyToolUseId = block['id'] ?? null;
|
|
@@ -1333,6 +1430,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1333
1430
|
}
|
|
1334
1431
|
replyCalled = false; // reset for next turn
|
|
1335
1432
|
replyToolUseId = null;
|
|
1433
|
+
seenLineImageIds.clear();
|
|
1336
1434
|
menuSentThisTurn = false;
|
|
1337
1435
|
menuPromptTextThisTurn = '';
|
|
1338
1436
|
// In pty-shell mode, a `result` fires after every Claude API sub-turn
|
|
@@ -1947,6 +2045,12 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1947
2045
|
getAgentConfig() {
|
|
1948
2046
|
return { ...this.agentConfig };
|
|
1949
2047
|
}
|
|
2048
|
+
/** Configured gateway.publicUrl, or undefined. The trusted source for the
|
|
2049
|
+
* gateway's own public origin — preferred over request-derived hosts, which
|
|
2050
|
+
* a client can spoof via X-Forwarded-Host. */
|
|
2051
|
+
getGatewayPublicUrl() {
|
|
2052
|
+
return this.gatewayConfig.gateway.publicUrl;
|
|
2053
|
+
}
|
|
1950
2054
|
startTelegramReceiver() {
|
|
1951
2055
|
if (this.receiver?.isRunning())
|
|
1952
2056
|
return;
|
|
@@ -2089,6 +2193,9 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2089
2193
|
const finalMediaFiles = opts.mediaFiles?.length
|
|
2090
2194
|
? await promoteUiUploads(this.agentsBaseDir, this.agentConfig.id, sessionId, opts.mediaFiles, this.logger)
|
|
2091
2195
|
: undefined;
|
|
2196
|
+
// Refs built from staging paths must follow the files to their promoted
|
|
2197
|
+
// location — see remapImageParamsRefs (#74).
|
|
2198
|
+
const imageParams = AgentRunner.remapImageParamsRefs(opts.imageParams, opts.mediaFiles, finalMediaFiles);
|
|
2092
2199
|
// Resolve media files to absolute paths for file-path based image passing
|
|
2093
2200
|
// (same pattern as Telegram — Claude Code reads files via Read tool instead of base64 inline)
|
|
2094
2201
|
const imagePaths = finalMediaFiles?.length ? this.resolveMediaPaths(finalMediaFiles) : [];
|
|
@@ -2112,10 +2219,18 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2112
2219
|
role: 'user',
|
|
2113
2220
|
content: message,
|
|
2114
2221
|
mediaFiles: finalMediaFiles?.length ? finalMediaFiles : undefined,
|
|
2222
|
+
// Display-only (#74): lets the UI show which earlier images this turn
|
|
2223
|
+
// referenced after a reload. The generation path reads the refs from the
|
|
2224
|
+
// image-params note, never from here.
|
|
2225
|
+
imageRefs: imageParams?.image_refs?.length ? imageParams.image_refs : undefined,
|
|
2115
2226
|
ts: apiUserTs,
|
|
2116
2227
|
});
|
|
2117
2228
|
}
|
|
2118
2229
|
this.pendingApiSessions.add(sessionId);
|
|
2230
|
+
// A previous turn that died (hard timeout, crash) may have left attachments
|
|
2231
|
+
// buffered under this session — they belong to THAT turn; never let them
|
|
2232
|
+
// ride along on this one (#75).
|
|
2233
|
+
this.pendingApiAttachments.delete(sessionId);
|
|
2119
2234
|
session.touch();
|
|
2120
2235
|
// Image paths only work when allowTools:true — Claude needs the Read tool to access them
|
|
2121
2236
|
const allowTools = opts.allowTools ?? false;
|
|
@@ -2135,14 +2250,16 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2135
2250
|
}
|
|
2136
2251
|
// Build channel XML with image_path attribute (like Telegram) for first image
|
|
2137
2252
|
const imageAttr = effectiveImagePaths.length ? ` image_path="${AgentRunner.escapeXmlAttr(effectiveImagePaths[0])}"` : '';
|
|
2138
|
-
const imageParamsNote =
|
|
2253
|
+
const imageParamsNote = imageParams ? AgentRunner.buildImageParamsNote(imageParams) : '';
|
|
2139
2254
|
// Persist the composer image options to session meta so the web can restore the
|
|
2140
2255
|
// selection on reload (SessionMeta.imageConfig). Only when the send carries them
|
|
2141
2256
|
// (the web sends image_params on first-set/change), so this holds the latest.
|
|
2257
|
+
// image_refs are per-turn and deliberately excluded (#73).
|
|
2142
2258
|
// Channel is 'api' here (api sessions live under api-<chatId>). Best-effort.
|
|
2143
|
-
|
|
2259
|
+
const durableImageConfig = imageParams ? AgentRunner.durableImageConfig(imageParams) : undefined;
|
|
2260
|
+
if (durableImageConfig) {
|
|
2144
2261
|
this.sessionStore
|
|
2145
|
-
.updateSessionMeta(this.agentConfig.id, chatId, sessionId, { imageConfig:
|
|
2262
|
+
.updateSessionMeta(this.agentConfig.id, chatId, sessionId, { imageConfig: durableImageConfig }, 'api')
|
|
2146
2263
|
.catch(() => { });
|
|
2147
2264
|
}
|
|
2148
2265
|
const channelXml = `<channel source="api" chat_id="${chatId}" session_id="${sessionId}" ts="${new Date().toISOString()}"${imageAttr}>\n` +
|
|
@@ -2183,11 +2300,17 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2183
2300
|
resolve({ text: result.trim(), attachments });
|
|
2184
2301
|
};
|
|
2185
2302
|
const fail = (err) => {
|
|
2303
|
+
// Terminal close for a turn that produced no result (#75): record it so
|
|
2304
|
+
// history does not end on a dangling user message, and drain the
|
|
2305
|
+
// attachment buffer so nothing leaks into the next turn.
|
|
2306
|
+
this.persistFailedApiTurn(chatId, sessionId, err, opts.skipUserMessage);
|
|
2186
2307
|
cleanup();
|
|
2187
|
-
reject(err);
|
|
2308
|
+
reject(err); // no-op when the soft timeout already rejected
|
|
2188
2309
|
};
|
|
2189
2310
|
const cleanup = () => {
|
|
2190
2311
|
clearTimeout(globalTimer);
|
|
2312
|
+
if (hardCapTimer)
|
|
2313
|
+
clearTimeout(hardCapTimer);
|
|
2191
2314
|
if (quietTimer)
|
|
2192
2315
|
clearTimeout(quietTimer);
|
|
2193
2316
|
session.off('output', onOutput);
|
|
@@ -2239,9 +2362,17 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2239
2362
|
/* non-JSON stdout line */
|
|
2240
2363
|
}
|
|
2241
2364
|
};
|
|
2365
|
+
// Soft timeout (#75): unblock the caller now, but KEEP the output listener
|
|
2366
|
+
// attached — a turn finishing moments late must still persist its result
|
|
2367
|
+
// and attachments, mirroring the client-disconnect path. The hard cap
|
|
2368
|
+
// bounds a genuinely hung turn.
|
|
2369
|
+
let hardCapTimer;
|
|
2242
2370
|
const globalTimer = setTimeout(() => {
|
|
2243
|
-
|
|
2244
|
-
|
|
2371
|
+
reject(Object.assign(new Error('Agent response timeout'), { code: 'TIMEOUT' }));
|
|
2372
|
+
hardCapTimer = setTimeout(() => {
|
|
2373
|
+
session.setProcessing(false);
|
|
2374
|
+
fail(Object.assign(new Error('Agent response timed out.'), { code: 'TIMEOUT' }));
|
|
2375
|
+
}, API_TIMEOUT_HARD_CAP_EXTRA_MS);
|
|
2245
2376
|
}, opts.timeoutMs);
|
|
2246
2377
|
session.on('output', onOutput);
|
|
2247
2378
|
session.setProcessing(true);
|
|
@@ -2273,6 +2404,9 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2273
2404
|
const finalMediaFilesStream = opts.mediaFiles?.length
|
|
2274
2405
|
? await promoteUiUploads(this.agentsBaseDir, this.agentConfig.id, sessionId, opts.mediaFiles, this.logger)
|
|
2275
2406
|
: undefined;
|
|
2407
|
+
// Refs built from staging paths must follow the files to their promoted
|
|
2408
|
+
// location — see remapImageParamsRefs (#74).
|
|
2409
|
+
const imageParamsStream = AgentRunner.remapImageParamsRefs(opts.imageParams, opts.mediaFiles, finalMediaFilesStream);
|
|
2276
2410
|
// Resolve media files to absolute paths for file-path based image passing
|
|
2277
2411
|
const imagePathsStream = finalMediaFilesStream?.length ? this.resolveMediaPaths(finalMediaFilesStream) : [];
|
|
2278
2412
|
if (!opts.skipUserMessage) {
|
|
@@ -2291,14 +2425,32 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2291
2425
|
role: 'user',
|
|
2292
2426
|
content: message,
|
|
2293
2427
|
mediaFiles: finalMediaFilesStream?.length ? finalMediaFilesStream : undefined,
|
|
2428
|
+
// Display-only (#74): lets the UI show which earlier images this turn
|
|
2429
|
+
// referenced after a reload. The generation path reads the refs from the
|
|
2430
|
+
// image-params note, never from here.
|
|
2431
|
+
imageRefs: imageParamsStream?.image_refs?.length ? imageParamsStream.image_refs : undefined,
|
|
2294
2432
|
ts: streamUserTs,
|
|
2295
2433
|
});
|
|
2296
2434
|
}
|
|
2297
2435
|
this.pendingApiSessions.add(sessionId);
|
|
2436
|
+
// A previous turn that died (hard timeout, crash) may have left attachments
|
|
2437
|
+
// buffered under this session — they belong to THAT turn; never let them
|
|
2438
|
+
// ride along on this one (#75).
|
|
2439
|
+
this.pendingApiAttachments.delete(sessionId);
|
|
2298
2440
|
session.touch();
|
|
2299
2441
|
const buffer = [];
|
|
2300
2442
|
let settled = false;
|
|
2301
2443
|
let clientGone = false;
|
|
2444
|
+
// onError must fire at most once per turn. The soft timeout notifies the
|
|
2445
|
+
// client directly (below) WITHOUT setting `settled`, so the later hard-cap
|
|
2446
|
+
// fail() would otherwise call onError a second time.
|
|
2447
|
+
let errorNotified = false;
|
|
2448
|
+
const notifyError = (err) => {
|
|
2449
|
+
if (errorNotified)
|
|
2450
|
+
return;
|
|
2451
|
+
errorNotified = true;
|
|
2452
|
+
callbacks.onError(err);
|
|
2453
|
+
};
|
|
2302
2454
|
// Track partial message text for delta computation (--include-partial-messages)
|
|
2303
2455
|
let lastPartialText = '';
|
|
2304
2456
|
// Accumulate tool_use blocks from stream_event (content_block_start → delta → stop)
|
|
@@ -2338,10 +2490,17 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2338
2490
|
return;
|
|
2339
2491
|
settled = true;
|
|
2340
2492
|
cleanup();
|
|
2341
|
-
|
|
2493
|
+
// Terminal close for a turn that produced no result (#75): record it so
|
|
2494
|
+
// history does not end on a dangling user message (the web reads that
|
|
2495
|
+
// state as "still thinking" and spins forever), and drain the attachment
|
|
2496
|
+
// buffer so nothing leaks into the next turn.
|
|
2497
|
+
this.persistFailedApiTurn(chatId, sessionId, err, opts.skipUserMessage);
|
|
2498
|
+
notifyError(err);
|
|
2342
2499
|
};
|
|
2343
2500
|
const cleanup = () => {
|
|
2344
2501
|
clearTimeout(globalTimer);
|
|
2502
|
+
if (hardCapTimer)
|
|
2503
|
+
clearTimeout(hardCapTimer);
|
|
2345
2504
|
session.off('output', onOutput);
|
|
2346
2505
|
this.pendingApiSessions.delete(sessionId);
|
|
2347
2506
|
};
|
|
@@ -2422,9 +2581,26 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2422
2581
|
/* non-JSON stdout line */
|
|
2423
2582
|
}
|
|
2424
2583
|
};
|
|
2584
|
+
// Soft timeout (#75): tell the SSE client now, but KEEP the output listener
|
|
2585
|
+
// attached — exactly like onClientDisconnect — so a turn that finishes a few
|
|
2586
|
+
// seconds past the budget still lands in history with its attachments
|
|
2587
|
+
// (observed: 304s image turn vs 300s budget → image generated, reply lost,
|
|
2588
|
+
// spinner stuck). The hard cap bounds a genuinely hung turn.
|
|
2589
|
+
let hardCapTimer;
|
|
2425
2590
|
const globalTimer = setTimeout(() => {
|
|
2426
|
-
|
|
2427
|
-
|
|
2591
|
+
clientGone = true;
|
|
2592
|
+
try {
|
|
2593
|
+
notifyError(Object.assign(new Error('Agent response timeout'), { code: 'TIMEOUT' }));
|
|
2594
|
+
}
|
|
2595
|
+
catch { /* client already gone */ }
|
|
2596
|
+
// The session stays in-flight (pendingApiSessions) until the hard cap so a
|
|
2597
|
+
// retry on the SAME session_id gets a 409 CONFLICT rather than interleaving
|
|
2598
|
+
// with the subprocess that is still processing this turn. This lock is
|
|
2599
|
+
// intentionally held for the whole soft→hard window (see API.md §409).
|
|
2600
|
+
hardCapTimer = setTimeout(() => {
|
|
2601
|
+
session.setProcessing(false);
|
|
2602
|
+
fail(Object.assign(new Error('Agent response timed out.'), { code: 'TIMEOUT' }));
|
|
2603
|
+
}, API_TIMEOUT_HARD_CAP_EXTRA_MS);
|
|
2428
2604
|
}, opts.timeoutMs);
|
|
2429
2605
|
session.on('output', onOutput);
|
|
2430
2606
|
// Image paths only work when allowTools:true — Claude needs the Read tool to access them
|
|
@@ -2445,13 +2621,17 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2445
2621
|
}
|
|
2446
2622
|
// Build channel XML with image_path attribute (like Telegram) for first image
|
|
2447
2623
|
const imageAttrStream = effectiveImagePathsStream.length ? ` image_path="${AgentRunner.escapeXmlAttr(effectiveImagePathsStream[0])}"` : '';
|
|
2448
|
-
const imageParamsNoteStream =
|
|
2624
|
+
const imageParamsNoteStream = imageParamsStream ? AgentRunner.buildImageParamsNote(imageParamsStream) : '';
|
|
2449
2625
|
// Persist composer image config to session meta (SessionMeta.imageConfig) so the
|
|
2450
2626
|
// web restores the selection on reload. This is the streaming path the web uses.
|
|
2627
|
+
// image_refs are per-turn and deliberately excluded (#73).
|
|
2451
2628
|
// Channel 'api' — api sessions live under api-<chatId>. Best-effort.
|
|
2452
|
-
|
|
2629
|
+
const durableImageConfigStream = imageParamsStream
|
|
2630
|
+
? AgentRunner.durableImageConfig(imageParamsStream)
|
|
2631
|
+
: undefined;
|
|
2632
|
+
if (durableImageConfigStream) {
|
|
2453
2633
|
this.sessionStore
|
|
2454
|
-
.updateSessionMeta(this.agentConfig.id, chatId, sessionId, { imageConfig:
|
|
2634
|
+
.updateSessionMeta(this.agentConfig.id, chatId, sessionId, { imageConfig: durableImageConfigStream }, 'api')
|
|
2455
2635
|
.catch(() => { });
|
|
2456
2636
|
}
|
|
2457
2637
|
const channelXml = `<channel source="api" chat_id="${chatId}" session_id="${sessionId}" ts="${new Date().toISOString()}"${imageAttrStream}>\n` +
|
|
@@ -2483,6 +2663,33 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2483
2663
|
* Pop and return buffered attachment paths for a session, then clear the buffer.
|
|
2484
2664
|
* Converts absolute paths to relative media URLs for the API response.
|
|
2485
2665
|
*/
|
|
2666
|
+
/**
|
|
2667
|
+
* Close a turn that died without a result (#75): drain any attachments the
|
|
2668
|
+
* turn already registered (the files exist on disk — deliver them rather
|
|
2669
|
+
* than leak them into the NEXT turn's reply) and record a terminal
|
|
2670
|
+
* assistant row so history never ends on a dangling user message.
|
|
2671
|
+
* System-initiated turns (skipUserMessage) have nothing dangling to close —
|
|
2672
|
+
* only the buffer is drained.
|
|
2673
|
+
*/
|
|
2674
|
+
persistFailedApiTurn(chatId, sessionId, err, skipUserMessage) {
|
|
2675
|
+
const attachments = this.popApiAttachments(sessionId);
|
|
2676
|
+
if (skipUserMessage)
|
|
2677
|
+
return;
|
|
2678
|
+
const failTs = Date.now();
|
|
2679
|
+
const content = `\u26a0\ufe0f ${err.message}`;
|
|
2680
|
+
this.sessionStore
|
|
2681
|
+
.appendMessage(this.agentConfig.id, sessionId, { role: 'assistant', content, ts: failTs })
|
|
2682
|
+
.catch(() => { });
|
|
2683
|
+
this.historyDb.insertMessage({
|
|
2684
|
+
chatId: `api-${chatId}`,
|
|
2685
|
+
sessionId,
|
|
2686
|
+
source: 'api',
|
|
2687
|
+
role: 'assistant',
|
|
2688
|
+
content,
|
|
2689
|
+
mediaFiles: attachments.length ? attachments.map((a) => `media/${a.relPath}`) : undefined,
|
|
2690
|
+
ts: failTs,
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2486
2693
|
popApiAttachments(sessionId) {
|
|
2487
2694
|
const paths = this.pendingApiAttachments.get(sessionId) ?? [];
|
|
2488
2695
|
this.pendingApiAttachments.delete(sessionId);
|