@0xmaxma/claude-gateway 1.3.25 → 1.3.32

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.
Files changed (72) hide show
  1. package/README.md +50 -2
  2. package/config.template.json +6 -2
  3. package/dist/agent/incident-store.d.ts +89 -0
  4. package/dist/agent/incident-store.d.ts.map +1 -0
  5. package/dist/agent/incident-store.js +299 -0
  6. package/dist/agent/incident-store.js.map +1 -0
  7. package/dist/agent/incident.d.ts +156 -0
  8. package/dist/agent/incident.d.ts.map +1 -0
  9. package/dist/agent/incident.js +177 -0
  10. package/dist/agent/incident.js.map +1 -0
  11. package/dist/agent/recovery-executor.d.ts +117 -0
  12. package/dist/agent/recovery-executor.d.ts.map +1 -0
  13. package/dist/agent/recovery-executor.js +168 -0
  14. package/dist/agent/recovery-executor.js.map +1 -0
  15. package/dist/agent/recovery-policy.d.ts +97 -0
  16. package/dist/agent/recovery-policy.d.ts.map +1 -0
  17. package/dist/agent/recovery-policy.js +164 -0
  18. package/dist/agent/recovery-policy.js.map +1 -0
  19. package/dist/agent/runner.d.ts +66 -6
  20. package/dist/agent/runner.d.ts.map +1 -1
  21. package/dist/agent/runner.js +332 -27
  22. package/dist/agent/runner.js.map +1 -1
  23. package/dist/agent/safe-mode.d.ts +61 -0
  24. package/dist/agent/safe-mode.d.ts.map +1 -0
  25. package/dist/agent/safe-mode.js +102 -0
  26. package/dist/agent/safe-mode.js.map +1 -0
  27. package/dist/agent/triage.d.ts +94 -0
  28. package/dist/agent/triage.d.ts.map +1 -0
  29. package/dist/agent/triage.js +209 -0
  30. package/dist/agent/triage.js.map +1 -0
  31. package/dist/agent/turn-trace.d.ts +120 -0
  32. package/dist/agent/turn-trace.d.ts.map +1 -0
  33. package/dist/agent/turn-trace.js +122 -0
  34. package/dist/agent/turn-trace.js.map +1 -0
  35. package/dist/api/gateway-router.d.ts +21 -0
  36. package/dist/api/gateway-router.d.ts.map +1 -1
  37. package/dist/api/gateway-router.js +58 -17
  38. package/dist/api/gateway-router.js.map +1 -1
  39. package/dist/api/router.d.ts.map +1 -1
  40. package/dist/api/router.js +104 -4
  41. package/dist/api/router.js.map +1 -1
  42. package/dist/config/migrator.d.ts +4 -0
  43. package/dist/config/migrator.d.ts.map +1 -1
  44. package/dist/config/migrator.js +60 -3
  45. package/dist/config/migrator.js.map +1 -1
  46. package/dist/history/db.d.ts +13 -1
  47. package/dist/history/db.d.ts.map +1 -1
  48. package/dist/history/db.js +67 -9
  49. package/dist/history/db.js.map +1 -1
  50. package/dist/history/types.d.ts +10 -0
  51. package/dist/history/types.d.ts.map +1 -1
  52. package/dist/index.js +3 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/session/process.d.ts +26 -0
  55. package/dist/session/process.d.ts.map +1 -1
  56. package/dist/session/process.js +77 -1
  57. package/dist/session/process.js.map +1 -1
  58. package/dist/shell/claude-pty-shell.js +59 -0
  59. package/dist/shell/claude-pty-shell.js.map +1 -1
  60. package/dist/shell/control-channel.d.ts +74 -0
  61. package/dist/shell/control-channel.d.ts.map +1 -0
  62. package/dist/shell/control-channel.js +114 -0
  63. package/dist/shell/control-channel.js.map +1 -0
  64. package/dist/types.d.ts +22 -0
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/ui/web-ui.d.ts.map +1 -1
  67. package/dist/ui/web-ui.js +103 -2
  68. package/dist/ui/web-ui.js.map +1 -1
  69. package/mcp/tools/skills/handlers.ts +4 -2
  70. package/mcp/tools/telegram/receiver-server.ts +163 -0
  71. package/mcp/tools/telegram/typing.ts +124 -0
  72. package/package.json +3 -1
@@ -13,6 +13,17 @@
13
13
  * STATE_DIR/typing/{chatId}.error — written by AgentRunner on session failure
14
14
  */
15
15
 
