@0xmaxma/claude-gateway 1.3.3 → 1.3.5
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 +1 -1
- package/dist/agent/runner.d.ts +26 -1
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +264 -68
- package/dist/agent/runner.js.map +1 -1
- package/dist/api/gateway-router.d.ts +10 -0
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +261 -7
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +52 -32
- package/dist/api/router.js.map +1 -1
- package/dist/session/process.d.ts +8 -0
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +28 -0
- package/dist/session/process.js.map +1 -1
- package/dist/session/store.d.ts.map +1 -1
- package/dist/session/store.js +46 -0
- package/dist/session/store.js.map +1 -1
- package/dist/shell/claude-pty-shell.js +211 -4
- package/dist/shell/claude-pty-shell.js.map +1 -1
- package/dist/shell/emitter.d.ts +13 -0
- package/dist/shell/emitter.d.ts.map +1 -1
- package/dist/shell/emitter.js +16 -0
- package/dist/shell/emitter.js.map +1 -1
- package/dist/shell/menu-cancel.d.ts +50 -0
- package/dist/shell/menu-cancel.d.ts.map +1 -0
- package/dist/shell/menu-cancel.js +62 -0
- package/dist/shell/menu-cancel.js.map +1 -0
- package/dist/shell/pty-serialize.d.ts +23 -0
- package/dist/shell/pty-serialize.d.ts.map +1 -0
- package/dist/shell/pty-serialize.js +131 -0
- package/dist/shell/pty-serialize.js.map +1 -0
- package/dist/shell/pty-stream-registry.d.ts +59 -0
- package/dist/shell/pty-stream-registry.d.ts.map +1 -0
- package/dist/shell/pty-stream-registry.js +279 -0
- package/dist/shell/pty-stream-registry.js.map +1 -0
- package/dist/shell/screen.d.ts +43 -0
- package/dist/shell/screen.d.ts.map +1 -1
- package/dist/shell/screen.js +91 -1
- package/dist/shell/screen.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/web-ui.d.ts +2 -2
- package/dist/ui/web-ui.d.ts.map +1 -1
- package/dist/ui/web-ui.js +615 -114
- package/dist/ui/web-ui.js.map +1 -1
- package/mcp/tools/discord/module.ts +88 -1
- package/mcp/tools/discord/outbound.ts +36 -0
- package/mcp/tools/discord/types.ts +2 -0
- package/mcp/tools/telegram/menu-parser.ts +37 -0
- package/mcp/tools/telegram/receiver-server.ts +83 -0
- package/mcp/tools/telegram/typing.ts +7 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ A self-hosted multi-agent gateway for Claude Code. Connect Claude agents to Tele
|
|
|
29
29
|
- **App Store** — install, update, and host Docker-compose apps on the gateway; apps get a reverse proxy at `/app/:name/:portName/*`, optional Unix socket bridge for host scripts, and optional AI agent injection
|
|
30
30
|
- **Self-update API** — check for newer versions of `claude-gateway` and `claude-code` and trigger an update via a single API call; no SSH or shell access needed
|
|
31
31
|
- **Session persistence** — conversation history saved and restored across restarts
|
|
32
|
-
- **PTY
|
|
32
|
+
- **PTY shell (wrap-shell mode)** — optional interactive pseudo-terminal backend (`gateway.headless: false`) for tools that require a real TTY; includes a live browser viewer (xterm.js) and a `/api/v1/sessions/:sessionId/screen` endpoint that returns the visible screen as plain text — agents can poll it to detect hang states, menus, or unexpected output without parsing ANSI escape codes; app-agents always stay headless
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
package/dist/agent/runner.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export declare class AgentRunner extends EventEmitter {
|
|
|
29
29
|
private readonly sessionSpawnLocks;
|
|
30
30
|
private readonly pendingImagePaths;
|
|
31
31
|
private readonly pendingApiAttachments;
|
|
32
|
+
private readonly sessionHistory;
|
|
33
|
+
private readonly apiChatIds;
|
|
32
34
|
private skillRegistry;
|
|
33
35
|
private readonly configPath;
|
|
34
36
|
private readonly historyDb;
|
|
@@ -133,6 +135,13 @@ export declare class AgentRunner extends EventEmitter {
|
|
|
133
135
|
private writeTypingDone;
|
|
134
136
|
private triggerSummaryAndRestart;
|
|
135
137
|
private writeAutoForward;
|
|
138
|
+
/**
|
|
139
|
+
* Signal the channel receiver to render an interactive menu as native UI
|
|
140
|
+
* (inline buttons on Telegram/Discord). The receiver maps each option to a
|
|
141
|
+
* `choice:N` callback whose tap arrives back as a normal "N" message — the
|
|
142
|
+
* same as the user typing the number.
|
|
143
|
+
*/
|
|
144
|
+
private writeMenuForward;
|
|
136
145
|
private startIdleCleaner;
|
|
137
146
|
start(): Promise<void>;
|
|
138
147
|
updateAgentConfig(newConfig: AgentConfig): void;
|
|
@@ -145,6 +154,17 @@ export declare class AgentRunner extends EventEmitter {
|
|
|
145
154
|
private _startCleanupScheduler;
|
|
146
155
|
restart(): Promise<void>;
|
|
147
156
|
isRunning(): boolean;
|
|
157
|
+
getSessionsSummary(): Array<{
|
|
158
|
+
chatId: string;
|
|
159
|
+
sessionId: string;
|
|
160
|
+
source: string;
|
|
161
|
+
mode: string;
|
|
162
|
+
model: string;
|
|
163
|
+
isRunning: boolean;
|
|
164
|
+
spawnedAt: number;
|
|
165
|
+
uptimeSec: number;
|
|
166
|
+
tokens: number;
|
|
167
|
+
}>;
|
|
148
168
|
/**
|
|
149
169
|
* Send a message to an API session and wait for the response.
|
|
150
170
|
*
|
|
@@ -203,7 +223,12 @@ export declare class AgentRunner extends EventEmitter {
|
|
|
203
223
|
name: string;
|
|
204
224
|
}>>;
|
|
205
225
|
listSessionsForChat(chatId: string, channel: 'telegram' | 'discord'): Promise<import('../types').SessionIndex>;
|
|
206
|
-
executeApiCommand(sessionId: string, chatId: string, command: string
|
|
226
|
+
executeApiCommand(sessionId: string, chatId: string, command: string, opts?: {
|
|
227
|
+
skipPersist?: boolean;
|
|
228
|
+
}): Promise<{
|
|
229
|
+
result: Record<string, unknown>;
|
|
230
|
+
responseText: string;
|
|
231
|
+
}>;
|
|
207
232
|
setModel(newModel: string): Promise<void>;
|
|
208
233
|
listApiSessions(chatId: string): Promise<import('../types').SessionIndex>;
|
|
209
234
|
createApiSession(chatId: string, prompt?: string, name?: string): Promise<import('../types').SessionMeta>;
|
|
@@ -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,MAAM,UAAU,CAAC;AAQhH,OAAO,EAA0C,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAEvF,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAQ1C,eAAO,MAAM,oBAAoB,QAAmB,CAAC;AAErD,eAAO,MAAM,cAAc,EAAE,WAAW,EAMvC,CAAC;AAoDF,qBAAa,WAAY,SAAQ,YAAY;IAC3C,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA6B;IAC9D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IACrD,OAAO,CAAC,SAAS,CAAoC;IAErD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IACjC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAEvC,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqC;IAC9D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA6C;IAC9E,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,gBAAgB,CAA+C;IAGvE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IAGxD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA8C;IAGhF,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA+B;IAGjE,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAA+B;IAGrE,OAAO,CAAC,aAAa,CAAwC;IAG7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IAGpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IAGtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IAEvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAGlC,OAAO,CAAC,aAAa,CAA6B;gBAEtC,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,MAAM;IAsBnF;;OAEG;IACH,gBAAgB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAI/C,gBAAgB,IAAI,aAAa;IAIjC,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED;;;OAGG;YACW,mBAAmB;IAuJjC;;;OAGG;YACW,oBAAoB;IAqLlC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAY5B,OAAO,CAAC,MAAM,CAAC,aAAa;IAI5B,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,MAAM,CAAC,eAAe;YAwChB,iBAAiB;YAgDjB,YAAY;
|
|
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,MAAM,UAAU,CAAC;AAQhH,OAAO,EAA0C,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAEvF,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAQ1C,eAAO,MAAM,oBAAoB,QAAmB,CAAC;AAErD,eAAO,MAAM,cAAc,EAAE,WAAW,EAMvC,CAAC;AAoDF,qBAAa,WAAY,SAAQ,YAAY;IAC3C,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA6B;IAC9D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IACrD,OAAO,CAAC,SAAS,CAAoC;IAErD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IACjC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAEvC,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqC;IAC9D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA6C;IAC9E,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,gBAAgB,CAA+C;IAGvE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IAGxD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA8C;IAGhF,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA+B;IAGjE,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAA+B;IAGrE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAoH;IAKnJ,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IAGxD,OAAO,CAAC,aAAa,CAAwC;IAG7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IAGpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IAGtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IAEvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAGlC,OAAO,CAAC,aAAa,CAA6B;gBAEtC,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,MAAM;IAsBnF;;OAEG;IACH,gBAAgB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAI/C,gBAAgB,IAAI,aAAa;IAIjC,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED;;;OAGG;YACW,mBAAmB;IAuJjC;;;OAGG;YACW,oBAAoB;IAqLlC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAY5B,OAAO,CAAC,MAAM,CAAC,aAAa;IAI5B,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,MAAM,CAAC,eAAe;YAwChB,iBAAiB;YAgDjB,YAAY;IAqQ1B,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAIpD;;OAEG;YACW,oBAAoB;IAsBlC;;OAEG;YACW,qBAAqB;IAiBnC;;OAEG;YACW,wBAAwB;IA+CtC;;OAEG;YACW,gBAAgB;IAM9B;;OAEG;YACW,mBAAmB;IAUjC;;;OAGG;YACW,iBAAiB;IAM/B;;;OAGG;YACW,kBAAkB;IAyBhC;;OAEG;YACW,oBAAoB;IA4ClC;;;OAGG;YACW,aAAa;IAS3B;;;OAGG;YACW,cAAc;IAS5B;;;;;;;;;;;OAWG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAsBrC;;OAEG;IACH,OAAO,CAAC,SAAS;IAWjB;;;;OAIG;IACD,OAAO,CAAC,YAAY;IAMtB,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,eAAe;YAST,wBAAwB;IA2BtC,OAAO,CAAC,gBAAgB;IAUxB;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAexB,OAAO,CAAC,gBAAgB;IAalB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwB5B,iBAAiB,CAAC,SAAS,EAAE,WAAW,GAAG,IAAI;IAI/C,cAAc,IAAI,WAAW;IAI7B,qBAAqB,IAAI,IAAI;IAY7B,oBAAoB,IAAI,IAAI;IAO5B,oBAAoB,IAAI,IAAI;IAY5B,mBAAmB,IAAI,IAAI;IAOrB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB3B,OAAO,CAAC,sBAAsB;IAqBxB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAM9B,SAAS,IAAI,OAAO;IAIpB,kBAAkB,IAAI,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IA4BzL;;;;;;;OAOG;IACG,cAAc,CAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAClH,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,aAAa,EAAE,CAAA;KAAE,CAAC;IA4L1D;;;;;OAKG;IACG,oBAAoB,CACxB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,SAAS,EAAE;QACT,OAAO,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;QACtC,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC;QACjE,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;KAC/B,EACD,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAClH,OAAO,CAAC,MAAM,IAAI,CAAC;IAyNtB;;OAEG;IACH,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAI/C;;;;OAIG;IACH,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI;IAK/D;;;OAGG;IACH,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,EAAE;IAcrD,OAAO,CAAC,yBAAyB;IAYjC,gBAAgB,IAAI,MAAM;IAI1B,WAAW,IAAI,MAAM;IAIrB,YAAY,IAAI,SAAS;IAIzB,iBAAiB,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAIrD,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,UAAU,EAAE,YAAY,CAAC;IAI9G,iBAAiB,CACrB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,OAAO,CAAA;KAAE,GAC/B,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;IAsK/D,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASzC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,UAAU,EAAE,YAAY,CAAC;IAIzE,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,UAAU,EAAE,WAAW,CAAC;IAc/G,OAAO,CAAC,+BAA+B;IAsBjC,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAmB7F,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAW5I,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWxE;;;;OAIG;IACG,oBAAoB,CACxB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,UAAU,GAAG,SAAS,EAC/B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,SAAS,EAAE;QACT,OAAO,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;QACtC,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;QACnC,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;KAC/B,EACD,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAC1B,OAAO,CAAC,MAAM,IAAI,CAAC;IAiItB,OAAO,CAAC,iBAAiB;IA2CzB;;;OAGG;IACH,eAAe,IAAI,MAAM;IAIzB;;;;;;OAMG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;CAoBnC"}
|
package/dist/agent/runner.js
CHANGED
|
@@ -128,6 +128,12 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
128
128
|
this.pendingImagePaths = new Map();
|
|
129
129
|
// Buffers attachment file paths registered via api_reply tool for the current API session turn.
|
|
130
130
|
this.pendingApiAttachments = new Map();
|
|
131
|
+
// Session history ring-buffer (last 10 spawned, newest first)
|
|
132
|
+
this.sessionHistory = [];
|
|
133
|
+
// For API sessions the map key is the sessionId, so the real caller chatId
|
|
134
|
+
// (e.g. the app/agent identifier) is not captured at spawn. Record it here
|
|
135
|
+
// keyed by sessionId so the dashboard can show the actual chat id.
|
|
136
|
+
this.apiChatIds = new Map();
|
|
131
137
|
// Skill registry for detecting /skill-name commands in user messages
|
|
132
138
|
this.skillRegistry = { skills: new Map() };
|
|
133
139
|
// Cancel function for the daily history cleanup timer
|
|
@@ -619,6 +625,9 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
619
625
|
// Auto-forward result text to channel if agent didn't call reply tool.
|
|
620
626
|
let replyCalled = false;
|
|
621
627
|
let replyToolUseId = null; // track id to detect failed tool calls
|
|
628
|
+
// Set when an interactive-menu prompt was rendered to the channel this turn,
|
|
629
|
+
// so the result's plain-text auto-forward is skipped (no duplicate message).
|
|
630
|
+
let menuSentThisTurn = false;
|
|
622
631
|
let typingDoneTimer = null;
|
|
623
632
|
const TYPING_DONE_DELAY_MS = 3000;
|
|
624
633
|
const replyToolName = source === 'discord' ? 'mcp__gateway__discord_reply' : 'mcp__gateway__telegram_reply';
|
|
@@ -667,6 +676,31 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
667
676
|
}
|
|
668
677
|
}
|
|
669
678
|
}
|
|
679
|
+
// Interactive select menu blocking the PTY: render it as channel-native
|
|
680
|
+
// UI (inline buttons). This output handler only runs for Telegram/Discord
|
|
681
|
+
// sessions; API uses a separate path and falls back to the result text's
|
|
682
|
+
// numbered list (no buttons), as intended.
|
|
683
|
+
if (obj['type'] === 'system' && obj['subtype'] === 'menu_prompt') {
|
|
684
|
+
const promptText = typeof obj['prompt'] === 'string' ? obj['prompt'] : '';
|
|
685
|
+
const rawOptions = Array.isArray(obj['options']) ? obj['options'] : [];
|
|
686
|
+
const options = rawOptions
|
|
687
|
+
.map((o) => ({ label: typeof o?.label === 'string' ? o.label : '' }))
|
|
688
|
+
.filter((o) => o.label);
|
|
689
|
+
if (promptText && options.length) {
|
|
690
|
+
this.writeMenuForward(mapKey, promptText, options);
|
|
691
|
+
menuSentThisTurn = true;
|
|
692
|
+
// Persist the question to chat history so it's visible in the transcript.
|
|
693
|
+
const channelSrc = this.channelSourceMap.get(mapKey) ?? 'telegram';
|
|
694
|
+
this.historyDb.insertMessage({
|
|
695
|
+
chatId: `${channelSrc}-${mapKey}`,
|
|
696
|
+
sessionId: actualSessionId,
|
|
697
|
+
source: channelSrc,
|
|
698
|
+
role: 'assistant',
|
|
699
|
+
content: promptText,
|
|
700
|
+
ts: Date.now(),
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
670
704
|
if (obj['type'] === 'result') {
|
|
671
705
|
proc.setProcessing(false);
|
|
672
706
|
// Telegram: accumulate image size via stat of local file (FIFO, one per turn)
|
|
@@ -699,7 +733,9 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
699
733
|
// Suppress the corrupted-thinking 400: the session auto-respawns to recover,
|
|
700
734
|
// so the raw API error must not reach the user's chat or the history DB.
|
|
701
735
|
const isThinkingCorruption = obj['is_error'] === true && process_1.SessionProcess.isThinkingCorruptionError(resultText);
|
|
702
|
-
|
|
736
|
+
// Skip when a menu was rendered to buttons this turn — the result
|
|
737
|
+
// text is the same numbered list and would duplicate the menu message.
|
|
738
|
+
if (resultText.trim() && !proc.queryMode && !replyCalled && !isThinkingCorruption && !menuSentThisTurn) {
|
|
703
739
|
const text = resultText.trim();
|
|
704
740
|
const channelSrcForResult = this.channelSourceMap.get(mapKey) ?? 'telegram';
|
|
705
741
|
// Persist assistant reply to permanent history DB
|
|
@@ -721,6 +757,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
721
757
|
}
|
|
722
758
|
replyCalled = false; // reset for next turn
|
|
723
759
|
replyToolUseId = null;
|
|
760
|
+
menuSentThisTurn = false;
|
|
724
761
|
// Delay typing done — agent may continue with more work
|
|
725
762
|
typingDoneTimer = setTimeout(() => {
|
|
726
763
|
this.writeTypingDone(mapKey);
|
|
@@ -775,6 +812,9 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
775
812
|
});
|
|
776
813
|
}
|
|
777
814
|
this.sessions.set(mapKey, proc);
|
|
815
|
+
this.sessionHistory.unshift({ chatId: mapKey, sessionId: proc.sessionId, source: proc.source, mode: proc.backend, model: proc.model, spawnedAt: proc.spawnedAt });
|
|
816
|
+
if (this.sessionHistory.length > 10)
|
|
817
|
+
this.sessionHistory.length = 10;
|
|
778
818
|
if (source === 'telegram' || source === 'discord') {
|
|
779
819
|
this.channelSourceMap.set(mapKey, source);
|
|
780
820
|
}
|
|
@@ -1112,6 +1152,27 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1112
1152
|
// Non-fatal
|
|
1113
1153
|
}
|
|
1114
1154
|
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Signal the channel receiver to render an interactive menu as native UI
|
|
1157
|
+
* (inline buttons on Telegram/Discord). The receiver maps each option to a
|
|
1158
|
+
* `choice:N` callback whose tap arrives back as a normal "N" message — the
|
|
1159
|
+
* same as the user typing the number.
|
|
1160
|
+
*/
|
|
1161
|
+
writeMenuForward(chatId, text, options) {
|
|
1162
|
+
const typingDir = this.getTypingDir(chatId);
|
|
1163
|
+
try {
|
|
1164
|
+
fs.mkdirSync(typingDir, { recursive: true });
|
|
1165
|
+
// Write atomically (tmp + rename): the receiver polls this directory every
|
|
1166
|
+
// second, so a non-atomic write could be read mid-flush as truncated JSON.
|
|
1167
|
+
const finalPath = path.join(typingDir, `${chatId}.menu`);
|
|
1168
|
+
const tmpPath = `${finalPath}.tmp`;
|
|
1169
|
+
fs.writeFileSync(tmpPath, JSON.stringify({ text, options }));
|
|
1170
|
+
fs.renameSync(tmpPath, finalPath);
|
|
1171
|
+
}
|
|
1172
|
+
catch {
|
|
1173
|
+
// Non-fatal — the result text still carries the numbered list as a fallback.
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1115
1176
|
startIdleCleaner() {
|
|
1116
1177
|
this.idleCleanerTimer = setInterval(async () => {
|
|
1117
1178
|
for (const [id, proc] of this.sessions) {
|
|
@@ -1220,6 +1281,34 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1220
1281
|
isRunning() {
|
|
1221
1282
|
return this.receiver?.isRunning() ?? false;
|
|
1222
1283
|
}
|
|
1284
|
+
getSessionsSummary() {
|
|
1285
|
+
const now = Date.now();
|
|
1286
|
+
// A single logical session can appear multiple times in the ring buffer: a
|
|
1287
|
+
// restart / model-switch / error-recovery respawn pushes a fresh entry with
|
|
1288
|
+
// the SAME sessionId but a new spawnedAt. History is newest-first (unshift),
|
|
1289
|
+
// so the first occurrence of a (source, sessionId, chatId) is the current
|
|
1290
|
+
// incarnation — collapse to it so the dashboard shows one row per session
|
|
1291
|
+
// instead of duplicating it.
|
|
1292
|
+
const seen = new Set();
|
|
1293
|
+
const out = [];
|
|
1294
|
+
for (const e of this.sessionHistory) {
|
|
1295
|
+
const dedupKey = `${e.source}:${e.sessionId}:${e.chatId}`;
|
|
1296
|
+
if (seen.has(dedupKey))
|
|
1297
|
+
continue;
|
|
1298
|
+
seen.add(dedupKey);
|
|
1299
|
+
const proc = this.sessions.get(e.chatId);
|
|
1300
|
+
// Running only when the live process IS this incarnation: a stale entry
|
|
1301
|
+
// from before a respawn must not borrow the new process's running state.
|
|
1302
|
+
const isRunning = !!proc && proc.spawnedAt === e.spawnedAt;
|
|
1303
|
+
// For API sessions the ring-buffer chatId equals the sessionId (the map key);
|
|
1304
|
+
// surface the real caller chatId instead when we have it.
|
|
1305
|
+
const chatId = e.source === 'api' ? (this.apiChatIds.get(e.sessionId) ?? e.chatId) : e.chatId;
|
|
1306
|
+
// Live context-window token usage from the running process (0 when stopped).
|
|
1307
|
+
const tokens = isRunning ? (proc?.totalTokens ?? 0) : 0;
|
|
1308
|
+
out.push({ ...e, chatId, isRunning, uptimeSec: Math.floor((now - e.spawnedAt) / 1000), tokens });
|
|
1309
|
+
}
|
|
1310
|
+
return out;
|
|
1311
|
+
}
|
|
1223
1312
|
/**
|
|
1224
1313
|
* Send a message to an API session and wait for the response.
|
|
1225
1314
|
*
|
|
@@ -1239,6 +1328,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1239
1328
|
});
|
|
1240
1329
|
// Use model from request body if provided, otherwise use agent default
|
|
1241
1330
|
const session = await this.getOrSpawnSession(sessionId, 'api', undefined, opts.model);
|
|
1331
|
+
this.apiChatIds.set(sessionId, chatId);
|
|
1242
1332
|
// Promote UI-uploaded files from staging to permanent per-session storage
|
|
1243
1333
|
const finalMediaFiles = opts.mediaFiles?.length
|
|
1244
1334
|
? await promoteUiUploads(this.agentsBaseDir, this.agentConfig.id, sessionId, opts.mediaFiles, this.logger)
|
|
@@ -1302,8 +1392,9 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1302
1392
|
const done = (result) => {
|
|
1303
1393
|
cleanup();
|
|
1304
1394
|
const attachments = this.popApiAttachments(sessionId);
|
|
1305
|
-
// Persist assistant reply
|
|
1306
|
-
|
|
1395
|
+
// Persist assistant reply. Image-only replies (empty text but attachments
|
|
1396
|
+
// present) must also persist — otherwise the screenshot vanishes from history.
|
|
1397
|
+
if (result.trim() || attachments.length) {
|
|
1307
1398
|
const apiAssistantTs = Date.now();
|
|
1308
1399
|
this.sessionStore
|
|
1309
1400
|
.appendMessage(this.agentConfig.id, sessionId, {
|
|
@@ -1318,6 +1409,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1318
1409
|
source: 'api',
|
|
1319
1410
|
role: 'assistant',
|
|
1320
1411
|
content: result.trim(),
|
|
1412
|
+
mediaFiles: attachments.length ? attachments.map((a) => `media/${a.relPath}`) : undefined,
|
|
1321
1413
|
ts: apiAssistantTs,
|
|
1322
1414
|
});
|
|
1323
1415
|
}
|
|
@@ -1409,6 +1501,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1409
1501
|
});
|
|
1410
1502
|
// Use model from request body if provided, otherwise use agent default
|
|
1411
1503
|
const session = await this.getOrSpawnSession(sessionId, 'api', undefined, opts.model);
|
|
1504
|
+
this.apiChatIds.set(sessionId, chatId);
|
|
1412
1505
|
// Promote UI-uploaded files from staging to permanent per-session storage
|
|
1413
1506
|
const finalMediaFilesStream = opts.mediaFiles?.length
|
|
1414
1507
|
? await promoteUiUploads(this.agentsBaseDir, this.agentConfig.id, sessionId, opts.mediaFiles, this.logger)
|
|
@@ -1449,8 +1542,10 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1449
1542
|
settled = true;
|
|
1450
1543
|
cleanup();
|
|
1451
1544
|
const attachments = this.popApiAttachments(sessionId);
|
|
1452
|
-
// Persist assistant reply regardless of whether the SSE client is still connected
|
|
1453
|
-
|
|
1545
|
+
// Persist assistant reply regardless of whether the SSE client is still connected.
|
|
1546
|
+
// Image-only replies (empty text but attachments present) must also persist —
|
|
1547
|
+
// otherwise the screenshot vanishes from history once the stream ends.
|
|
1548
|
+
if (result.trim() || attachments.length) {
|
|
1454
1549
|
const streamAssistantTs = Date.now();
|
|
1455
1550
|
this.sessionStore
|
|
1456
1551
|
.appendMessage(this.agentConfig.id, sessionId, {
|
|
@@ -1465,6 +1560,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1465
1560
|
source: 'api',
|
|
1466
1561
|
role: 'assistant',
|
|
1467
1562
|
content: result.trim(),
|
|
1563
|
+
mediaFiles: attachments.length ? attachments.map((a) => `media/${a.relPath}`) : undefined,
|
|
1468
1564
|
ts: streamAssistantTs,
|
|
1469
1565
|
});
|
|
1470
1566
|
}
|
|
@@ -1621,11 +1717,13 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1621
1717
|
if (!fs.existsSync(absPath))
|
|
1622
1718
|
return null;
|
|
1623
1719
|
const rel = absPath.slice(mediaRoot.length).replace(/\\/g, '/');
|
|
1624
|
-
return { type: 'image', url: `/v1/agents/${encodeURIComponent(this.agentConfig.id)}/media/${rel}
|
|
1720
|
+
return { type: 'image', url: `/v1/agents/${encodeURIComponent(this.agentConfig.id)}/media/${rel}`, relPath: rel };
|
|
1625
1721
|
})
|
|
1626
1722
|
.filter((a) => a !== null);
|
|
1627
1723
|
}
|
|
1628
1724
|
cleanupApiSessionMediaDir(sessionId, source) {
|
|
1725
|
+
// Always evict the chat-id mapping — safe no-op for non-api sessions.
|
|
1726
|
+
this.apiChatIds.delete(sessionId);
|
|
1629
1727
|
if (source !== 'api')
|
|
1630
1728
|
return;
|
|
1631
1729
|
const mediaDir = path.join(this.agentsBaseDir, this.agentConfig.id, 'media', `api-${sessionId}`);
|
|
@@ -1651,73 +1749,171 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1651
1749
|
async listSessionsForChat(chatId, channel) {
|
|
1652
1750
|
return this.sessionStore.listSessions(this.agentConfig.id, chatId, channel);
|
|
1653
1751
|
}
|
|
1654
|
-
async executeApiCommand(sessionId, chatId, command) {
|
|
1752
|
+
async executeApiCommand(sessionId, chatId, command, opts) {
|
|
1655
1753
|
const agentId = this.agentConfig.id;
|
|
1656
1754
|
const storeChatId = chatId; // sessionStore adds channel prefix internally
|
|
1657
1755
|
const dbChatId = `api-${chatId}`; // historyDb uses full channel-chatId key
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1756
|
+
const skipPersist = opts?.skipPersist ?? false;
|
|
1757
|
+
// Dispatch on the first token so commands with arguments (e.g. "/model sonnet",
|
|
1758
|
+
// "/stop now") resolve to their base command instead of falling through to the
|
|
1759
|
+
// unknown-command branch. The router gate is prefix-based, so these already pass.
|
|
1760
|
+
const cmd = command.trim().split(/\s+/)[0] ?? '';
|
|
1761
|
+
// Validate BEFORE persisting anything. Reuse the same api-channel gate as the router
|
|
1762
|
+
// so an unknown command throws with nothing written — no orphan user row is left, and
|
|
1763
|
+
// the router surfaces the error/500 exactly as before. After token dispatch + the
|
|
1764
|
+
// /sessions branch below, this is only reachable on a direct caller passing garbage.
|
|
1765
|
+
if (!AgentRunner.isApiBuiltinCommand(cmd)) {
|
|
1766
|
+
throw new Error(`Unknown command: ${cmd}`);
|
|
1767
|
+
}
|
|
1768
|
+
// Register session in the api-{chatId} index on first use (same as sendApiMessageStream)
|
|
1769
|
+
await this.sessionStore.ensureApiSession(agentId, storeChatId, sessionId).catch((err) => {
|
|
1770
|
+
this.logger.warn('Failed to register API session in index for command', { agentId, chatId, sessionId, error: err.message });
|
|
1771
|
+
});
|
|
1772
|
+
// Persist a command message to the permanent history DB ONLY — not the model's session
|
|
1773
|
+
// context (sessionStore). historyDb is what the web history endpoint reads, so this is
|
|
1774
|
+
// all the display + spinner fix needs. Keeping commands out of the JSONL context means
|
|
1775
|
+
// /model replies aren't replayed into the model, /compact counts stay accurate, /clear
|
|
1776
|
+
// leaves no dangling turn, and the api channel matches telegram (which persists commands
|
|
1777
|
+
// nowhere).
|
|
1778
|
+
//
|
|
1779
|
+
// skipPersist (wire param store_user_message=false) suppresses BOTH the user command and
|
|
1780
|
+
// the assistant reply: programmatic REST callers leave no trace in chat history at all.
|
|
1781
|
+
const persist = (role, content) => {
|
|
1782
|
+
if (skipPersist || !content)
|
|
1783
|
+
return;
|
|
1784
|
+
this.historyDb.insertMessage({ chatId: dbChatId, sessionId, source: 'api', role, content, ts: Date.now() });
|
|
1785
|
+
};
|
|
1786
|
+
// Persist the full user command before executing so it appears in history.
|
|
1787
|
+
// Skip for /clear — clearSession() below wipes the table anyway; only the response survives.
|
|
1788
|
+
if (cmd !== '/clear') {
|
|
1789
|
+
persist('user', command);
|
|
1688
1790
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1791
|
+
let result;
|
|
1792
|
+
let responseText;
|
|
1793
|
+
try {
|
|
1794
|
+
if (cmd === '/model') {
|
|
1795
|
+
const model = this.agentConfig.claude.model;
|
|
1796
|
+
const hasArg = command.trim().includes(' ');
|
|
1797
|
+
result = { model };
|
|
1798
|
+
responseText = hasArg
|
|
1799
|
+
? `Current model: ${model}\n(To switch models use the model picker or the /api/v1/agents/:id/model endpoint — argument ignored.)`
|
|
1800
|
+
: `Current model: ${model}`;
|
|
1801
|
+
}
|
|
1802
|
+
else if (cmd === '/stop') {
|
|
1803
|
+
const session = this.sessions.get(sessionId);
|
|
1804
|
+
const stopped = session ? session.interrupt() : false;
|
|
1805
|
+
result = { stopped };
|
|
1806
|
+
responseText = stopped ? 'Session interrupted.' : 'No active session to stop.';
|
|
1807
|
+
}
|
|
1808
|
+
else if (cmd === '/restart') {
|
|
1809
|
+
this.restartProcess(sessionId).catch(() => { });
|
|
1810
|
+
result = { restarting: true };
|
|
1811
|
+
responseText = 'Session is restarting.';
|
|
1812
|
+
}
|
|
1813
|
+
else if (cmd === '/session') {
|
|
1814
|
+
const index = await this.sessionStore.listSessions(agentId, storeChatId, 'api').catch(() => null);
|
|
1815
|
+
const meta = index?.sessions.find((s) => s.id === sessionId);
|
|
1816
|
+
const effectiveModel = this.agentConfig.claude.model;
|
|
1817
|
+
// The api append path doesn't maintain the index messageCount, so count the flat store
|
|
1818
|
+
// (the real conversation) directly rather than trusting meta.messageCount.
|
|
1819
|
+
const messageCount = (await this.sessionStore.loadSession(agentId, sessionId).catch(() => [])).length;
|
|
1820
|
+
if (!meta) {
|
|
1821
|
+
result = { sessionId, sessionName: null, messageCount, archivedCount: 0, contextUsedPct: 0, model: effectiveModel };
|
|
1822
|
+
responseText = `Session: (unnamed)\nMessages: ${messageCount}\nContext used: 0%\nModel: ${effectiveModel}`;
|
|
1823
|
+
}
|
|
1824
|
+
else {
|
|
1825
|
+
const availableModels = this.gatewayConfig.gateway.models ?? exports.DEFAULT_MODELS;
|
|
1826
|
+
const modelConfig = availableModels.find((m) => m.id === effectiveModel);
|
|
1827
|
+
const contextWindow = modelConfig?.contextWindow ?? 200000;
|
|
1828
|
+
const contextUsedPct = Math.round(((meta.lastInputTokens ?? 0) / contextWindow) * 100);
|
|
1829
|
+
result = { sessionId, sessionName: meta.name, messageCount, archivedCount: meta.archivedCount ?? 0, contextUsedPct, model: effectiveModel };
|
|
1830
|
+
responseText = [
|
|
1831
|
+
`Session: ${meta.name ?? '(unnamed)'}`,
|
|
1832
|
+
`Messages: ${messageCount}${(meta.archivedCount ?? 0) > 0 ? ` (${meta.archivedCount} archived)` : ''}`,
|
|
1833
|
+
`Context used: ${contextUsedPct}%`,
|
|
1834
|
+
`Model: ${effectiveModel}`,
|
|
1835
|
+
].join('\n');
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
else if (cmd === '/sessions') {
|
|
1839
|
+
// Advertised for the api channel (BUILTIN_COMMANDS), so handle it here — mirrors the
|
|
1840
|
+
// telegram /sessions list. Marks the session this command runs in as (current).
|
|
1841
|
+
const index = await this.sessionStore.listSessions(agentId, storeChatId, 'api').catch(() => null);
|
|
1842
|
+
const list = index?.sessions ?? [];
|
|
1843
|
+
// Count each session's flat store directly — the api append path doesn't keep the
|
|
1844
|
+
// index messageCount in sync.
|
|
1845
|
+
const withCounts = await Promise.all(list.map(async (s) => ({
|
|
1846
|
+
id: s.id,
|
|
1847
|
+
name: s.name,
|
|
1848
|
+
messageCount: (await this.sessionStore.loadSession(agentId, s.id).catch(() => [])).length,
|
|
1849
|
+
current: s.id === sessionId,
|
|
1850
|
+
})));
|
|
1851
|
+
result = { sessions: withCounts, count: withCounts.length };
|
|
1852
|
+
if (!withCounts.length) {
|
|
1853
|
+
responseText = 'No sessions.';
|
|
1854
|
+
}
|
|
1855
|
+
else {
|
|
1856
|
+
const lines = withCounts.map((s) => {
|
|
1857
|
+
const n = s.messageCount;
|
|
1858
|
+
const marker = s.current ? ' (current)' : '';
|
|
1859
|
+
return `• ${s.name ?? '(unnamed)'} — ${n} message${n === 1 ? '' : 's'}${marker}`;
|
|
1860
|
+
});
|
|
1861
|
+
responseText = `Sessions (${withCounts.length}):\n${lines.join('\n')}`;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
else if (cmd === '/clear') {
|
|
1865
|
+
const ch = 'api';
|
|
1866
|
+
await this.sessionStore.clearTelegramSessionHistory(agentId, storeChatId, sessionId, ch);
|
|
1867
|
+
await this.sessionStore.updateSessionMeta(agentId, storeChatId, sessionId, {
|
|
1868
|
+
totalTokensUsed: 0,
|
|
1869
|
+
lastInputTokens: 0,
|
|
1870
|
+
archivedCount: 0,
|
|
1871
|
+
loadedAtSpawn: undefined,
|
|
1872
|
+
messageCountAtSpawn: undefined,
|
|
1873
|
+
}, ch);
|
|
1874
|
+
const mediaPaths = this.historyDb.clearSession(dbChatId, sessionId);
|
|
1875
|
+
media_store_1.MediaStore.deleteMediaFiles(this.agentsBaseDir, agentId, mediaPaths);
|
|
1876
|
+
this.restartProcess(sessionId).catch(() => { });
|
|
1877
|
+
result = { success: true };
|
|
1878
|
+
responseText = 'Session cleared.';
|
|
1879
|
+
}
|
|
1880
|
+
else if (cmd === '/compact') {
|
|
1881
|
+
const ch = 'api';
|
|
1882
|
+
const compactEffectiveModel = this.agentConfig.claude.model;
|
|
1883
|
+
const availableModels = this.gatewayConfig.gateway.models ?? exports.DEFAULT_MODELS;
|
|
1884
|
+
const modelConfig = availableModels.find((m) => m.id === compactEffectiveModel);
|
|
1885
|
+
const contextWindow = modelConfig?.contextWindow ?? 200000;
|
|
1886
|
+
const compactor = new compactor_1.SessionCompactor(this.sessionStore);
|
|
1887
|
+
const compactResult = await compactor.compact(agentId, storeChatId, sessionId, compactEffectiveModel, contextWindow, ch);
|
|
1888
|
+
await this.sessionStore.updateSessionMeta(agentId, storeChatId, sessionId, {
|
|
1889
|
+
loadedAtSpawn: undefined,
|
|
1890
|
+
archivedCount: undefined,
|
|
1891
|
+
messageCountAtSpawn: undefined,
|
|
1892
|
+
}, ch);
|
|
1893
|
+
await this.restartProcess(sessionId);
|
|
1894
|
+
result = { success: true, keptMessages: compactResult.afterMessages, archivedMessages: compactResult.beforeMessages - compactResult.afterMessages };
|
|
1895
|
+
responseText = `Session compacted. Kept ${compactResult.afterMessages} messages, archived ${compactResult.beforeMessages - compactResult.afterMessages}.`;
|
|
1896
|
+
}
|
|
1897
|
+
else {
|
|
1898
|
+
// Passed the gate but has no dispatch branch — BUILTIN_COMMANDS drifted from this
|
|
1899
|
+
// table. Throw so the failure surfaces (and ends history with an assistant turn via
|
|
1900
|
+
// the catch below) rather than returning an empty response.
|
|
1901
|
+
throw new Error(`Unhandled command: ${cmd}`);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
catch (err) {
|
|
1905
|
+
// Ensure history always ends with an assistant turn, even when execution fails midway
|
|
1906
|
+
// (e.g. /compact throws after the user row is persisted). Otherwise the trailing user
|
|
1907
|
+
// row re-triggers the web's stuck-spinner condition on reload. Rethrow so the router
|
|
1908
|
+
// still emits its SSE error event / REST 500.
|
|
1909
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1910
|
+
persist('assistant', `Command failed: ${errMsg}`);
|
|
1911
|
+
throw err;
|
|
1719
1912
|
}
|
|
1720
|
-
|
|
1913
|
+
// Persist the human-readable response as an assistant turn so the conversation ends with
|
|
1914
|
+
// an assistant message (the web's computeIsPendingResponse needs last.role !== 'user').
|
|
1915
|
+
persist('assistant', responseText);
|
|
1916
|
+
return { result, responseText };
|
|
1721
1917
|
}
|
|
1722
1918
|
async setModel(newModel) {
|
|
1723
1919
|
// Allow any model string through — BYOK/third-party models (openrouter/* etc.)
|