16
+ // Import compiled dist/, not raw src/ — src/ is not published (files: ["mcp/"]),
17
+ // so a src/ import crashes this bun-run tool on installed packages. Enforced by
18
+ // tests/unit/mcp-no-src-imports.test.ts.
19
+ import {
20
+ classifyTurn,
21
+ type TurnObservation,
22
+ type TurnStage,
23
+ type TurnIncidentSink,
24
+ type TurnIncidentEvidence,
25
+ } from '../../../dist/agent/turn-trace.js'
26
+
16
27
  export const TELEGRAM_MAX_CHARS = 4096
17
28
 
18
29
  /**
@@ -145,6 +156,10 @@ export const ERROR_MESSAGES: Record<string, string> = {
145
156
  PROCESS_FAILED: '❌ Claude stopped unexpectedly. Please try sending a new message.',
146
157
  POOL_FULL: '⚠️ Too many concurrent sessions. Please try again in a moment.',
147
158
  SPAWN_FAILED: '❌ Failed to start Claude session. Please try again.',
159
+ // Epic #195, Phase 3: the interactive backend failed repeatedly, so the agent
160
+ // temporarily fell back to the headless backend to keep serving.
161
+ SAFE_MODE_ENABLED:
162
+ '⚠️ The interactive backend kept failing, so I switched to safe mode (headless) for now. Please resend your message — it should go through.',
148
163
  }
149
164
 
150
165
  export const STATUS_EMOJI: Record<string, string> = {
@@ -177,6 +192,10 @@ export interface WorkingState {
177
192
  currentReaction: string | null
178
193
  lastDetail: string | null
179
194
  recentDetails: string[]
195
+ /** Last stage an incident was raised for — dedupes the turn-trace watchdog
196
+ * so one contiguous stalled episode emits a single incident, not one per
197
+ * 15s tick. Reset to null once the turn is no longer stalled. */
198
+ lastIncidentStage: TurnStage | null
180
199
  }
181
200
 
182
201
  export interface BotApi {
@@ -295,6 +314,7 @@ export function createWorkingStateManager(
295
314
  typingDir: string,
296
315
  botApi: BotApi,
297
316
  fsApi: FsApi,
317
+ onIncident?: TurnIncidentSink,
298
318
  ) {
299
319
  const states = new Map<string, WorkingState>()
300
320
 
@@ -330,6 +350,106 @@ export function createWorkingStateManager(
330
350
  return `${typingDir}/${chatId}.replied`
331
351
  }
332
352
 
353
+ function menuFilePath(chatId: string): string {
354
+ return `${typingDir}/${chatId}.menu`
355
+ }
356
+
357
+ // ─── Turn-trace watchdog (Epic #195, Phase 1) ──────────────────────────────
358
+ // Read-only: builds an observation of this turn's on-disk artifacts, classifies
359
+ // the current pipeline stage, and emits a (silent) incident when a stage stalls.
360
+ // It never mutates state or messages the user — the existing heartbeat warn/stop
361
+ // below remains the sole user-facing stalled behaviour.
362
+
363
+ function statMtime(path: string): number | null {
364
+ try {
365
+ return fsApi.existsSync(path) ? fsApi.statSync(path).mtimeMs : null
366
+ } catch {
367
+ return null
368
+ }
369
+ }
370
+
371
+ function readTurnObservation(chatId: string, startedAt: number): TurnObservation {
372
+ let statusLabel: string | null = null
373
+ const statusPath = statusFilePath(chatId)
374
+ if (fsApi.existsSync(statusPath)) {
375
+ try {
376
+ statusLabel = parseStatusFile(fsApi.readFileSync(statusPath, 'utf8')).status
377
+ } catch {}
378
+ }
379
+ return {
380
+ now: Date.now(),
381
+ // A live WorkingState means the signal file was written at startedAt.
382
+ signalAt: fsApi.existsSync(typingFilePath(chatId)) ? startedAt : null,
383
+ statusLabel,
384
+ heartbeatAt: statMtime(heartbeatFilePath(chatId)),
385
+ processingAt: statMtime(processingFilePath(chatId)),
386
+ forwardAt: statMtime(forwardFilePath(chatId)),
387
+ menuAt: statMtime(menuFilePath(chatId)),
388
+ repliedPresent: fsApi.existsSync(repliedFilePath(chatId)),
389
+ errorPresent: fsApi.existsSync(errorFilePath(chatId)),
390
+ }
391
+ }
392
+
393
+ function readFileSafe(path: string): string | null {
394
+ try {
395
+ return fsApi.existsSync(path) ? fsApi.readFileSync(path, 'utf8') : null
396
+ } catch {
397
+ return null
398
+ }
399
+ }
400
+
401
+ // Build the diagnostic evidence bundle for an incident from the same
402
+ // observation the classifier used, plus the raw status/error file contents.
403
+ // Cheap and read-only — no listing of the whole typing dir, just this turn's
404
+ // known artifacts. The store scrubs everything before it is persisted.
405
+ function readTurnEvidence(chatId: string, obs: TurnObservation): TurnIncidentEvidence {
406
+ const artifacts: string[] = []
407
+ if (obs.signalAt !== null) artifacts.push('signal')
408
+ if (obs.statusLabel !== null) artifacts.push(`status=${obs.statusLabel}`)
409
+ if (obs.heartbeatAt !== null) artifacts.push('heartbeat')
410
+ if (obs.processingAt !== null) artifacts.push('processing')
411
+ if (obs.forwardAt !== null) artifacts.push('forward')
412
+ if (obs.menuAt !== null) artifacts.push('menu')
413
+ if (obs.repliedPresent) artifacts.push('replied')
414
+ if (obs.errorPresent) artifacts.push('error')
415
+ return {
416
+ artifacts,
417
+ statusText: readFileSafe(statusFilePath(chatId)),
418
+ errorText: readFileSafe(errorFilePath(chatId)),
419
+ }
420
+ }
421
+
422
+ function checkTurnTrace(chatId: string, startedAt: number): void {
423
+ const state = states.get(chatId)
424
+ if (!state) return
425
+ const obs = readTurnObservation(chatId, startedAt)
426
+ const trace = classifyTurn(obs)
427
+ if (!trace.stalled) {
428
+ state.lastIncidentStage = null
429
+ return
430
+ }
431
+ // Dedupe: one incident per contiguous stalled-stage episode.
432
+ if (state.lastIncidentStage === trace.stage) return
433
+ state.lastIncidentStage = trace.stage
434
+ if (onIncident) {
435
+ // A fresh .processing sentinel (mtime >= startedAt) means the turn is
436
+ // genuinely mid-work (e.g. a long sub-agent), not silently wedged.
437
+ const midTurn = obs.processingAt !== null && obs.processingAt >= startedAt
438
+ onIncident(
439
+ {
440
+ chatId,
441
+ stage: trace.stage,
442
+ failureClass: trace.failureClass,
443
+ sinceMs: trace.sinceMs,
444
+ budgetMs: trace.budgetMs,
445
+ midTurn,
446
+ at: Date.now(),
447
+ },
448
+ readTurnEvidence(chatId, obs),
449
+ )
450
+ }
451
+ }
452
+
333
453
  async function stop(chatId: string): Promise<void> {
334
454
  const state = states.get(chatId)
335
455
  if (!state) return
@@ -430,6 +550,7 @@ export function createWorkingStateManager(
430
550
  currentReaction: null,
431
551
  lastDetail: null,
432
552
  recentDetails: [],
553
+ lastIncidentStage: null,
433
554
  }
434
555
  states.set(chatId, state)
435
556
 
@@ -530,6 +651,9 @@ export function createWorkingStateManager(
530
651
  // Heartbeat file is written by SessionProcess on every Claude stdout line.
531
652
  state.stalledInterval = setInterval(async () => {
532
653
  if (!states.has(chatId)) return
654
+ // Turn-trace watchdog: staged classification + incident telemetry. Silent
655
+ // and side-effect-free — the heartbeat warn/stop below is unchanged.
656
+ checkTurnTrace(chatId, startedAt)
533
657
  const hbPath = heartbeatFilePath(chatId)
534
658
  let lastActivity = startedAt
535
659
  if (fsApi.existsSync(hbPath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xmaxma/claude-gateway",
3
- "version": "1.3.25",
3
+ "version": "1.3.32",
4
4
  "description": "Multi-agent gateway for Claude",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,7 +28,9 @@
28
28
  "build": "tsc",
29
29
  "start": "node --no-warnings=ExperimentalWarning --env-file-if-exists=.env dist/index.js",
30
30
  "postinstall": "if command -v bun >/dev/null 2>&1; then bun install --cwd mcp; else echo 'Warning: bun not found. Run `cd mcp && bun install` manually to enable MCP tools.'; fi",
31
+ "pretest": "npm run build",
31
32
  "test": "jest",
33
+ "pretest:unit": "npm run build",
32
34
  "test:unit": "jest --testPathPattern=unit",
33
35
  "test:e2e": "jest --testPathPattern=tests/e2e --forceExit",
34
36
  "integration": "jest --testPathPattern=integration --testTimeout=15000",