agent-relay 2.0.28 → 2.0.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.
- package/README.md +19 -0
- package/dist/index.cjs +85691 -0
- package/dist/src/bridge/index.d.ts.map +1 -0
- package/dist/src/bridge/index.js.map +1 -0
- package/dist/src/cli/commands/doctor.d.ts +2 -0
- package/dist/src/cli/commands/doctor.d.ts.map +1 -0
- package/dist/src/cli/commands/doctor.js +451 -0
- package/dist/src/cli/commands/doctor.js.map +1 -0
- package/dist/src/cli/index.d.ts.map +1 -0
- package/dist/src/cli/index.js +29 -1
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/config/relay-config.d.ts.map +1 -0
- package/dist/src/config/relay-config.js.map +1 -0
- package/dist/src/continuity/index.d.ts.map +1 -0
- package/dist/src/continuity/index.js.map +1 -0
- package/dist/src/daemon/index.d.ts.map +1 -0
- package/dist/src/daemon/index.js.map +1 -0
- package/dist/src/health-worker-manager.d.ts.map +1 -0
- package/dist/src/health-worker-manager.js.map +1 -0
- package/dist/src/health-worker.d.ts.map +1 -0
- package/dist/src/health-worker.js.map +1 -0
- package/dist/src/hooks/index.d.ts.map +1 -0
- package/dist/src/hooks/index.js.map +1 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/memory/index.d.ts.map +1 -0
- package/dist/src/memory/index.js.map +1 -0
- package/dist/src/policy/index.d.ts.map +1 -0
- package/dist/src/policy/index.js.map +1 -0
- package/dist/src/protocol/index.d.ts.map +1 -0
- package/dist/src/protocol/index.js.map +1 -0
- package/dist/src/resiliency/index.d.ts.map +1 -0
- package/dist/src/resiliency/index.js.map +1 -0
- package/dist/src/shared/cli-auth-config.d.ts.map +1 -0
- package/dist/src/shared/cli-auth-config.js.map +1 -0
- package/dist/src/state/index.d.ts.map +1 -0
- package/dist/src/state/index.js.map +1 -0
- package/dist/src/storage/index.d.ts.map +1 -0
- package/dist/src/storage/index.js.map +1 -0
- package/dist/src/trajectory/index.d.ts.map +1 -0
- package/dist/src/trajectory/index.js.map +1 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/wrapper/index.d.ts.map +1 -0
- package/dist/src/wrapper/index.js.map +1 -0
- package/package.json +83 -20
- package/packages/api-types/dist/index.d.ts.map +1 -0
- package/packages/api-types/dist/index.js.map +1 -0
- package/packages/api-types/dist/schemas/agent.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/agent.js.map +1 -0
- package/packages/api-types/dist/schemas/api.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/api.js.map +1 -0
- package/packages/api-types/dist/schemas/decision.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/decision.js.map +1 -0
- package/packages/api-types/dist/schemas/fleet.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/fleet.js.map +1 -0
- package/packages/api-types/dist/schemas/history.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/history.js.map +1 -0
- package/packages/api-types/dist/schemas/index.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/index.js.map +1 -0
- package/packages/api-types/dist/schemas/message.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/message.js.map +1 -0
- package/packages/api-types/dist/schemas/session.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/session.js.map +1 -0
- package/packages/api-types/dist/schemas/task.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/task.js.map +1 -0
- package/packages/api-types/package.json +1 -1
- package/packages/api-types/src/index.ts +22 -0
- package/packages/api-types/src/schemas/agent.test.ts +164 -0
- package/packages/api-types/src/schemas/agent.ts +110 -0
- package/packages/api-types/src/schemas/api.test.ts +372 -0
- package/packages/api-types/src/schemas/api.ts +194 -0
- package/packages/api-types/src/schemas/decision.test.ts +324 -0
- package/packages/api-types/src/schemas/decision.ts +136 -0
- package/packages/api-types/src/schemas/fleet.test.ts +212 -0
- package/packages/api-types/src/schemas/fleet.ts +83 -0
- package/packages/api-types/src/schemas/history.test.ts +242 -0
- package/packages/api-types/src/schemas/history.ts +84 -0
- package/packages/api-types/src/schemas/index.ts +148 -0
- package/packages/api-types/src/schemas/message.test.ts +192 -0
- package/packages/api-types/src/schemas/message.ts +98 -0
- package/packages/api-types/src/schemas/session.test.ts +104 -0
- package/packages/api-types/src/schemas/session.ts +40 -0
- package/packages/api-types/src/schemas/task.test.ts +192 -0
- package/packages/api-types/src/schemas/task.ts +78 -0
- package/packages/api-types/tsconfig.json +19 -0
- package/packages/api-types/vitest.config.ts +9 -0
- package/packages/benchmark/README.md +200 -0
- package/packages/benchmark/datasets/coding-tasks.yaml +127 -0
- package/packages/benchmark/datasets/coordination-tasks.yaml +122 -0
- package/packages/benchmark/dist/benchmark.d.ts +47 -0
- package/packages/benchmark/dist/benchmark.d.ts.map +1 -0
- package/packages/benchmark/dist/benchmark.js +224 -0
- package/packages/benchmark/dist/benchmark.js.map +1 -0
- package/packages/benchmark/dist/cli.d.ts +8 -0
- package/packages/benchmark/dist/cli.d.ts.map +1 -0
- package/packages/benchmark/dist/cli.js +185 -0
- package/packages/benchmark/dist/cli.js.map +1 -0
- package/packages/benchmark/dist/harbor.d.ts +53 -0
- package/packages/benchmark/dist/harbor.d.ts.map +1 -0
- package/packages/benchmark/dist/harbor.js +127 -0
- package/packages/benchmark/dist/harbor.js.map +1 -0
- package/packages/benchmark/dist/index.d.ts +48 -0
- package/packages/benchmark/dist/index.d.ts.map +1 -0
- package/packages/benchmark/dist/index.js +50 -0
- package/packages/benchmark/dist/index.js.map +1 -0
- package/packages/benchmark/dist/runners/base.d.ts +63 -0
- package/packages/benchmark/dist/runners/base.d.ts.map +1 -0
- package/packages/benchmark/dist/runners/base.js +155 -0
- package/packages/benchmark/dist/runners/base.js.map +1 -0
- package/packages/benchmark/dist/runners/index.d.ts +10 -0
- package/packages/benchmark/dist/runners/index.d.ts.map +1 -0
- package/packages/benchmark/dist/runners/index.js +10 -0
- package/packages/benchmark/dist/runners/index.js.map +1 -0
- package/packages/benchmark/dist/runners/single.d.ts +19 -0
- package/packages/benchmark/dist/runners/single.d.ts.map +1 -0
- package/packages/benchmark/dist/runners/single.js +111 -0
- package/packages/benchmark/dist/runners/single.js.map +1 -0
- package/packages/benchmark/dist/runners/subagent.d.ts +32 -0
- package/packages/benchmark/dist/runners/subagent.d.ts.map +1 -0
- package/packages/benchmark/dist/runners/subagent.js +212 -0
- package/packages/benchmark/dist/runners/subagent.js.map +1 -0
- package/packages/benchmark/dist/runners/swarm.d.ts +36 -0
- package/packages/benchmark/dist/runners/swarm.d.ts.map +1 -0
- package/packages/benchmark/dist/runners/swarm.js +273 -0
- package/packages/benchmark/dist/runners/swarm.js.map +1 -0
- package/packages/benchmark/dist/types.d.ts +178 -0
- package/packages/benchmark/dist/types.d.ts.map +1 -0
- package/packages/benchmark/dist/types.js +16 -0
- package/packages/benchmark/dist/types.js.map +1 -0
- package/packages/benchmark/package.json +80 -0
- package/packages/benchmark/src/benchmark.ts +298 -0
- package/packages/benchmark/src/cli.ts +240 -0
- package/packages/benchmark/src/harbor.ts +170 -0
- package/packages/benchmark/src/index.ts +73 -0
- package/packages/benchmark/src/runners/base.ts +204 -0
- package/packages/benchmark/src/runners/index.ts +10 -0
- package/packages/benchmark/src/runners/single.ts +121 -0
- package/packages/benchmark/src/runners/subagent.ts +240 -0
- package/packages/benchmark/src/runners/swarm.ts +326 -0
- package/packages/benchmark/src/types.ts +205 -0
- package/packages/benchmark/tsconfig.json +20 -0
- package/packages/bridge/dist/index.d.ts.map +1 -0
- package/packages/bridge/dist/index.js.map +1 -0
- package/packages/bridge/dist/multi-project-client.d.ts.map +1 -0
- package/packages/bridge/dist/multi-project-client.js.map +1 -0
- package/packages/bridge/dist/shadow-cli.d.ts.map +1 -0
- package/packages/bridge/dist/shadow-cli.js.map +1 -0
- package/packages/bridge/dist/spawner.d.ts.map +1 -0
- package/packages/bridge/dist/spawner.js +15 -2
- package/packages/bridge/dist/spawner.js.map +1 -0
- package/packages/bridge/dist/types.d.ts.map +1 -0
- package/packages/bridge/dist/types.js.map +1 -0
- package/packages/bridge/dist/utils.d.ts.map +1 -0
- package/packages/bridge/dist/utils.js.map +1 -0
- package/packages/bridge/package.json +8 -8
- package/packages/bridge/src/index.ts +25 -0
- package/packages/bridge/src/multi-project-client.test.ts +340 -0
- package/packages/bridge/src/multi-project-client.ts +469 -0
- package/packages/bridge/src/shadow-cli.ts +95 -0
- package/packages/bridge/src/spawner-mcp.test.ts +505 -0
- package/packages/bridge/src/spawner.ts +1724 -0
- package/packages/bridge/src/types.ts +145 -0
- package/packages/bridge/src/utils.test.ts +98 -0
- package/packages/bridge/src/utils.ts +67 -0
- package/packages/bridge/tsconfig.json +29 -0
- package/packages/bridge/vitest.config.ts +9 -0
- package/packages/cli-tester/dist/index.d.ts.map +1 -0
- package/packages/cli-tester/dist/index.js.map +1 -0
- package/packages/cli-tester/dist/utils/credential-check.d.ts.map +1 -0
- package/packages/cli-tester/dist/utils/credential-check.js.map +1 -0
- package/packages/cli-tester/dist/utils/socket-client.d.ts.map +1 -0
- package/packages/cli-tester/dist/utils/socket-client.js.map +1 -0
- package/packages/cli-tester/docker/Dockerfile +61 -0
- package/packages/cli-tester/docker/docker-compose.yml +71 -0
- package/packages/cli-tester/package.json +1 -1
- package/packages/cli-tester/src/index.ts +40 -0
- package/packages/cli-tester/src/utils/credential-check.ts +284 -0
- package/packages/cli-tester/src/utils/socket-client.ts +211 -0
- package/packages/cli-tester/tests/credential-check.test.ts +56 -0
- package/packages/cli-tester/tsconfig.json +11 -0
- package/packages/config/dist/agent-config.d.ts.map +1 -0
- package/packages/config/dist/agent-config.js.map +1 -0
- package/packages/config/dist/bridge-config.d.ts.map +1 -0
- package/packages/config/dist/bridge-config.js.map +1 -0
- package/packages/config/dist/bridge-utils.d.ts.map +1 -0
- package/packages/config/dist/bridge-utils.js.map +1 -0
- package/packages/config/dist/cli-auth-config.d.ts.map +1 -0
- package/packages/config/dist/cli-auth-config.js.map +1 -0
- package/packages/config/dist/cloud-config.d.ts.map +1 -0
- package/packages/config/dist/cloud-config.js.map +1 -0
- package/packages/config/dist/index.d.ts.map +1 -0
- package/packages/config/dist/index.js.map +1 -0
- package/packages/config/dist/project-namespace.d.ts.map +1 -0
- package/packages/config/dist/project-namespace.js.map +1 -0
- package/packages/config/dist/relay-config.d.ts.map +1 -0
- package/packages/config/dist/relay-config.js.map +1 -0
- package/packages/config/dist/relay-file-writer.d.ts.map +1 -0
- package/packages/config/dist/relay-file-writer.js.map +1 -0
- package/packages/config/dist/schemas.d.ts.map +1 -0
- package/packages/config/dist/schemas.js.map +1 -0
- package/packages/config/dist/shadow-config.d.ts.map +1 -0
- package/packages/config/dist/shadow-config.js.map +1 -0
- package/packages/config/dist/teams-config.d.ts.map +1 -0
- package/packages/config/dist/teams-config.js.map +1 -0
- package/packages/config/dist/trajectory-config.d.ts.map +1 -0
- package/packages/config/dist/trajectory-config.js.map +1 -0
- package/packages/config/package.json +2 -2
- package/packages/config/src/agent-config.test.ts +245 -0
- package/packages/config/src/agent-config.ts +160 -0
- package/packages/config/src/bridge-config.test.ts +132 -0
- package/packages/config/src/bridge-config.ts +189 -0
- package/packages/config/src/bridge-utils.ts +59 -0
- package/packages/config/src/cli-auth-config.ts +548 -0
- package/packages/config/src/cloud-config.ts +208 -0
- package/packages/config/src/index.ts +12 -0
- package/packages/config/src/project-namespace.ts +344 -0
- package/packages/config/src/relay-config.test.ts +51 -0
- package/packages/config/src/relay-config.ts +36 -0
- package/packages/config/src/relay-file-writer.test.ts +351 -0
- package/packages/config/src/relay-file-writer.ts +508 -0
- package/packages/config/src/schemas.test.ts +59 -0
- package/packages/config/src/schemas.ts +201 -0
- package/packages/config/src/shadow-config.ts +205 -0
- package/packages/config/src/teams-config.ts +135 -0
- package/packages/config/src/trajectory-config.ts +222 -0
- package/packages/config/tsconfig.json +21 -0
- package/packages/config/vitest.config.ts +9 -0
- package/packages/continuity/dist/formatter.d.ts.map +1 -0
- package/packages/continuity/dist/formatter.js.map +1 -0
- package/packages/continuity/dist/handoff-store.d.ts.map +1 -0
- package/packages/continuity/dist/handoff-store.js.map +1 -0
- package/packages/continuity/dist/index.d.ts.map +1 -0
- package/packages/continuity/dist/index.js.map +1 -0
- package/packages/continuity/dist/ledger-store.d.ts.map +1 -0
- package/packages/continuity/dist/ledger-store.js.map +1 -0
- package/packages/continuity/dist/manager.d.ts.map +1 -0
- package/packages/continuity/dist/manager.js.map +1 -0
- package/packages/continuity/dist/parser.d.ts.map +1 -0
- package/packages/continuity/dist/parser.js.map +1 -0
- package/packages/continuity/dist/types.d.ts.map +1 -0
- package/packages/continuity/dist/types.js.map +1 -0
- package/packages/continuity/package.json +1 -1
- package/packages/continuity/src/formatter.ts +371 -0
- package/packages/continuity/src/handoff-store.ts +523 -0
- package/packages/continuity/src/index.ts +9 -0
- package/packages/continuity/src/ledger-store.ts +594 -0
- package/packages/continuity/src/manager.test.ts +291 -0
- package/packages/continuity/src/manager.ts +774 -0
- package/packages/continuity/src/parser.test.ts +292 -0
- package/packages/continuity/src/parser.ts +680 -0
- package/packages/continuity/src/types.ts +211 -0
- package/packages/continuity/tsconfig.json +21 -0
- package/packages/continuity/vitest.config.ts +9 -0
- package/packages/daemon/dist/agent-manager.d.ts.map +1 -0
- package/packages/daemon/dist/agent-manager.js.map +1 -0
- package/packages/daemon/dist/agent-registry.d.ts.map +1 -0
- package/packages/daemon/dist/agent-registry.js.map +1 -0
- package/packages/daemon/dist/agent-signing.d.ts.map +1 -0
- package/packages/daemon/dist/agent-signing.js.map +1 -0
- package/packages/daemon/dist/api.d.ts.map +1 -0
- package/packages/daemon/dist/api.js.map +1 -0
- package/packages/daemon/dist/auth.d.ts.map +1 -0
- package/packages/daemon/dist/auth.js.map +1 -0
- package/packages/daemon/dist/channel-membership-store.d.ts.map +1 -0
- package/packages/daemon/dist/channel-membership-store.js.map +1 -0
- package/packages/daemon/dist/cli-auth.d.ts.map +1 -0
- package/packages/daemon/dist/cli-auth.js.map +1 -0
- package/packages/daemon/dist/cloud-sync.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-sync.js.map +1 -0
- package/packages/daemon/dist/connection.d.ts.map +1 -0
- package/packages/daemon/dist/connection.js.map +1 -0
- package/packages/daemon/dist/consensus-integration.d.ts.map +1 -0
- package/packages/daemon/dist/consensus-integration.js.map +1 -0
- package/packages/daemon/dist/consensus.d.ts.map +1 -0
- package/packages/daemon/dist/consensus.js.map +1 -0
- package/packages/daemon/dist/delivery-tracker.d.ts.map +1 -0
- package/packages/daemon/dist/delivery-tracker.js.map +1 -0
- package/packages/daemon/dist/enhanced-features.d.ts.map +1 -0
- package/packages/daemon/dist/enhanced-features.js.map +1 -0
- package/packages/daemon/dist/index.d.ts.map +1 -0
- package/packages/daemon/dist/index.js.map +1 -0
- package/packages/daemon/dist/migrations/index.d.ts.map +1 -0
- package/packages/daemon/dist/migrations/index.js.map +1 -0
- package/packages/daemon/dist/orchestrator.d.ts.map +1 -0
- package/packages/daemon/dist/orchestrator.js.map +1 -0
- package/packages/daemon/dist/rate-limiter.d.ts.map +1 -0
- package/packages/daemon/dist/rate-limiter.js.map +1 -0
- package/packages/daemon/dist/registry.d.ts.map +1 -0
- package/packages/daemon/dist/registry.js.map +1 -0
- package/packages/daemon/dist/relay-ledger.d.ts.map +1 -0
- package/packages/daemon/dist/relay-ledger.js.map +1 -0
- package/packages/daemon/dist/relay-watchdog.d.ts.map +1 -0
- package/packages/daemon/dist/relay-watchdog.js.map +1 -0
- package/packages/daemon/dist/repo-manager.d.ts.map +1 -0
- package/packages/daemon/dist/repo-manager.js.map +1 -0
- package/packages/daemon/dist/router.d.ts.map +1 -0
- package/packages/daemon/dist/router.js.map +1 -0
- package/packages/daemon/dist/server.d.ts +1 -0
- package/packages/daemon/dist/server.d.ts.map +1 -0
- package/packages/daemon/dist/server.js +46 -16
- package/packages/daemon/dist/server.js.map +1 -0
- package/packages/daemon/dist/spawn-manager.d.ts.map +1 -0
- package/packages/daemon/dist/spawn-manager.js.map +1 -0
- package/packages/daemon/dist/sync-queue.d.ts.map +1 -0
- package/packages/daemon/dist/sync-queue.js.map +1 -0
- package/packages/daemon/dist/types.d.ts.map +1 -0
- package/packages/daemon/dist/types.js.map +1 -0
- package/packages/daemon/dist/workspace-manager.d.ts.map +1 -0
- package/packages/daemon/dist/workspace-manager.js.map +1 -0
- package/packages/daemon/package.json +12 -12
- package/packages/daemon/src/agent-manager.ts +679 -0
- package/packages/daemon/src/agent-registry.ts +284 -0
- package/packages/daemon/src/agent-signing.ts +707 -0
- package/packages/daemon/src/api.ts +1012 -0
- package/packages/daemon/src/auth.ts +276 -0
- package/packages/daemon/src/channel-membership-store.ts +217 -0
- package/packages/daemon/src/cli-auth.ts +906 -0
- package/packages/daemon/src/cloud-sync.ts +902 -0
- package/packages/daemon/src/connection.ts +534 -0
- package/packages/daemon/src/consensus-integration.ts +510 -0
- package/packages/daemon/src/consensus.ts +848 -0
- package/packages/daemon/src/delivery-tracker.ts +145 -0
- package/packages/daemon/src/enhanced-features.ts +390 -0
- package/packages/daemon/src/index.ts +52 -0
- package/packages/daemon/src/migrations/0001_initial.sql +72 -0
- package/packages/daemon/src/migrations/index.test.ts +195 -0
- package/packages/daemon/src/migrations/index.ts +286 -0
- package/packages/daemon/src/orchestrator.test.ts +231 -0
- package/packages/daemon/src/orchestrator.ts +1376 -0
- package/packages/daemon/src/rate-limiter.ts +172 -0
- package/packages/daemon/src/registry.ts +8 -0
- package/packages/daemon/src/relay-ledger.test.ts +358 -0
- package/packages/daemon/src/relay-ledger.ts +713 -0
- package/packages/daemon/src/relay-watchdog.test.ts +881 -0
- package/packages/daemon/src/relay-watchdog.ts +785 -0
- package/packages/daemon/src/repo-manager.ts +468 -0
- package/packages/daemon/src/router.test.ts +149 -0
- package/packages/daemon/src/router.ts +1885 -0
- package/packages/daemon/src/server.ts +1871 -0
- package/packages/daemon/src/spawn-manager.ts +275 -0
- package/packages/daemon/src/sync-queue.ts +477 -0
- package/packages/daemon/src/types.ts +158 -0
- package/packages/daemon/src/workspace-manager.ts +371 -0
- package/packages/daemon/tsconfig.json +21 -0
- package/packages/hooks/dist/browser.d.ts.map +1 -0
- package/packages/hooks/dist/browser.js.map +1 -0
- package/packages/hooks/dist/emitter.d.ts.map +1 -0
- package/packages/hooks/dist/emitter.js.map +1 -0
- package/packages/hooks/dist/inbox-check/hook.d.ts.map +1 -0
- package/packages/hooks/dist/inbox-check/hook.js.map +1 -0
- package/packages/hooks/dist/inbox-check/index.d.ts.map +1 -0
- package/packages/hooks/dist/inbox-check/index.js.map +1 -0
- package/packages/hooks/dist/inbox-check/types.d.ts.map +1 -0
- package/packages/hooks/dist/inbox-check/types.js.map +1 -0
- package/packages/hooks/dist/inbox-check/utils.d.ts.map +1 -0
- package/packages/hooks/dist/inbox-check/utils.js.map +1 -0
- package/packages/hooks/dist/index.d.ts.map +1 -0
- package/packages/hooks/dist/index.js.map +1 -0
- package/packages/hooks/dist/registry.d.ts.map +1 -0
- package/packages/hooks/dist/registry.js.map +1 -0
- package/packages/hooks/dist/trajectory-hooks.d.ts.map +1 -0
- package/packages/hooks/dist/trajectory-hooks.js.map +1 -0
- package/packages/hooks/dist/types.d.ts.map +1 -0
- package/packages/hooks/dist/types.js.map +1 -0
- package/packages/hooks/package.json +4 -4
- package/packages/hooks/src/browser.ts +2 -0
- package/packages/hooks/src/emitter.ts +84 -0
- package/packages/hooks/src/inbox-check/hook.ts +114 -0
- package/packages/hooks/src/inbox-check/index.ts +8 -0
- package/packages/hooks/src/inbox-check/types.ts +39 -0
- package/packages/hooks/src/inbox-check/utils.test.ts +287 -0
- package/packages/hooks/src/inbox-check/utils.ts +125 -0
- package/packages/hooks/src/index.ts +11 -0
- package/packages/hooks/src/registry.ts +614 -0
- package/packages/hooks/src/shims.d.ts +3 -0
- package/packages/hooks/src/trajectory-hooks.ts +251 -0
- package/packages/hooks/src/types.ts +342 -0
- package/packages/hooks/tsconfig.json +21 -0
- package/packages/hooks/vitest.config.ts +9 -0
- package/packages/mcp/dist/bin.d.ts.map +1 -0
- package/packages/mcp/dist/bin.js.map +1 -0
- package/packages/mcp/dist/client.d.ts +9 -15
- package/packages/mcp/dist/client.d.ts.map +1 -0
- package/packages/mcp/dist/client.js +42 -74
- package/packages/mcp/dist/client.js.map +1 -0
- package/packages/mcp/dist/cloud.d.ts.map +1 -0
- package/packages/mcp/dist/cloud.js.map +1 -0
- package/packages/mcp/dist/errors.d.ts.map +1 -0
- package/packages/mcp/dist/errors.js.map +1 -0
- package/packages/mcp/dist/file-transport.d.ts.map +1 -0
- package/packages/mcp/dist/file-transport.js.map +1 -0
- package/packages/mcp/dist/hybrid-client.d.ts.map +1 -0
- package/packages/mcp/dist/hybrid-client.js.map +1 -0
- package/packages/mcp/dist/index.d.ts.map +1 -0
- package/packages/mcp/dist/index.js.map +1 -0
- package/packages/mcp/dist/install-cli.d.ts.map +1 -0
- package/packages/mcp/dist/install-cli.js.map +1 -0
- package/packages/mcp/dist/install.d.ts.map +1 -0
- package/packages/mcp/dist/install.js.map +1 -0
- package/packages/mcp/dist/prompts/index.d.ts.map +1 -0
- package/packages/mcp/dist/prompts/index.js.map +1 -0
- package/packages/mcp/dist/prompts/protocol.d.ts.map +1 -0
- package/packages/mcp/dist/prompts/protocol.js.map +1 -0
- package/packages/mcp/dist/resources/agents.d.ts.map +1 -0
- package/packages/mcp/dist/resources/agents.js.map +1 -0
- package/packages/mcp/dist/resources/inbox.d.ts.map +1 -0
- package/packages/mcp/dist/resources/inbox.js.map +1 -0
- package/packages/mcp/dist/resources/index.d.ts.map +1 -0
- package/packages/mcp/dist/resources/index.js.map +1 -0
- package/packages/mcp/dist/resources/project.d.ts.map +1 -0
- package/packages/mcp/dist/resources/project.js.map +1 -0
- package/packages/mcp/dist/server.d.ts.map +1 -0
- package/packages/mcp/dist/server.js.map +1 -0
- package/packages/mcp/dist/simple.d.ts +2 -5
- package/packages/mcp/dist/simple.d.ts.map +1 -0
- package/packages/mcp/dist/simple.js.map +1 -0
- package/packages/mcp/dist/tools/index.d.ts.map +1 -0
- package/packages/mcp/dist/tools/index.js.map +1 -0
- package/packages/mcp/dist/tools/relay-broadcast.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-broadcast.js.map +1 -0
- package/packages/mcp/dist/tools/relay-channel.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-channel.js.map +1 -0
- package/packages/mcp/dist/tools/relay-connected.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-connected.js.map +1 -0
- package/packages/mcp/dist/tools/relay-consensus.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-consensus.js.map +1 -0
- package/packages/mcp/dist/tools/relay-continuity.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-continuity.js.map +1 -0
- package/packages/mcp/dist/tools/relay-health.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-health.js.map +1 -0
- package/packages/mcp/dist/tools/relay-inbox.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-inbox.js.map +1 -0
- package/packages/mcp/dist/tools/relay-logs.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-logs.js.map +1 -0
- package/packages/mcp/dist/tools/relay-metrics.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-metrics.js.map +1 -0
- package/packages/mcp/dist/tools/relay-release.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-release.js.map +1 -0
- package/packages/mcp/dist/tools/relay-remove-agent.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-remove-agent.js.map +1 -0
- package/packages/mcp/dist/tools/relay-send.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-send.js +4 -2
- package/packages/mcp/dist/tools/relay-send.js.map +1 -0
- package/packages/mcp/dist/tools/relay-shadow.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-shadow.js.map +1 -0
- package/packages/mcp/dist/tools/relay-spawn.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-spawn.js.map +1 -0
- package/packages/mcp/dist/tools/relay-status.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-status.js.map +1 -0
- package/packages/mcp/dist/tools/relay-subscribe.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-subscribe.js.map +1 -0
- package/packages/mcp/dist/tools/relay-who.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-who.js.map +1 -0
- package/packages/mcp/package.json +3 -3
- package/packages/mcp/src/bin.ts +149 -0
- package/packages/mcp/src/client.ts +400 -0
- package/packages/mcp/src/cloud.ts +523 -0
- package/packages/mcp/src/errors.ts +54 -0
- package/packages/mcp/src/file-transport.ts +268 -0
- package/packages/mcp/src/hybrid-client.ts +209 -0
- package/packages/mcp/src/index.ts +122 -0
- package/packages/mcp/src/install-cli.ts +210 -0
- package/packages/mcp/src/install.ts +745 -0
- package/packages/mcp/src/prompts/index.ts +1 -0
- package/packages/mcp/src/prompts/protocol.ts +164 -0
- package/packages/mcp/src/resources/agents.ts +21 -0
- package/packages/mcp/src/resources/inbox.ts +21 -0
- package/packages/mcp/src/resources/index.ts +3 -0
- package/packages/mcp/src/resources/project.ts +29 -0
- package/packages/mcp/src/server.ts +431 -0
- package/packages/mcp/src/simple.ts +214 -0
- package/packages/mcp/src/tools/index.ts +133 -0
- package/packages/mcp/src/tools/relay-broadcast.ts +32 -0
- package/packages/mcp/src/tools/relay-channel.ts +93 -0
- package/packages/mcp/src/tools/relay-connected.ts +52 -0
- package/packages/mcp/src/tools/relay-consensus.ts +92 -0
- package/packages/mcp/src/tools/relay-continuity.ts +127 -0
- package/packages/mcp/src/tools/relay-health.ts +148 -0
- package/packages/mcp/src/tools/relay-inbox.ts +70 -0
- package/packages/mcp/src/tools/relay-logs.ts +106 -0
- package/packages/mcp/src/tools/relay-metrics.ts +140 -0
- package/packages/mcp/src/tools/relay-release.ts +54 -0
- package/packages/mcp/src/tools/relay-remove-agent.ts +58 -0
- package/packages/mcp/src/tools/relay-send.ts +84 -0
- package/packages/mcp/src/tools/relay-shadow.ts +67 -0
- package/packages/mcp/src/tools/relay-spawn.ts +87 -0
- package/packages/mcp/src/tools/relay-status.ts +57 -0
- package/packages/mcp/src/tools/relay-subscribe.ts +61 -0
- package/packages/mcp/src/tools/relay-who.ts +59 -0
- package/packages/mcp/tests/client.test.ts +476 -0
- package/packages/mcp/tests/discover.test.ts +195 -0
- package/packages/mcp/tests/install.test.ts +123 -0
- package/packages/mcp/tests/prompts.test.ts +12 -0
- package/packages/mcp/tests/resources.test.ts +53 -0
- package/packages/mcp/tests/tools.test.ts +1242 -0
- package/packages/mcp/tsconfig.json +22 -0
- package/packages/mcp/vitest.config.ts +9 -0
- package/packages/memory/dist/adapters/index.d.ts.map +1 -0
- package/packages/memory/dist/adapters/index.js.map +1 -0
- package/packages/memory/dist/adapters/inmemory.d.ts.map +1 -0
- package/packages/memory/dist/adapters/inmemory.js.map +1 -0
- package/packages/memory/dist/adapters/supermemory.d.ts.map +1 -0
- package/packages/memory/dist/adapters/supermemory.js.map +1 -0
- package/packages/memory/dist/context-compaction.d.ts.map +1 -0
- package/packages/memory/dist/context-compaction.js.map +1 -0
- package/packages/memory/dist/factory.d.ts.map +1 -0
- package/packages/memory/dist/factory.js.map +1 -0
- package/packages/memory/dist/index.d.ts.map +1 -0
- package/packages/memory/dist/index.js.map +1 -0
- package/packages/memory/dist/memory-hooks.d.ts.map +1 -0
- package/packages/memory/dist/memory-hooks.js.map +1 -0
- package/packages/memory/dist/service.d.ts.map +1 -0
- package/packages/memory/dist/service.js.map +1 -0
- package/packages/memory/dist/types.d.ts.map +1 -0
- package/packages/memory/dist/types.js.map +1 -0
- package/packages/memory/package.json +2 -2
- package/packages/memory/src/adapters/index.ts +8 -0
- package/packages/memory/src/adapters/inmemory.ts +265 -0
- package/packages/memory/src/adapters/supermemory.ts +449 -0
- package/packages/memory/src/context-compaction.test.ts +660 -0
- package/packages/memory/src/context-compaction.ts +612 -0
- package/packages/memory/src/factory.ts +170 -0
- package/packages/memory/src/index.ts +33 -0
- package/packages/memory/src/memory-hooks.ts +410 -0
- package/packages/memory/src/service.ts +194 -0
- package/packages/memory/src/types.ts +211 -0
- package/packages/memory/tsconfig.json +21 -0
- package/packages/memory/vitest.config.ts +9 -0
- package/packages/policy/dist/agent-policy.d.ts.map +1 -0
- package/packages/policy/dist/agent-policy.js.map +1 -0
- package/packages/policy/dist/cloud-policy-fetcher.d.ts.map +1 -0
- package/packages/policy/dist/cloud-policy-fetcher.js.map +1 -0
- package/packages/policy/dist/index.d.ts.map +1 -0
- package/packages/policy/dist/index.js.map +1 -0
- package/packages/policy/package.json +2 -2
- package/packages/policy/src/agent-policy.ts +866 -0
- package/packages/policy/src/cloud-policy-fetcher.ts +78 -0
- package/packages/policy/src/index.ts +21 -0
- package/packages/policy/tsconfig.json +21 -0
- package/packages/policy/vitest.config.ts +9 -0
- package/packages/protocol/dist/channels.d.ts.map +1 -0
- package/packages/protocol/dist/channels.js.map +1 -0
- package/packages/protocol/dist/framing.d.ts.map +1 -0
- package/packages/protocol/dist/framing.js.map +1 -0
- package/packages/protocol/dist/id-generator.d.ts.map +1 -0
- package/packages/protocol/dist/id-generator.js.map +1 -0
- package/packages/protocol/dist/index.d.ts.map +1 -0
- package/packages/protocol/dist/index.js.map +1 -0
- package/packages/protocol/dist/relay-pty-schemas.d.ts +70 -2
- package/packages/protocol/dist/relay-pty-schemas.d.ts.map +1 -0
- package/packages/protocol/dist/relay-pty-schemas.js.map +1 -0
- package/packages/protocol/dist/types.d.ts +8 -0
- package/packages/protocol/dist/types.d.ts.map +1 -0
- package/packages/protocol/dist/types.js.map +1 -0
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/src/channels.test.ts +330 -0
- package/packages/protocol/src/channels.ts +270 -0
- package/packages/protocol/src/framing.test.ts +164 -0
- package/packages/protocol/src/framing.ts +242 -0
- package/packages/protocol/src/id-generator.ts +69 -0
- package/packages/protocol/src/index.ts +4 -0
- package/packages/protocol/src/relay-pty-schemas.ts +400 -0
- package/packages/protocol/src/types.test.ts +271 -0
- package/packages/protocol/src/types.ts +846 -0
- package/packages/protocol/tsconfig.json +21 -0
- package/packages/protocol/vitest.config.ts +9 -0
- package/packages/resiliency/dist/cgroup-manager.d.ts.map +1 -0
- package/packages/resiliency/dist/cgroup-manager.js.map +1 -0
- package/packages/resiliency/dist/context-persistence.d.ts.map +1 -0
- package/packages/resiliency/dist/context-persistence.js.map +1 -0
- package/packages/resiliency/dist/crash-insights.d.ts.map +1 -0
- package/packages/resiliency/dist/crash-insights.js.map +1 -0
- package/packages/resiliency/dist/gossip-health.d.ts.map +1 -0
- package/packages/resiliency/dist/gossip-health.js.map +1 -0
- package/packages/resiliency/dist/health-monitor.d.ts.map +1 -0
- package/packages/resiliency/dist/health-monitor.js.map +1 -0
- package/packages/resiliency/dist/index.d.ts.map +1 -0
- package/packages/resiliency/dist/index.js.map +1 -0
- package/packages/resiliency/dist/leader-watchdog.d.ts.map +1 -0
- package/packages/resiliency/dist/leader-watchdog.js.map +1 -0
- package/packages/resiliency/dist/logger.d.ts.map +1 -0
- package/packages/resiliency/dist/logger.js.map +1 -0
- package/packages/resiliency/dist/memory-monitor.d.ts.map +1 -0
- package/packages/resiliency/dist/memory-monitor.js.map +1 -0
- package/packages/resiliency/dist/metrics.d.ts.map +1 -0
- package/packages/resiliency/dist/metrics.js.map +1 -0
- package/packages/resiliency/dist/provider-context.d.ts.map +1 -0
- package/packages/resiliency/dist/provider-context.js.map +1 -0
- package/packages/resiliency/dist/stateless-lead.d.ts.map +1 -0
- package/packages/resiliency/dist/stateless-lead.js.map +1 -0
- package/packages/resiliency/dist/supervisor.d.ts.map +1 -0
- package/packages/resiliency/dist/supervisor.js.map +1 -0
- package/packages/resiliency/package.json +1 -1
- package/packages/resiliency/src/cgroup-manager.ts +468 -0
- package/packages/resiliency/src/context-persistence.ts +538 -0
- package/packages/resiliency/src/crash-insights.test.ts +620 -0
- package/packages/resiliency/src/crash-insights.ts +660 -0
- package/packages/resiliency/src/gossip-health.ts +333 -0
- package/packages/resiliency/src/health-monitor.ts +371 -0
- package/packages/resiliency/src/index.ts +157 -0
- package/packages/resiliency/src/leader-watchdog.ts +260 -0
- package/packages/resiliency/src/logger.ts +320 -0
- package/packages/resiliency/src/memory-monitor.test.ts +637 -0
- package/packages/resiliency/src/memory-monitor.ts +740 -0
- package/packages/resiliency/src/metrics.ts +311 -0
- package/packages/resiliency/src/provider-context.ts +452 -0
- package/packages/resiliency/src/stateless-lead.ts +408 -0
- package/packages/resiliency/src/supervisor.ts +578 -0
- package/packages/resiliency/tsconfig.json +21 -0
- package/packages/resiliency/vitest.config.ts +9 -0
- package/packages/sdk/dist/client.d.ts.map +1 -0
- package/packages/sdk/dist/client.js.map +1 -0
- package/packages/sdk/dist/index.d.ts.map +1 -0
- package/packages/sdk/dist/index.js.map +1 -0
- package/packages/sdk/dist/logs.d.ts.map +1 -0
- package/packages/sdk/dist/logs.js.map +1 -0
- package/packages/sdk/dist/protocol/index.d.ts.map +1 -0
- package/packages/sdk/dist/protocol/index.js.map +1 -0
- package/packages/sdk/dist/standalone.d.ts.map +1 -0
- package/packages/sdk/dist/standalone.js.map +1 -0
- package/packages/sdk/examples/SWARM_CAPABILITIES.md +498 -0
- package/packages/sdk/examples/SWARM_PATTERNS.md +541 -0
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/client.test.ts +568 -0
- package/packages/sdk/src/client.ts +1418 -0
- package/packages/sdk/src/index.ts +103 -0
- package/packages/sdk/src/logs.test.ts +98 -0
- package/packages/sdk/src/logs.ts +126 -0
- package/packages/sdk/src/protocol/framing.test.ts +164 -0
- package/packages/sdk/src/protocol/index.ts +8 -0
- package/packages/sdk/src/standalone.ts +176 -0
- package/packages/sdk/tsconfig.json +22 -0
- package/packages/sdk/vitest.config.ts +9 -0
- package/packages/spawner/.trajectories/index.json +5 -0
- package/packages/spawner/dist/index.d.ts.map +1 -0
- package/packages/spawner/dist/index.js.map +1 -0
- package/packages/spawner/dist/types.d.ts.map +1 -0
- package/packages/spawner/dist/types.js.map +1 -0
- package/packages/spawner/package.json +1 -1
- package/packages/spawner/src/index.ts +8 -0
- package/packages/spawner/src/types.test.ts +385 -0
- package/packages/spawner/src/types.ts +228 -0
- package/packages/spawner/tsconfig.json +19 -0
- package/packages/spawner/vitest.config.ts +9 -0
- package/packages/state/dist/agent-state.d.ts.map +1 -0
- package/packages/state/dist/agent-state.js.map +1 -0
- package/packages/state/dist/index.d.ts.map +1 -0
- package/packages/state/dist/index.js.map +1 -0
- package/packages/state/package.json +1 -1
- package/packages/state/src/agent-state.test.ts +335 -0
- package/packages/state/src/agent-state.ts +153 -0
- package/packages/state/src/index.ts +12 -0
- package/packages/state/tsconfig.json +21 -0
- package/packages/state/vitest.config.ts +9 -0
- package/packages/storage/dist/adapter.d.ts +28 -1
- package/packages/storage/dist/adapter.d.ts.map +1 -0
- package/packages/storage/dist/adapter.js +104 -10
- package/packages/storage/dist/adapter.js.map +1 -0
- package/packages/storage/dist/batched-sqlite-adapter.d.ts.map +1 -0
- package/packages/storage/dist/batched-sqlite-adapter.js.map +1 -0
- package/packages/storage/dist/dead-letter-queue.d.ts.map +1 -0
- package/packages/storage/dist/dead-letter-queue.js.map +1 -0
- package/packages/storage/dist/dlq-adapter.d.ts.map +1 -0
- package/packages/storage/dist/dlq-adapter.js.map +1 -0
- package/packages/storage/dist/index.d.ts +1 -0
- package/packages/storage/dist/index.d.ts.map +1 -0
- package/packages/storage/dist/index.js +1 -0
- package/packages/storage/dist/index.js.map +1 -0
- package/packages/storage/dist/jsonl-adapter.d.ts +77 -0
- package/packages/storage/dist/jsonl-adapter.d.ts.map +1 -0
- package/packages/storage/dist/jsonl-adapter.js +505 -0
- package/packages/storage/dist/jsonl-adapter.js.map +1 -0
- package/packages/storage/dist/sqlite-adapter.d.ts +6 -1
- package/packages/storage/dist/sqlite-adapter.d.ts.map +1 -0
- package/packages/storage/dist/sqlite-adapter.js +47 -0
- package/packages/storage/dist/sqlite-adapter.js.map +1 -0
- package/packages/storage/package.json +2 -2
- package/packages/storage/src/adapter.ts +438 -0
- package/packages/storage/src/batched-sqlite-adapter.test.ts +240 -0
- package/packages/storage/src/batched-sqlite-adapter.ts +239 -0
- package/packages/storage/src/dead-letter-queue.ts +643 -0
- package/packages/storage/src/dlq-adapter.test.ts +492 -0
- package/packages/storage/src/dlq-adapter.ts +954 -0
- package/packages/storage/src/index.ts +6 -0
- package/packages/storage/src/jsonl-adapter.test.ts +200 -0
- package/packages/storage/src/jsonl-adapter.ts +618 -0
- package/packages/storage/src/memory-adapter.test.ts +36 -0
- package/packages/storage/src/sqlite-adapter.test.ts +562 -0
- package/packages/storage/src/sqlite-adapter.ts +1058 -0
- package/packages/storage/tsconfig.json +21 -0
- package/packages/storage/vitest.config.ts +9 -0
- package/packages/telemetry/dist/client.d.ts.map +1 -0
- package/packages/telemetry/dist/client.js.map +1 -0
- package/packages/telemetry/dist/config.d.ts.map +1 -0
- package/packages/telemetry/dist/config.js.map +1 -0
- package/packages/telemetry/dist/events.d.ts.map +1 -0
- package/packages/telemetry/dist/events.js.map +1 -0
- package/packages/telemetry/dist/index.d.ts.map +1 -0
- package/packages/telemetry/dist/index.js.map +1 -0
- package/packages/telemetry/dist/machine-id.d.ts.map +1 -0
- package/packages/telemetry/dist/machine-id.js.map +1 -0
- package/packages/telemetry/dist/posthog-config.d.ts.map +1 -0
- package/packages/telemetry/dist/posthog-config.js.map +1 -0
- package/packages/telemetry/package.json +1 -1
- package/packages/telemetry/src/client.ts +158 -0
- package/packages/telemetry/src/config.ts +110 -0
- package/packages/telemetry/src/events.ts +137 -0
- package/packages/telemetry/src/index.ts +46 -0
- package/packages/telemetry/src/machine-id.ts +63 -0
- package/packages/telemetry/src/posthog-config.ts +39 -0
- package/packages/telemetry/tsconfig.json +21 -0
- package/packages/trajectory/dist/index.d.ts.map +1 -0
- package/packages/trajectory/dist/index.js.map +1 -0
- package/packages/trajectory/dist/integration.d.ts.map +1 -0
- package/packages/trajectory/dist/integration.js.map +1 -0
- package/packages/trajectory/package.json +2 -2
- package/packages/trajectory/src/index.ts +1 -0
- package/packages/trajectory/src/integration.ts +1268 -0
- package/packages/trajectory/tsconfig.json +21 -0
- package/packages/trajectory/vitest.config.ts +9 -0
- package/packages/user-directory/dist/index.d.ts.map +1 -0
- package/packages/user-directory/dist/index.js.map +1 -0
- package/packages/user-directory/dist/user-directory.d.ts.map +1 -0
- package/packages/user-directory/dist/user-directory.js.map +1 -0
- package/packages/user-directory/package.json +2 -2
- package/packages/user-directory/src/index.ts +12 -0
- package/packages/user-directory/src/user-directory.ts +393 -0
- package/packages/user-directory/tsconfig.json +21 -0
- package/packages/user-directory/vitest.config.ts +9 -0
- package/packages/utils/dist/cjs/client-helpers.js +127 -0
- package/packages/utils/dist/cjs/command-resolver.js +89 -0
- package/packages/utils/dist/cjs/error-tracking.js +106 -0
- package/packages/utils/dist/cjs/git-remote.js +120 -0
- package/packages/utils/dist/cjs/index.js +40 -0
- package/packages/utils/dist/cjs/logger.js +105 -0
- package/packages/utils/dist/cjs/model-mapping.js +54 -0
- package/packages/utils/dist/cjs/name-generator.js +179 -0
- package/packages/utils/dist/cjs/package.json +3 -0
- package/packages/utils/dist/cjs/precompiled-patterns.js +271 -0
- package/packages/utils/dist/cjs/relay-pty-path.js +143 -0
- package/packages/utils/dist/cjs/update-checker.js +185 -0
- package/packages/utils/dist/client-helpers.d.ts +73 -0
- package/packages/utils/dist/client-helpers.d.ts.map +1 -0
- package/packages/utils/dist/client-helpers.js +130 -0
- package/packages/utils/dist/client-helpers.js.map +1 -0
- package/packages/utils/dist/command-resolver.d.ts.map +1 -0
- package/packages/utils/dist/command-resolver.js.map +1 -0
- package/packages/utils/dist/error-tracking.d.ts.map +1 -0
- package/packages/utils/dist/error-tracking.js.map +1 -0
- package/packages/utils/dist/git-remote.d.ts.map +1 -0
- package/packages/utils/dist/git-remote.js.map +1 -0
- package/packages/utils/dist/index.d.ts +1 -0
- package/packages/utils/dist/index.d.ts.map +1 -0
- package/packages/utils/dist/index.js +1 -0
- package/packages/utils/dist/index.js.map +1 -0
- package/packages/utils/dist/logger.d.ts.map +1 -0
- package/packages/utils/dist/logger.js.map +1 -0
- package/packages/utils/dist/model-mapping.d.ts.map +1 -0
- package/packages/utils/dist/model-mapping.js.map +1 -0
- package/packages/utils/dist/name-generator.d.ts.map +1 -0
- package/packages/utils/dist/name-generator.js.map +1 -0
- package/packages/utils/dist/precompiled-patterns.d.ts.map +1 -0
- package/packages/utils/dist/precompiled-patterns.js.map +1 -0
- package/packages/utils/dist/relay-pty-path.d.ts +11 -5
- package/packages/utils/dist/relay-pty-path.d.ts.map +1 -0
- package/packages/utils/dist/relay-pty-path.js +60 -5
- package/packages/utils/dist/relay-pty-path.js.map +1 -0
- package/packages/utils/dist/update-checker.d.ts.map +1 -0
- package/packages/utils/dist/update-checker.js.map +1 -0
- package/packages/utils/package.json +37 -14
- package/packages/utils/scripts/build-cjs.mjs +24 -0
- package/packages/utils/src/client-helpers.ts +221 -0
- package/packages/utils/src/command-resolver.ts +82 -0
- package/packages/utils/src/error-tracking.ts +189 -0
- package/packages/utils/src/git-remote.ts +143 -0
- package/packages/utils/src/index.ts +10 -0
- package/packages/utils/src/logger.ts +107 -0
- package/packages/utils/src/model-mapping.test.ts +122 -0
- package/packages/utils/src/model-mapping.ts +58 -0
- package/packages/utils/src/name-generator.test.ts +259 -0
- package/packages/utils/src/name-generator.ts +56 -0
- package/packages/utils/src/precompiled-patterns.test.ts +452 -0
- package/packages/utils/src/precompiled-patterns.ts +395 -0
- package/packages/utils/src/relay-pty-path.ts +196 -0
- package/packages/utils/src/update-checker.test.ts +260 -0
- package/packages/utils/src/update-checker.ts +211 -0
- package/packages/utils/tsconfig.json +21 -0
- package/packages/utils/vitest.config.ts +9 -0
- package/packages/wrapper/dist/__fixtures__/claude-outputs.d.ts.map +1 -0
- package/packages/wrapper/dist/__fixtures__/claude-outputs.js.map +1 -0
- package/packages/wrapper/dist/__fixtures__/codex-outputs.d.ts.map +1 -0
- package/packages/wrapper/dist/__fixtures__/codex-outputs.js.map +1 -0
- package/packages/wrapper/dist/__fixtures__/gemini-outputs.d.ts.map +1 -0
- package/packages/wrapper/dist/__fixtures__/gemini-outputs.js.map +1 -0
- package/packages/wrapper/dist/__fixtures__/index.d.ts.map +1 -0
- package/packages/wrapper/dist/__fixtures__/index.js.map +1 -0
- package/packages/wrapper/dist/auth-detection.d.ts.map +1 -0
- package/packages/wrapper/dist/auth-detection.js.map +1 -0
- package/packages/wrapper/dist/base-wrapper.d.ts.map +1 -0
- package/packages/wrapper/dist/base-wrapper.js.map +1 -0
- package/packages/wrapper/dist/client.d.ts.map +1 -0
- package/packages/wrapper/dist/client.js.map +1 -0
- package/packages/wrapper/dist/id-generator.d.ts.map +1 -0
- package/packages/wrapper/dist/id-generator.js.map +1 -0
- package/packages/wrapper/dist/idle-detector.d.ts.map +1 -0
- package/packages/wrapper/dist/idle-detector.js.map +1 -0
- package/packages/wrapper/dist/inbox.d.ts.map +1 -0
- package/packages/wrapper/dist/inbox.js.map +1 -0
- package/packages/wrapper/dist/index.d.ts.map +1 -0
- package/packages/wrapper/dist/index.js.map +1 -0
- package/packages/wrapper/dist/parser.d.ts.map +1 -0
- package/packages/wrapper/dist/parser.js.map +1 -0
- package/packages/wrapper/dist/prompt-composer.d.ts.map +1 -0
- package/packages/wrapper/dist/prompt-composer.js.map +1 -0
- package/packages/wrapper/dist/relay-pty-orchestrator.d.ts +10 -0
- package/packages/wrapper/dist/relay-pty-orchestrator.d.ts.map +1 -0
- package/packages/wrapper/dist/relay-pty-orchestrator.js +69 -0
- package/packages/wrapper/dist/relay-pty-orchestrator.js.map +1 -0
- package/packages/wrapper/dist/shared.d.ts.map +1 -0
- package/packages/wrapper/dist/shared.js.map +1 -0
- package/packages/wrapper/dist/stuck-detector.d.ts.map +1 -0
- package/packages/wrapper/dist/stuck-detector.js.map +1 -0
- package/packages/wrapper/dist/tmux-resolver.d.ts.map +1 -0
- package/packages/wrapper/dist/tmux-resolver.js.map +1 -0
- package/packages/wrapper/dist/tmux-wrapper.d.ts.map +1 -0
- package/packages/wrapper/dist/tmux-wrapper.js.map +1 -0
- package/packages/wrapper/dist/trajectory-integration.d.ts.map +1 -0
- package/packages/wrapper/dist/trajectory-integration.js.map +1 -0
- package/packages/wrapper/dist/wrapper-types.d.ts.map +1 -0
- package/packages/wrapper/dist/wrapper-types.js.map +1 -0
- package/packages/wrapper/package.json +6 -9
- package/packages/wrapper/src/__fixtures__/claude-outputs.ts +471 -0
- package/packages/wrapper/src/__fixtures__/codex-outputs.ts +99 -0
- package/packages/wrapper/src/__fixtures__/gemini-outputs.ts +151 -0
- package/packages/wrapper/src/__fixtures__/index.ts +47 -0
- package/packages/wrapper/src/auth-detection.ts +244 -0
- package/packages/wrapper/src/base-wrapper.test.ts +589 -0
- package/packages/wrapper/src/base-wrapper.ts +810 -0
- package/packages/wrapper/src/client.test.ts +262 -0
- package/packages/wrapper/src/client.ts +984 -0
- package/packages/wrapper/src/id-generator.test.ts +71 -0
- package/packages/wrapper/src/id-generator.ts +69 -0
- package/packages/wrapper/src/idle-detector.test.ts +418 -0
- package/packages/wrapper/src/idle-detector.ts +384 -0
- package/packages/wrapper/src/inbox.test.ts +233 -0
- package/packages/wrapper/src/inbox.ts +89 -0
- package/packages/wrapper/src/index.ts +170 -0
- package/packages/wrapper/src/parser.regression.test.ts +251 -0
- package/packages/wrapper/src/parser.test.ts +1359 -0
- package/packages/wrapper/src/parser.ts +1477 -0
- package/packages/wrapper/src/prompt-composer.test.ts +219 -0
- package/packages/wrapper/src/prompt-composer.ts +231 -0
- package/packages/wrapper/src/relay-pty-orchestrator.test.ts +1204 -0
- package/packages/wrapper/src/relay-pty-orchestrator.ts +2626 -0
- package/packages/wrapper/src/shared.test.ts +322 -0
- package/packages/wrapper/src/shared.ts +495 -0
- package/packages/wrapper/src/stuck-detector.test.ts +303 -0
- package/packages/wrapper/src/stuck-detector.ts +511 -0
- package/packages/wrapper/src/tmux-resolver.test.ts +104 -0
- package/packages/wrapper/src/tmux-resolver.ts +207 -0
- package/packages/wrapper/src/tmux-wrapper.test.ts +316 -0
- package/packages/wrapper/src/tmux-wrapper.ts +2095 -0
- package/packages/wrapper/src/trajectory-detection.test.ts +151 -0
- package/packages/wrapper/src/trajectory-integration.ts +1261 -0
- package/packages/wrapper/src/wrapper-types.ts +45 -0
- package/packages/wrapper/tsconfig.json +19 -0
- package/packages/wrapper/vitest.config.ts +9 -0
- package/scripts/build-cjs.mjs +23 -0
- package/scripts/postinstall.js +132 -0
- package/.cursor/mcp.json +0 -11
- package/.gitattributes +0 -3
- package/.gitleaks.toml +0 -26
- package/.mcp.json +0 -11
- package/.nvmrc +0 -1
- package/ARCHITECTURE.md +0 -1245
- package/CHANGELOG.md +0 -231
- package/TESTING.md +0 -278
- package/TRAIL_GIT_AUTH_FIX.md +0 -113
- package/scripts/demos/README.md +0 -79
- package/scripts/demos/server-capacity.sh +0 -69
- package/scripts/demos/sprint-planning.sh +0 -73
- package/scripts/hooks/install.sh +0 -16
- package/scripts/hooks/pre-commit +0 -60
- package/scripts/post-publish-verify/README.md +0 -80
- package/scripts/post-publish-verify/run-verify.sh +0 -127
- package/scripts/post-publish-verify/verify-install.sh +0 -249
- package/scripts/stress-test-orchestrator-integration.mts +0 -1366
- package/scripts/stress-test-orchestrator.mjs +0 -584
- package/scripts/stress-test-relay-pty.sh +0 -452
- package/scripts/test-interactive-terminal.sh +0 -248
- package/specs/PRIMITIVES_ROADMAP.md +0 -2154
- package/tests/benchmarks/protocol.bench.ts +0 -310
- package/turbo.json +0 -37
|
@@ -0,0 +1,2626 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RelayPtyOrchestrator - Orchestrates the relay-pty Rust binary
|
|
3
|
+
*
|
|
4
|
+
* This wrapper spawns the relay-pty binary and communicates via Unix socket.
|
|
5
|
+
* It provides the same interface as PtyWrapper but with improved latency
|
|
6
|
+
* (~550ms vs ~1700ms) by using direct PTY writes instead of tmux send-keys.
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* 1. Spawn relay-pty --name {agentName} -- {command} as child process
|
|
10
|
+
* 2. Connect to socket for injection:
|
|
11
|
+
* - With WORKSPACE_ID: /tmp/relay/{workspaceId}/sockets/{agentName}.sock
|
|
12
|
+
* - Without: /tmp/relay-pty-{agentName}.sock (legacy)
|
|
13
|
+
* 3. Parse stdout for relay commands (relay-pty echoes all output)
|
|
14
|
+
* 4. Translate SEND envelopes → inject messages via socket
|
|
15
|
+
*
|
|
16
|
+
* @see docs/RUST_WRAPPER_DESIGN.md for protocol details
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { spawn, ChildProcess } from 'node:child_process';
|
|
20
|
+
import { createConnection, Socket } from 'node:net';
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { join, dirname } from 'node:path';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
import { existsSync, unlinkSync, mkdirSync, symlinkSync, lstatSync, rmSync, watch, readdirSync, readlinkSync, writeFileSync, appendFileSync } from 'node:fs';
|
|
25
|
+
import type { FSWatcher } from 'node:fs';
|
|
26
|
+
import { getProjectPaths } from '@agent-relay/config/project-namespace';
|
|
27
|
+
import { getAgentOutboxTemplate } from '@agent-relay/config/relay-file-writer';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
|
|
30
|
+
// Get the directory where this module is located
|
|
31
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
32
|
+
const __dirname = dirname(__filename);
|
|
33
|
+
import { BaseWrapper, type BaseWrapperConfig } from './base-wrapper.js';
|
|
34
|
+
import { parseSummaryWithDetails, parseSessionEndFromOutput } from './parser.js';
|
|
35
|
+
import type { SendPayload, SendMeta, Envelope } from '@agent-relay/protocol/types';
|
|
36
|
+
import type { ChannelMessagePayload } from '@agent-relay/protocol/channels';
|
|
37
|
+
import { findRelayPtyBinary as findRelayPtyBinaryUtil } from '@agent-relay/utils/relay-pty-path';
|
|
38
|
+
import {
|
|
39
|
+
type QueuedMessage,
|
|
40
|
+
stripAnsi,
|
|
41
|
+
sleep,
|
|
42
|
+
buildInjectionString,
|
|
43
|
+
AdaptiveThrottle,
|
|
44
|
+
} from './shared.js';
|
|
45
|
+
import {
|
|
46
|
+
getMemoryMonitor,
|
|
47
|
+
type AgentMemoryMonitor,
|
|
48
|
+
type MemoryAlert,
|
|
49
|
+
formatBytes,
|
|
50
|
+
getCgroupManager,
|
|
51
|
+
type CgroupManager,
|
|
52
|
+
} from '@agent-relay/resiliency';
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Types for relay-pty socket protocol
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
const MAX_SOCKET_PATH_LENGTH = 107;
|
|
59
|
+
|
|
60
|
+
function hashWorkspaceId(workspaceId: string): string {
|
|
61
|
+
return createHash('sha256').update(workspaceId).digest('hex').slice(0, 12);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Request types sent to relay-pty socket
|
|
66
|
+
*/
|
|
67
|
+
interface InjectRequest {
|
|
68
|
+
type: 'inject';
|
|
69
|
+
id: string;
|
|
70
|
+
from: string;
|
|
71
|
+
body: string;
|
|
72
|
+
priority: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface StatusRequest {
|
|
76
|
+
type: 'status';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface ShutdownRequest {
|
|
80
|
+
type: 'shutdown';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Send just Enter key (for stuck input recovery)
|
|
85
|
+
* Used when message was written to PTY but Enter wasn't processed
|
|
86
|
+
*/
|
|
87
|
+
interface SendEnterRequest {
|
|
88
|
+
type: 'send_enter';
|
|
89
|
+
/** Message ID this is for (for tracking) */
|
|
90
|
+
id: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type RelayPtyRequest = InjectRequest | StatusRequest | ShutdownRequest | SendEnterRequest;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Response types received from relay-pty socket
|
|
97
|
+
*/
|
|
98
|
+
interface InjectResultResponse {
|
|
99
|
+
type: 'inject_result';
|
|
100
|
+
id: string;
|
|
101
|
+
status: 'queued' | 'injecting' | 'delivered' | 'failed';
|
|
102
|
+
timestamp: number;
|
|
103
|
+
error?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface StatusResponse {
|
|
107
|
+
type: 'status';
|
|
108
|
+
agent_idle: boolean;
|
|
109
|
+
queue_length: number;
|
|
110
|
+
cursor_position?: [number, number];
|
|
111
|
+
last_output_ms: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface BackpressureResponse {
|
|
115
|
+
type: 'backpressure';
|
|
116
|
+
queue_length: number;
|
|
117
|
+
accept: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface ErrorResponse {
|
|
121
|
+
type: 'error';
|
|
122
|
+
message: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface ShutdownAckResponse {
|
|
126
|
+
type: 'shutdown_ack';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Response for SendEnter request (stuck input recovery)
|
|
131
|
+
*/
|
|
132
|
+
interface SendEnterResultResponse {
|
|
133
|
+
type: 'send_enter_result';
|
|
134
|
+
/** Message ID this is for */
|
|
135
|
+
id: string;
|
|
136
|
+
/** Whether Enter was sent successfully */
|
|
137
|
+
success: boolean;
|
|
138
|
+
/** Unix timestamp in milliseconds */
|
|
139
|
+
timestamp: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
type RelayPtyResponse =
|
|
143
|
+
| InjectResultResponse
|
|
144
|
+
| StatusResponse
|
|
145
|
+
| BackpressureResponse
|
|
146
|
+
| ErrorResponse
|
|
147
|
+
| ShutdownAckResponse
|
|
148
|
+
| SendEnterResultResponse;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Configuration for RelayPtyOrchestrator
|
|
152
|
+
*/
|
|
153
|
+
export interface RelayPtyOrchestratorConfig extends BaseWrapperConfig {
|
|
154
|
+
/** Path to relay-pty binary (default: searches PATH and ./relay-pty/target/release) */
|
|
155
|
+
relayPtyPath?: string;
|
|
156
|
+
/** Socket connect timeout in ms (default: 5000) */
|
|
157
|
+
socketConnectTimeoutMs?: number;
|
|
158
|
+
/** Socket reconnect attempts (default: 3) */
|
|
159
|
+
socketReconnectAttempts?: number;
|
|
160
|
+
/** Callback when agent exits */
|
|
161
|
+
onExit?: (code: number) => void;
|
|
162
|
+
/** Callback when injection fails after retries */
|
|
163
|
+
onInjectionFailed?: (messageId: string, error: string) => void;
|
|
164
|
+
/** Enable debug logging (default: false) */
|
|
165
|
+
debug?: boolean;
|
|
166
|
+
/** Force headless mode (use pipes instead of inheriting TTY) */
|
|
167
|
+
headless?: boolean;
|
|
168
|
+
/** CPU limit percentage per agent (1-100 per core, e.g., 50 = 50% of one core). Requires cgroups v2. */
|
|
169
|
+
cpuLimitPercent?: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Events emitted by RelayPtyOrchestrator
|
|
174
|
+
*/
|
|
175
|
+
export interface RelayPtyOrchestratorEvents {
|
|
176
|
+
output: (data: string) => void;
|
|
177
|
+
exit: (code: number) => void;
|
|
178
|
+
error: (error: Error) => void;
|
|
179
|
+
'injection-failed': (event: { messageId: string; from: string; error: string }) => void;
|
|
180
|
+
'backpressure': (event: { queueLength: number; accept: boolean }) => void;
|
|
181
|
+
'summary': (event: { agentName: string; summary: unknown }) => void;
|
|
182
|
+
'session-end': (event: { agentName: string; marker: unknown }) => void;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Orchestrator for relay-pty Rust binary
|
|
187
|
+
*
|
|
188
|
+
* Extends BaseWrapper to provide the same interface as PtyWrapper
|
|
189
|
+
* but uses the relay-pty binary for improved injection reliability.
|
|
190
|
+
*/
|
|
191
|
+
export class RelayPtyOrchestrator extends BaseWrapper {
|
|
192
|
+
protected override config: RelayPtyOrchestratorConfig;
|
|
193
|
+
|
|
194
|
+
// Process management
|
|
195
|
+
private relayPtyProcess?: ChildProcess;
|
|
196
|
+
private socketPath: string;
|
|
197
|
+
private _logPath: string;
|
|
198
|
+
private _outboxPath: string;
|
|
199
|
+
private _legacyOutboxPath: string; // Legacy /tmp/relay-outbox path for backwards compat
|
|
200
|
+
private _canonicalOutboxPath: string; // Canonical ~/.agent-relay/outbox path (agents write here)
|
|
201
|
+
private _workspaceId?: string; // For symlink setup
|
|
202
|
+
private socket?: Socket;
|
|
203
|
+
private socketConnected = false;
|
|
204
|
+
|
|
205
|
+
// Output buffering
|
|
206
|
+
private outputBuffer = '';
|
|
207
|
+
private rawBuffer = '';
|
|
208
|
+
private lastParsedLength = 0;
|
|
209
|
+
|
|
210
|
+
// Interactive mode (show output to terminal)
|
|
211
|
+
private isInteractive = false;
|
|
212
|
+
|
|
213
|
+
// Injection state
|
|
214
|
+
private pendingInjections: Map<string, {
|
|
215
|
+
resolve: (success: boolean) => void;
|
|
216
|
+
reject: (error: Error) => void;
|
|
217
|
+
timeout: NodeJS.Timeout;
|
|
218
|
+
from: string; // For verification pattern matching
|
|
219
|
+
shortId: string; // First 8 chars of messageId for verification
|
|
220
|
+
retryCount: number; // Track retry attempts
|
|
221
|
+
originalBody: string; // Original injection content for retries
|
|
222
|
+
}> = new Map();
|
|
223
|
+
private backpressureActive = false;
|
|
224
|
+
private readyForMessages = false;
|
|
225
|
+
|
|
226
|
+
// Adaptive throttle for message queue - adjusts delay based on success/failure
|
|
227
|
+
private throttle = new AdaptiveThrottle();
|
|
228
|
+
|
|
229
|
+
// Unread message indicator state
|
|
230
|
+
private lastUnreadIndicatorTime = 0;
|
|
231
|
+
private readonly UNREAD_INDICATOR_COOLDOWN_MS = 5000; // Don't spam indicators
|
|
232
|
+
|
|
233
|
+
// Track whether any output has been received from the CLI
|
|
234
|
+
private hasReceivedOutput = false;
|
|
235
|
+
|
|
236
|
+
// Queue monitor for stuck message detection
|
|
237
|
+
private queueMonitorTimer?: NodeJS.Timeout;
|
|
238
|
+
private readonly QUEUE_MONITOR_INTERVAL_MS = 5000; // Check every 5 seconds
|
|
239
|
+
private injectionStartTime = 0; // Track when isInjecting was set to true
|
|
240
|
+
private readonly MAX_INJECTION_STUCK_MS = 60000; // Force reset after 60 seconds
|
|
241
|
+
|
|
242
|
+
// Protocol monitor for detecting agent mistakes (e.g., empty AGENT_RELAY_NAME)
|
|
243
|
+
private protocolWatcher?: FSWatcher;
|
|
244
|
+
private protocolReminderCooldown = 0; // Prevent spam
|
|
245
|
+
private readonly PROTOCOL_REMINDER_COOLDOWN_MS = 30000; // 30 second cooldown between reminders
|
|
246
|
+
|
|
247
|
+
// Periodic protocol reminder for long sessions (agents sometimes forget the protocol)
|
|
248
|
+
private periodicReminderTimer?: NodeJS.Timeout;
|
|
249
|
+
private readonly PERIODIC_REMINDER_INTERVAL_MS = 45 * 60 * 1000; // 45 minutes
|
|
250
|
+
private sessionStartTime = 0;
|
|
251
|
+
|
|
252
|
+
// Track if agent is being gracefully stopped (vs crashed)
|
|
253
|
+
private isGracefulStop = false;
|
|
254
|
+
|
|
255
|
+
// Track early process exit for better error messages
|
|
256
|
+
private earlyExitInfo?: { code: number | null; signal: NodeJS.Signals | null; stderr: string };
|
|
257
|
+
|
|
258
|
+
// Memory/CPU monitoring
|
|
259
|
+
private memoryMonitor: AgentMemoryMonitor;
|
|
260
|
+
private memoryAlertHandler: ((alert: MemoryAlert) => void) | null = null;
|
|
261
|
+
|
|
262
|
+
// CPU limiting via cgroups (optional, Linux only)
|
|
263
|
+
private cgroupManager: CgroupManager;
|
|
264
|
+
private hasCgroupSetup = false;
|
|
265
|
+
|
|
266
|
+
// Note: sessionEndProcessed and lastSummaryRawContent are inherited from BaseWrapper
|
|
267
|
+
|
|
268
|
+
constructor(config: RelayPtyOrchestratorConfig) {
|
|
269
|
+
super(config);
|
|
270
|
+
this.config = config;
|
|
271
|
+
|
|
272
|
+
// Validate agent name to prevent path traversal attacks
|
|
273
|
+
if (config.name.includes('..') || config.name.includes('/') || config.name.includes('\\')) {
|
|
274
|
+
throw new Error(`Invalid agent name: "${config.name}" contains path traversal characters`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Get project paths (used for logs and local mode)
|
|
278
|
+
const projectPaths = getProjectPaths(config.cwd);
|
|
279
|
+
|
|
280
|
+
// Canonical outbox path - agents ALWAYS write here (transparent symlink in workspace mode)
|
|
281
|
+
// Uses ~/.agent-relay/outbox/{agentName}/ so agents don't need to know about workspace IDs
|
|
282
|
+
this._canonicalOutboxPath = join(projectPaths.dataDir, 'outbox', config.name);
|
|
283
|
+
|
|
284
|
+
// Check for workspace namespacing (for multi-tenant cloud deployment)
|
|
285
|
+
// WORKSPACE_ID can be in process.env or passed via config.env
|
|
286
|
+
const workspaceId = config.env?.WORKSPACE_ID || process.env.WORKSPACE_ID;
|
|
287
|
+
this._workspaceId = workspaceId;
|
|
288
|
+
|
|
289
|
+
if (workspaceId) {
|
|
290
|
+
// Workspace mode: relay-pty watches the actual workspace path
|
|
291
|
+
// Canonical path (~/.agent-relay/outbox/) will be symlinked to workspace path
|
|
292
|
+
const getWorkspacePaths = (id: string) => {
|
|
293
|
+
const workspaceDir = `/tmp/relay/${id}`;
|
|
294
|
+
return {
|
|
295
|
+
workspaceDir,
|
|
296
|
+
socketPath: `${workspaceDir}/sockets/${config.name}.sock`,
|
|
297
|
+
outboxPath: `${workspaceDir}/outbox/${config.name}`,
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
let paths = getWorkspacePaths(workspaceId);
|
|
302
|
+
if (paths.socketPath.length > MAX_SOCKET_PATH_LENGTH) {
|
|
303
|
+
const hashedWorkspaceId = hashWorkspaceId(workspaceId);
|
|
304
|
+
const hashedPaths = getWorkspacePaths(hashedWorkspaceId);
|
|
305
|
+
console.warn(
|
|
306
|
+
`[relay-pty-orchestrator:${config.name}] Socket path too long (${paths.socketPath.length} chars); using hashed workspace id ${hashedWorkspaceId}`
|
|
307
|
+
);
|
|
308
|
+
paths = hashedPaths;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (paths.socketPath.length > MAX_SOCKET_PATH_LENGTH) {
|
|
312
|
+
throw new Error(`Socket path exceeds ${MAX_SOCKET_PATH_LENGTH} chars: ${paths.socketPath.length}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this.socketPath = paths.socketPath;
|
|
316
|
+
// relay-pty watches the actual workspace path
|
|
317
|
+
this._outboxPath = paths.outboxPath;
|
|
318
|
+
// Legacy path for backwards compat (older agents might still use /tmp/relay-outbox)
|
|
319
|
+
this._legacyOutboxPath = `/tmp/relay-outbox/${config.name}`;
|
|
320
|
+
} else {
|
|
321
|
+
// Local mode: use project paths directly (no symlinks needed)
|
|
322
|
+
this._outboxPath = this._canonicalOutboxPath;
|
|
323
|
+
// Socket path: use ~/.agent-relay/sockets/{projectId}/{agentName}.sock
|
|
324
|
+
// This keeps paths short (uses 12-char hashed projectId) while staying organized
|
|
325
|
+
// Example: /Users/foo/.agent-relay/sockets/abc123def456/MyAgent.sock (~65 chars)
|
|
326
|
+
this.socketPath = join(homedir(), '.agent-relay', 'sockets', projectPaths.projectId, `${config.name}.sock`);
|
|
327
|
+
// Legacy path for backwards compat (older agents might still use /tmp/relay-outbox)
|
|
328
|
+
// Even in local mode, we need this symlink for agents with stale instructions
|
|
329
|
+
this._legacyOutboxPath = `/tmp/relay-outbox/${config.name}`;
|
|
330
|
+
}
|
|
331
|
+
if (this.socketPath.length > MAX_SOCKET_PATH_LENGTH) {
|
|
332
|
+
throw new Error(`Socket path exceeds ${MAX_SOCKET_PATH_LENGTH} chars: ${this.socketPath.length}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Generate log path using project paths
|
|
336
|
+
this._logPath = join(projectPaths.teamDir, 'worker-logs', `${config.name}.log`);
|
|
337
|
+
|
|
338
|
+
// Check if we're running interactively (stdin is a TTY)
|
|
339
|
+
// If headless mode is forced via config, always use pipes
|
|
340
|
+
this.isInteractive = config.headless ? false : (process.stdin.isTTY === true);
|
|
341
|
+
|
|
342
|
+
// Initialize memory monitor (shared singleton, 10s polling interval)
|
|
343
|
+
this.memoryMonitor = getMemoryMonitor({ checkIntervalMs: 10_000 });
|
|
344
|
+
|
|
345
|
+
// Initialize cgroup manager for CPU limiting (shared singleton)
|
|
346
|
+
this.cgroupManager = getCgroupManager();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Debug log - only outputs when debug is enabled
|
|
351
|
+
* Writes to log file to avoid polluting TUI output
|
|
352
|
+
*/
|
|
353
|
+
private log(message: string): void {
|
|
354
|
+
if (this.config.debug) {
|
|
355
|
+
const logLine = `${new Date().toISOString()} [relay-pty-orchestrator:${this.config.name}] ${message}\n`;
|
|
356
|
+
try {
|
|
357
|
+
const logDir = dirname(this._logPath);
|
|
358
|
+
if (!existsSync(logDir)) {
|
|
359
|
+
mkdirSync(logDir, { recursive: true });
|
|
360
|
+
}
|
|
361
|
+
appendFileSync(this._logPath, logLine);
|
|
362
|
+
} catch {
|
|
363
|
+
// Fallback to stderr if file write fails (only during init before _logPath is set)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Error log - always outputs (errors are important)
|
|
370
|
+
* Writes to log file to avoid polluting TUI output
|
|
371
|
+
*/
|
|
372
|
+
private logError(message: string): void {
|
|
373
|
+
const logLine = `${new Date().toISOString()} [relay-pty-orchestrator:${this.config.name}] ERROR: ${message}\n`;
|
|
374
|
+
try {
|
|
375
|
+
const logDir = dirname(this._logPath);
|
|
376
|
+
if (!existsSync(logDir)) {
|
|
377
|
+
mkdirSync(logDir, { recursive: true });
|
|
378
|
+
}
|
|
379
|
+
appendFileSync(this._logPath, logLine);
|
|
380
|
+
} catch {
|
|
381
|
+
// Fallback to stderr if file write fails (only during init before _logPath is set)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Get the outbox path for this agent (for documentation purposes)
|
|
387
|
+
*/
|
|
388
|
+
get outboxPath(): string {
|
|
389
|
+
return this._outboxPath;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// =========================================================================
|
|
393
|
+
// Abstract method implementations (required by BaseWrapper)
|
|
394
|
+
// =========================================================================
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Start the relay-pty process and connect to socket
|
|
398
|
+
*/
|
|
399
|
+
override async start(): Promise<void> {
|
|
400
|
+
if (this.running) return;
|
|
401
|
+
|
|
402
|
+
this.log(` Starting...`);
|
|
403
|
+
|
|
404
|
+
// Ensure socket directory exists (for workspace-namespaced paths)
|
|
405
|
+
const socketDir = dirname(this.socketPath);
|
|
406
|
+
try {
|
|
407
|
+
if (!existsSync(socketDir)) {
|
|
408
|
+
mkdirSync(socketDir, { recursive: true });
|
|
409
|
+
this.log(` Created socket directory: ${socketDir}`);
|
|
410
|
+
}
|
|
411
|
+
} catch (err: any) {
|
|
412
|
+
this.logError(` Failed to create socket directory: ${err.message}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Clean up any stale socket from previous crashed process
|
|
416
|
+
try {
|
|
417
|
+
if (existsSync(this.socketPath)) {
|
|
418
|
+
this.log(` Removing stale socket: ${this.socketPath}`);
|
|
419
|
+
unlinkSync(this.socketPath);
|
|
420
|
+
}
|
|
421
|
+
} catch (err: any) {
|
|
422
|
+
this.logError(` Failed to clean up socket: ${err.message}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Set up outbox directory structure
|
|
426
|
+
// - Workspace mode:
|
|
427
|
+
// 1. Create actual workspace path /tmp/relay/{workspaceId}/outbox/{name}
|
|
428
|
+
// 2. Symlink canonical ~/.agent-relay/outbox/{name} -> workspace path
|
|
429
|
+
// 3. Optional: symlink /tmp/relay-outbox/{name} -> workspace path (backwards compat)
|
|
430
|
+
// - Local mode: just create ~/.agent-relay/{projectId}/outbox/{name} directly
|
|
431
|
+
try {
|
|
432
|
+
// Ensure the actual outbox directory exists (where relay-pty watches)
|
|
433
|
+
const outboxDir = dirname(this._outboxPath);
|
|
434
|
+
if (!existsSync(outboxDir)) {
|
|
435
|
+
mkdirSync(outboxDir, { recursive: true });
|
|
436
|
+
}
|
|
437
|
+
if (!existsSync(this._outboxPath)) {
|
|
438
|
+
mkdirSync(this._outboxPath, { recursive: true });
|
|
439
|
+
}
|
|
440
|
+
this.log(` Created outbox directory: ${this._outboxPath}`);
|
|
441
|
+
|
|
442
|
+
// Helper to create a symlink, cleaning up existing path first
|
|
443
|
+
const createSymlinkSafe = (linkPath: string, targetPath: string) => {
|
|
444
|
+
const linkParent = dirname(linkPath);
|
|
445
|
+
if (!existsSync(linkParent)) {
|
|
446
|
+
mkdirSync(linkParent, { recursive: true });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Remove existing path if it exists (file, symlink, or directory)
|
|
450
|
+
// Use lstatSync instead of existsSync to detect broken symlinks
|
|
451
|
+
// (existsSync returns false for broken symlinks, but the symlink itself still exists)
|
|
452
|
+
let pathExists = false;
|
|
453
|
+
try {
|
|
454
|
+
lstatSync(linkPath);
|
|
455
|
+
pathExists = true;
|
|
456
|
+
} catch {
|
|
457
|
+
// Path doesn't exist at all - proceed to create symlink
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (pathExists) {
|
|
461
|
+
try {
|
|
462
|
+
const stats = lstatSync(linkPath);
|
|
463
|
+
if (stats.isSymbolicLink()) {
|
|
464
|
+
// Handle both valid and broken symlinks
|
|
465
|
+
try {
|
|
466
|
+
const currentTarget = readlinkSync(linkPath);
|
|
467
|
+
if (currentTarget === targetPath) {
|
|
468
|
+
// Symlink already points to correct target, no need to recreate
|
|
469
|
+
this.log(` Symlink already exists and is correct: ${linkPath} -> ${targetPath}`);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
} catch {
|
|
473
|
+
// Broken symlink (target doesn't exist) - remove it
|
|
474
|
+
this.log(` Removing broken symlink: ${linkPath}`);
|
|
475
|
+
}
|
|
476
|
+
unlinkSync(linkPath);
|
|
477
|
+
} else if (stats.isFile()) {
|
|
478
|
+
unlinkSync(linkPath);
|
|
479
|
+
} else if (stats.isDirectory()) {
|
|
480
|
+
// Force remove directory - this is critical for fixing existing directories
|
|
481
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
482
|
+
// Verify removal succeeded using lstatSync to catch broken symlinks
|
|
483
|
+
try {
|
|
484
|
+
lstatSync(linkPath);
|
|
485
|
+
throw new Error(`Failed to remove existing directory: ${linkPath}`);
|
|
486
|
+
} catch (err: any) {
|
|
487
|
+
if (err.code !== 'ENOENT') {
|
|
488
|
+
throw err; // Re-throw if it's not a "doesn't exist" error
|
|
489
|
+
}
|
|
490
|
+
// Path successfully removed
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
} catch (err: any) {
|
|
494
|
+
// Log cleanup errors instead of silently ignoring them
|
|
495
|
+
this.logError(` Failed to clean up existing path ${linkPath}: ${err.message}`);
|
|
496
|
+
throw err; // Re-throw to prevent symlink creation on failed cleanup
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Create the symlink
|
|
501
|
+
try {
|
|
502
|
+
symlinkSync(targetPath, linkPath);
|
|
503
|
+
// Verify symlink was created correctly
|
|
504
|
+
if (!existsSync(linkPath)) {
|
|
505
|
+
throw new Error(`Symlink creation failed: ${linkPath}`);
|
|
506
|
+
}
|
|
507
|
+
const verifyStats = lstatSync(linkPath);
|
|
508
|
+
if (!verifyStats.isSymbolicLink()) {
|
|
509
|
+
throw new Error(`Created path is not a symlink: ${linkPath}`);
|
|
510
|
+
}
|
|
511
|
+
const verifyTarget = readlinkSync(linkPath);
|
|
512
|
+
if (verifyTarget !== targetPath) {
|
|
513
|
+
throw new Error(`Symlink points to wrong target: expected ${targetPath}, got ${verifyTarget}`);
|
|
514
|
+
}
|
|
515
|
+
this.log(` Created symlink: ${linkPath} -> ${targetPath}`);
|
|
516
|
+
} catch (err: any) {
|
|
517
|
+
this.logError(` Failed to create symlink ${linkPath} -> ${targetPath}: ${err.message}`);
|
|
518
|
+
throw err;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// In workspace mode, create symlinks so agents can use canonical path
|
|
523
|
+
if (this._workspaceId) {
|
|
524
|
+
// Symlink canonical path (~/.agent-relay/outbox/{name}) -> workspace path
|
|
525
|
+
// This is the PRIMARY symlink - agents write to canonical path, relay-pty watches workspace path
|
|
526
|
+
if (this._canonicalOutboxPath !== this._outboxPath) {
|
|
527
|
+
createSymlinkSafe(this._canonicalOutboxPath, this._outboxPath);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Also create legacy /tmp/relay-outbox symlink for backwards compat with older agents
|
|
531
|
+
if (this._legacyOutboxPath !== this._outboxPath && this._legacyOutboxPath !== this._canonicalOutboxPath) {
|
|
532
|
+
createSymlinkSafe(this._legacyOutboxPath, this._outboxPath);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// In local mode, also create legacy symlink for backwards compat with stale instructions
|
|
537
|
+
if (!this._workspaceId && this._legacyOutboxPath !== this._outboxPath) {
|
|
538
|
+
createSymlinkSafe(this._legacyOutboxPath, this._outboxPath);
|
|
539
|
+
}
|
|
540
|
+
} catch (err: any) {
|
|
541
|
+
this.logError(` Failed to set up outbox: ${err.message}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Write MCP identity file so MCP servers can discover their agent name
|
|
545
|
+
// This is needed because Claude Code may not pass through env vars to MCP server processes
|
|
546
|
+
try {
|
|
547
|
+
const projectPaths = getProjectPaths(this.config.cwd);
|
|
548
|
+
const identityDir = join(projectPaths.dataDir);
|
|
549
|
+
if (!existsSync(identityDir)) {
|
|
550
|
+
mkdirSync(identityDir, { recursive: true });
|
|
551
|
+
}
|
|
552
|
+
// Write a per-process identity file (using PPID so MCP server finds parent's identity)
|
|
553
|
+
const identityPath = join(identityDir, `mcp-identity-${process.pid}`);
|
|
554
|
+
writeFileSync(identityPath, this.config.name, 'utf-8');
|
|
555
|
+
this.log(` Wrote MCP identity file: ${identityPath}`);
|
|
556
|
+
|
|
557
|
+
// Also write a simple identity file (for single-agent scenarios)
|
|
558
|
+
const simpleIdentityPath = join(identityDir, 'mcp-identity');
|
|
559
|
+
writeFileSync(simpleIdentityPath, this.config.name, 'utf-8');
|
|
560
|
+
} catch (err: any) {
|
|
561
|
+
this.logError(` Failed to write MCP identity file: ${err.message}`);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Find relay-pty binary
|
|
565
|
+
const binaryPath = this.findRelayPtyBinary();
|
|
566
|
+
if (!binaryPath) {
|
|
567
|
+
throw new Error('relay-pty binary not found. Build with: cd relay-pty && cargo build --release');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
this.log(` Using binary: ${binaryPath}`);
|
|
571
|
+
|
|
572
|
+
// Spawn relay-pty process FIRST (before connecting to daemon)
|
|
573
|
+
// This ensures the CLI is actually running before we register with the daemon
|
|
574
|
+
await this.spawnRelayPty(binaryPath);
|
|
575
|
+
|
|
576
|
+
// Wait for socket to become available and connect
|
|
577
|
+
await this.connectToSocket();
|
|
578
|
+
|
|
579
|
+
// Connect to relay daemon AFTER CLI is spawned
|
|
580
|
+
// This prevents the spawner from seeing us as "registered" before the CLI runs
|
|
581
|
+
try {
|
|
582
|
+
await this.client.connect();
|
|
583
|
+
this.log(` Relay daemon connected`);
|
|
584
|
+
} catch (err: any) {
|
|
585
|
+
this.logError(` Relay connect failed: ${err.message}`);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
this.running = true;
|
|
589
|
+
// DON'T set readyForMessages yet - wait for CLI to be ready first
|
|
590
|
+
// This prevents messages from being injected during CLI startup
|
|
591
|
+
this.startStuckDetection();
|
|
592
|
+
this.startQueueMonitor();
|
|
593
|
+
this.startProtocolMonitor();
|
|
594
|
+
this.startPeriodicReminder();
|
|
595
|
+
|
|
596
|
+
this.log(` Socket connected: ${this.socketConnected}`);
|
|
597
|
+
this.log(` Relay client state: ${this.client.state}`);
|
|
598
|
+
|
|
599
|
+
// Wait for CLI to be fully ready (output received + idle state)
|
|
600
|
+
// This ensures we don't inject messages while the CLI is still starting up
|
|
601
|
+
// Messages arriving via daemon during this time will be queued but not processed
|
|
602
|
+
this.log(` Waiting for CLI to be ready before accepting messages...`);
|
|
603
|
+
const cliReady = await this.waitUntilCliReady(30000, 100);
|
|
604
|
+
if (cliReady) {
|
|
605
|
+
this.log(` CLI is ready, enabling message processing`);
|
|
606
|
+
} else {
|
|
607
|
+
this.log(` CLI readiness timeout, enabling message processing anyway`);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Now enable message processing
|
|
611
|
+
this.readyForMessages = true;
|
|
612
|
+
this.log(` Ready for messages`);
|
|
613
|
+
|
|
614
|
+
// Process any queued messages that arrived during startup
|
|
615
|
+
this.processMessageQueue();
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Stop the relay-pty process gracefully
|
|
620
|
+
*/
|
|
621
|
+
override async stop(): Promise<void> {
|
|
622
|
+
if (!this.running) return;
|
|
623
|
+
this.isGracefulStop = true; // Mark as graceful to prevent crash broadcast
|
|
624
|
+
this.running = false;
|
|
625
|
+
this.stopStuckDetection();
|
|
626
|
+
this.stopQueueMonitor();
|
|
627
|
+
this.stopProtocolMonitor();
|
|
628
|
+
this.stopPeriodicReminder();
|
|
629
|
+
|
|
630
|
+
// Clear socket reconnect timer
|
|
631
|
+
if (this.socketReconnectTimer) {
|
|
632
|
+
clearTimeout(this.socketReconnectTimer);
|
|
633
|
+
this.socketReconnectTimer = undefined;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Unregister from memory monitor
|
|
637
|
+
this.memoryMonitor.unregister(this.config.name);
|
|
638
|
+
if (this.memoryAlertHandler) {
|
|
639
|
+
this.memoryMonitor.off('alert', this.memoryAlertHandler);
|
|
640
|
+
this.memoryAlertHandler = null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Clean up cgroup if we set one up
|
|
644
|
+
if (this.hasCgroupSetup) {
|
|
645
|
+
await this.cgroupManager.removeAgentCgroup(this.config.name);
|
|
646
|
+
this.hasCgroupSetup = false;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
this.log(` Stopping...`);
|
|
650
|
+
|
|
651
|
+
// Send shutdown command via socket
|
|
652
|
+
if (this.socket && this.socketConnected) {
|
|
653
|
+
try {
|
|
654
|
+
await this.sendSocketRequest({ type: 'shutdown' });
|
|
655
|
+
} catch {
|
|
656
|
+
// Ignore errors during shutdown
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Close socket
|
|
661
|
+
this.disconnectSocket();
|
|
662
|
+
|
|
663
|
+
// Kill process if still running
|
|
664
|
+
if (this.relayPtyProcess && !this.relayPtyProcess.killed) {
|
|
665
|
+
this.relayPtyProcess.kill('SIGTERM');
|
|
666
|
+
|
|
667
|
+
// Force kill after timeout
|
|
668
|
+
await Promise.race([
|
|
669
|
+
new Promise<void>((resolve) => {
|
|
670
|
+
this.relayPtyProcess?.on('exit', () => resolve());
|
|
671
|
+
}),
|
|
672
|
+
sleep(5000).then(() => {
|
|
673
|
+
if (this.relayPtyProcess && !this.relayPtyProcess.killed) {
|
|
674
|
+
this.relayPtyProcess.kill('SIGKILL');
|
|
675
|
+
}
|
|
676
|
+
}),
|
|
677
|
+
]);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Cleanup relay client
|
|
681
|
+
this.destroyClient();
|
|
682
|
+
|
|
683
|
+
// Clean up socket file
|
|
684
|
+
try {
|
|
685
|
+
if (existsSync(this.socketPath)) {
|
|
686
|
+
unlinkSync(this.socketPath);
|
|
687
|
+
this.log(` Cleaned up socket: ${this.socketPath}`);
|
|
688
|
+
}
|
|
689
|
+
} catch (err: any) {
|
|
690
|
+
this.logError(` Failed to clean up socket: ${err.message}`);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
this.log(` Stopped`);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Inject content into the agent via socket
|
|
698
|
+
*/
|
|
699
|
+
protected async performInjection(_content: string): Promise<void> {
|
|
700
|
+
// This is called by BaseWrapper but we handle injection differently
|
|
701
|
+
// via the socket protocol in processMessageQueue
|
|
702
|
+
throw new Error('Use injectMessage() instead of performInjection()');
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Get cleaned output for parsing
|
|
707
|
+
*/
|
|
708
|
+
protected getCleanOutput(): string {
|
|
709
|
+
return stripAnsi(this.rawBuffer);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// =========================================================================
|
|
713
|
+
// Process management
|
|
714
|
+
// =========================================================================
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Find the relay-pty binary
|
|
718
|
+
* Uses shared utility from @agent-relay/utils
|
|
719
|
+
*/
|
|
720
|
+
private findRelayPtyBinary(): string | null {
|
|
721
|
+
// Check config path first
|
|
722
|
+
if (this.config.relayPtyPath && existsSync(this.config.relayPtyPath)) {
|
|
723
|
+
return this.config.relayPtyPath;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Use shared utility with current module's __dirname
|
|
727
|
+
return findRelayPtyBinaryUtil(__dirname);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Spawn the relay-pty process
|
|
732
|
+
*/
|
|
733
|
+
private async spawnRelayPty(binaryPath: string): Promise<void> {
|
|
734
|
+
// Get terminal dimensions for proper rendering
|
|
735
|
+
const rows = process.stdout.rows || 24;
|
|
736
|
+
const cols = process.stdout.columns || 80;
|
|
737
|
+
|
|
738
|
+
const args = [
|
|
739
|
+
'--name', this.config.name,
|
|
740
|
+
'--socket', this.socketPath,
|
|
741
|
+
'--idle-timeout', String(this.config.idleBeforeInjectMs ?? 500),
|
|
742
|
+
'--json-output', // Enable Rust parsing output
|
|
743
|
+
'--rows', String(rows),
|
|
744
|
+
'--cols', String(cols),
|
|
745
|
+
'--log-level', 'warn', // Only show warnings and errors
|
|
746
|
+
'--log-file', this._logPath, // Enable output logging
|
|
747
|
+
'--outbox', this._outboxPath, // Enable file-based relay messages
|
|
748
|
+
'--', this.config.command,
|
|
749
|
+
...(this.config.args ?? []),
|
|
750
|
+
];
|
|
751
|
+
|
|
752
|
+
this.log(` Spawning: ${binaryPath} ${args.join(' ')}`);
|
|
753
|
+
|
|
754
|
+
// Reset early exit info from any previous spawn attempt
|
|
755
|
+
this.earlyExitInfo = undefined;
|
|
756
|
+
|
|
757
|
+
// For interactive mode, let Rust directly inherit stdin/stdout from the terminal
|
|
758
|
+
// This is more robust than manual forwarding through pipes
|
|
759
|
+
// We still pipe stderr to capture JSON parsed commands
|
|
760
|
+
const stdio: ('inherit' | 'pipe')[] = this.isInteractive
|
|
761
|
+
? ['inherit', 'inherit', 'pipe'] // Rust handles terminal directly
|
|
762
|
+
: ['pipe', 'pipe', 'pipe']; // Headless mode - we handle I/O
|
|
763
|
+
|
|
764
|
+
const proc = spawn(binaryPath, args, {
|
|
765
|
+
cwd: this.config.cwd ?? process.cwd(),
|
|
766
|
+
env: {
|
|
767
|
+
...process.env,
|
|
768
|
+
...this.config.env,
|
|
769
|
+
AGENT_RELAY_NAME: this.config.name,
|
|
770
|
+
RELAY_AGENT_NAME: this.config.name, // MCP server uses this env var
|
|
771
|
+
AGENT_RELAY_OUTBOX: this._canonicalOutboxPath, // Agents use this for outbox path
|
|
772
|
+
TERM: 'xterm-256color',
|
|
773
|
+
},
|
|
774
|
+
stdio,
|
|
775
|
+
});
|
|
776
|
+
this.relayPtyProcess = proc;
|
|
777
|
+
|
|
778
|
+
// Handle stdout (agent output) - only in headless mode
|
|
779
|
+
if (!this.isInteractive && proc.stdout) {
|
|
780
|
+
proc.stdout.on('data', (data: Buffer) => {
|
|
781
|
+
const text = data.toString();
|
|
782
|
+
this.handleOutput(text);
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Capture stderr for early exit diagnosis
|
|
787
|
+
let stderrBuffer = '';
|
|
788
|
+
|
|
789
|
+
// Handle stderr (relay-pty logs and JSON output) - always needed
|
|
790
|
+
// Also captures to buffer for error diagnostics if process dies early
|
|
791
|
+
if (proc.stderr) {
|
|
792
|
+
proc.stderr.on('data', (data: Buffer) => {
|
|
793
|
+
const text = data.toString();
|
|
794
|
+
stderrBuffer += text;
|
|
795
|
+
this.handleStderr(text);
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Handle exit
|
|
800
|
+
proc.on('exit', (code, signal) => {
|
|
801
|
+
const exitCode = code ?? (signal === 'SIGKILL' ? 137 : 1);
|
|
802
|
+
this.log(` Process exited: code=${exitCode} signal=${signal}`);
|
|
803
|
+
|
|
804
|
+
// Capture early exit info for better error messages if socket not yet connected
|
|
805
|
+
if (!this.socketConnected) {
|
|
806
|
+
this.earlyExitInfo = { code, signal, stderr: stderrBuffer };
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
this.running = false;
|
|
810
|
+
|
|
811
|
+
// Get crash context before unregistering from memory monitor
|
|
812
|
+
const crashContext = this.memoryMonitor.getCrashContext(this.config.name);
|
|
813
|
+
|
|
814
|
+
// Unregister from memory monitor
|
|
815
|
+
this.memoryMonitor.unregister(this.config.name);
|
|
816
|
+
if (this.memoryAlertHandler) {
|
|
817
|
+
this.memoryMonitor.off('alert', this.memoryAlertHandler);
|
|
818
|
+
this.memoryAlertHandler = null;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Clean up cgroup (fire and forget - process already exited)
|
|
822
|
+
if (this.hasCgroupSetup) {
|
|
823
|
+
this.cgroupManager.removeAgentCgroup(this.config.name).catch(() => {});
|
|
824
|
+
this.hasCgroupSetup = false;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Broadcast crash notification if not a graceful stop
|
|
828
|
+
if (!this.isGracefulStop && this.client.state === 'READY') {
|
|
829
|
+
const canBroadcast = typeof (this.client as any).broadcast === 'function';
|
|
830
|
+
const isNormalExit = exitCode === 0;
|
|
831
|
+
const wasKilled = signal === 'SIGKILL' || signal === 'SIGTERM' || exitCode === 137;
|
|
832
|
+
|
|
833
|
+
if (!isNormalExit) {
|
|
834
|
+
const reason = wasKilled
|
|
835
|
+
? `killed by signal ${signal || 'SIGKILL'}`
|
|
836
|
+
: `exit code ${exitCode}`;
|
|
837
|
+
|
|
838
|
+
// Include crash context analysis if available
|
|
839
|
+
const contextInfo = crashContext.likelyCause !== 'unknown'
|
|
840
|
+
? ` Likely cause: ${crashContext.likelyCause}. ${crashContext.analysisNotes.slice(0, 2).join('. ')}`
|
|
841
|
+
: '';
|
|
842
|
+
|
|
843
|
+
const message = `AGENT CRASHED: "${this.config.name}" has died unexpectedly (${reason}).${contextInfo}`;
|
|
844
|
+
|
|
845
|
+
this.log(` Broadcasting crash notification: ${message}`);
|
|
846
|
+
if (canBroadcast) {
|
|
847
|
+
this.client.broadcast(message, 'message', {
|
|
848
|
+
isSystemMessage: true,
|
|
849
|
+
agentName: this.config.name,
|
|
850
|
+
exitCode,
|
|
851
|
+
signal: signal || undefined,
|
|
852
|
+
crashType: 'unexpected_exit',
|
|
853
|
+
crashContext: {
|
|
854
|
+
likelyCause: crashContext.likelyCause,
|
|
855
|
+
peakMemory: crashContext.peakMemory,
|
|
856
|
+
averageMemory: crashContext.averageMemory,
|
|
857
|
+
memoryTrend: crashContext.memoryTrend,
|
|
858
|
+
},
|
|
859
|
+
});
|
|
860
|
+
} else {
|
|
861
|
+
this.log(' broadcast skipped: client.broadcast not available');
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
this.emit('exit', exitCode);
|
|
867
|
+
this.config.onExit?.(exitCode);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
// Handle error
|
|
871
|
+
proc.on('error', (err) => {
|
|
872
|
+
this.logError(` Process error: ${err.message}`);
|
|
873
|
+
this.emit('error', err);
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
// Wait for process to start
|
|
877
|
+
await sleep(500);
|
|
878
|
+
|
|
879
|
+
if (proc.exitCode !== null) {
|
|
880
|
+
// Include any captured stderr in the error for debugging
|
|
881
|
+
const stderrInfo = stderrBuffer ? `\nStderr: ${stderrBuffer.slice(0, 500)}` : '';
|
|
882
|
+
throw new Error(`relay-pty exited immediately with code ${proc.exitCode}${stderrInfo}`);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Register for memory/CPU monitoring
|
|
886
|
+
if (proc.pid) {
|
|
887
|
+
this.memoryMonitor.register(this.config.name, proc.pid);
|
|
888
|
+
this.memoryMonitor.start(); // Idempotent - starts if not already running
|
|
889
|
+
|
|
890
|
+
// Set up CPU limiting via cgroups (if configured and available)
|
|
891
|
+
// This prevents one agent from starving others during npm install/build
|
|
892
|
+
if (this.config.cpuLimitPercent && this.config.cpuLimitPercent > 0) {
|
|
893
|
+
this.setupCgroupLimit(proc.pid, this.config.cpuLimitPercent).catch((err) => {
|
|
894
|
+
this.log(` Failed to set up cgroup CPU limit: ${err.message}`);
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Set up alert handler to send resource alerts to dashboard only (not other agents)
|
|
899
|
+
this.memoryAlertHandler = (alert: MemoryAlert) => {
|
|
900
|
+
if (alert.agentName !== this.config.name) return;
|
|
901
|
+
if (this.client.state !== 'READY') return;
|
|
902
|
+
|
|
903
|
+
const message = alert.type === 'recovered'
|
|
904
|
+
? `AGENT RECOVERED: "${this.config.name}" memory usage returned to normal.`
|
|
905
|
+
: `AGENT RESOURCE ALERT: "${this.config.name}" - ${alert.message} (${formatBytes(alert.currentRss)})`;
|
|
906
|
+
|
|
907
|
+
this.log(` Sending resource alert to users: ${message}`);
|
|
908
|
+
// Send to all human users - agents don't need to know about each other's resource usage
|
|
909
|
+
this.client.sendMessage('@users', message, 'message', {
|
|
910
|
+
isSystemMessage: true,
|
|
911
|
+
agentName: this.config.name,
|
|
912
|
+
alertType: alert.type,
|
|
913
|
+
currentMemory: alert.currentRss,
|
|
914
|
+
threshold: alert.threshold,
|
|
915
|
+
recommendation: alert.recommendation,
|
|
916
|
+
});
|
|
917
|
+
};
|
|
918
|
+
this.memoryMonitor.on('alert', this.memoryAlertHandler);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Set up cgroup CPU limit for this agent
|
|
924
|
+
*/
|
|
925
|
+
private async setupCgroupLimit(pid: number, cpuPercent: number): Promise<void> {
|
|
926
|
+
await this.cgroupManager.initialize();
|
|
927
|
+
|
|
928
|
+
if (!this.cgroupManager.isAvailable()) {
|
|
929
|
+
this.log(` cgroups not available, skipping CPU limit`);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const created = await this.cgroupManager.createAgentCgroup(this.config.name, { cpuPercent });
|
|
934
|
+
if (!created) {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const added = await this.cgroupManager.addProcess(this.config.name, pid);
|
|
939
|
+
if (added) {
|
|
940
|
+
this.hasCgroupSetup = true;
|
|
941
|
+
this.log(` CPU limit set to ${cpuPercent}% for agent ${this.config.name}`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Handle output from relay-pty stdout (headless mode only)
|
|
947
|
+
* In interactive mode, stdout goes directly to terminal via inherited stdio
|
|
948
|
+
*/
|
|
949
|
+
private handleOutput(data: string): void {
|
|
950
|
+
// Skip processing if agent is no longer running (prevents ghost messages after release)
|
|
951
|
+
if (!this.running) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
this.rawBuffer += data;
|
|
956
|
+
this.outputBuffer += data;
|
|
957
|
+
this.hasReceivedOutput = true;
|
|
958
|
+
|
|
959
|
+
// Feed to idle detector
|
|
960
|
+
this.feedIdleDetectorOutput(data);
|
|
961
|
+
|
|
962
|
+
// Check for unread messages and append indicator if needed
|
|
963
|
+
const indicator = this.formatUnreadIndicator();
|
|
964
|
+
const outputWithIndicator = indicator ? data + indicator : data;
|
|
965
|
+
|
|
966
|
+
// Emit output event (with indicator if present)
|
|
967
|
+
this.emit('output', outputWithIndicator);
|
|
968
|
+
|
|
969
|
+
// Stream to daemon if configured
|
|
970
|
+
if (this.config.streamLogs !== false && this.client.state === 'READY') {
|
|
971
|
+
this.client.sendLog(outputWithIndicator);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Parse for relay commands
|
|
975
|
+
this.parseRelayCommands();
|
|
976
|
+
|
|
977
|
+
// Check for summary and session end
|
|
978
|
+
const cleanContent = stripAnsi(this.rawBuffer);
|
|
979
|
+
this.checkForSummary(cleanContent);
|
|
980
|
+
this.checkForSessionEnd(cleanContent);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Format an unread message indicator if there are pending messages.
|
|
985
|
+
* Returns empty string if no pending messages or within cooldown period.
|
|
986
|
+
*
|
|
987
|
+
* Example output:
|
|
988
|
+
* ───────────────────────────
|
|
989
|
+
* 📬 2 unread messages (from: Alice, Bob)
|
|
990
|
+
*/
|
|
991
|
+
private formatUnreadIndicator(): string {
|
|
992
|
+
const queueLength = this.messageQueue.length;
|
|
993
|
+
if (queueLength === 0) {
|
|
994
|
+
return '';
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Check cooldown to avoid spamming
|
|
998
|
+
const now = Date.now();
|
|
999
|
+
if (now - this.lastUnreadIndicatorTime < this.UNREAD_INDICATOR_COOLDOWN_MS) {
|
|
1000
|
+
return '';
|
|
1001
|
+
}
|
|
1002
|
+
this.lastUnreadIndicatorTime = now;
|
|
1003
|
+
|
|
1004
|
+
// Collect unique sender names
|
|
1005
|
+
const senders = [...new Set(this.messageQueue.map(m => m.from))];
|
|
1006
|
+
const senderList = senders.slice(0, 3).join(', ');
|
|
1007
|
+
const moreCount = senders.length > 3 ? ` +${senders.length - 3} more` : '';
|
|
1008
|
+
|
|
1009
|
+
const line = '─'.repeat(27);
|
|
1010
|
+
const messageWord = queueLength === 1 ? 'message' : 'messages';
|
|
1011
|
+
|
|
1012
|
+
return `\n${line}\n📬 ${queueLength} unread ${messageWord} (from: ${senderList}${moreCount})\n`;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Handle stderr from relay-pty (logs and JSON parsed commands)
|
|
1017
|
+
*/
|
|
1018
|
+
private handleStderr(data: string): void {
|
|
1019
|
+
// Skip processing if agent is no longer running (prevents ghost messages after release)
|
|
1020
|
+
if (!this.running) {
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// relay-pty outputs JSON parsed commands to stderr with --json-output
|
|
1025
|
+
const lines = data.split('\n').filter(l => l.trim());
|
|
1026
|
+
for (const line of lines) {
|
|
1027
|
+
if (line.startsWith('{')) {
|
|
1028
|
+
// JSON output - parsed relay command from Rust
|
|
1029
|
+
try {
|
|
1030
|
+
const parsed = JSON.parse(line);
|
|
1031
|
+
if (parsed.type === 'relay_command' && parsed.kind) {
|
|
1032
|
+
// Log parsed commands (only in debug mode to avoid TUI pollution)
|
|
1033
|
+
if (parsed.kind === 'spawn' || parsed.kind === 'release') {
|
|
1034
|
+
this.log(`Rust parsed [${parsed.kind}]: ${JSON.stringify({
|
|
1035
|
+
spawn_name: parsed.spawn_name,
|
|
1036
|
+
spawn_cli: parsed.spawn_cli,
|
|
1037
|
+
spawn_task: parsed.spawn_task?.substring(0, 50),
|
|
1038
|
+
release_name: parsed.release_name,
|
|
1039
|
+
})}`);
|
|
1040
|
+
} else {
|
|
1041
|
+
this.log(`Rust parsed [${parsed.kind}]: ${parsed.from} -> ${parsed.to}`);
|
|
1042
|
+
}
|
|
1043
|
+
this.handleRustParsedCommand(parsed);
|
|
1044
|
+
} else if (parsed.type === 'continuity') {
|
|
1045
|
+
// Handle continuity commands from relay-pty file-based protocol
|
|
1046
|
+
this.log(`Rust parsed [continuity]: action=${parsed.action}`);
|
|
1047
|
+
this.handleRustContinuityCommand(parsed);
|
|
1048
|
+
}
|
|
1049
|
+
} catch (e) {
|
|
1050
|
+
// Not JSON, just log (only in debug mode)
|
|
1051
|
+
if (this.config.debug) {
|
|
1052
|
+
console.error(`[relay-pty:${this.config.name}] ${line}`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
} else {
|
|
1056
|
+
// Non-JSON stderr - only show in debug mode (logs, info messages)
|
|
1057
|
+
if (this.config.debug) {
|
|
1058
|
+
console.error(`[relay-pty:${this.config.name}] ${line}`);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Handle a parsed command from Rust relay-pty
|
|
1066
|
+
* Rust outputs structured JSON with 'kind' field: "message", "spawn", "release"
|
|
1067
|
+
*/
|
|
1068
|
+
private handleRustParsedCommand(parsed: {
|
|
1069
|
+
type: string;
|
|
1070
|
+
kind: string;
|
|
1071
|
+
from: string;
|
|
1072
|
+
to: string;
|
|
1073
|
+
body: string;
|
|
1074
|
+
raw: string;
|
|
1075
|
+
thread?: string;
|
|
1076
|
+
spawn_name?: string;
|
|
1077
|
+
spawn_cli?: string;
|
|
1078
|
+
spawn_task?: string;
|
|
1079
|
+
release_name?: string;
|
|
1080
|
+
}): void {
|
|
1081
|
+
switch (parsed.kind) {
|
|
1082
|
+
case 'spawn':
|
|
1083
|
+
if (parsed.spawn_name && parsed.spawn_cli) {
|
|
1084
|
+
this.log(` Spawn detected: ${parsed.spawn_name} (${parsed.spawn_cli})`);
|
|
1085
|
+
this.handleSpawnCommand(parsed.spawn_name, parsed.spawn_cli, parsed.spawn_task || '');
|
|
1086
|
+
}
|
|
1087
|
+
break;
|
|
1088
|
+
|
|
1089
|
+
case 'release':
|
|
1090
|
+
if (parsed.release_name) {
|
|
1091
|
+
this.log(`Release: ${parsed.release_name}`);
|
|
1092
|
+
this.handleReleaseCommand(parsed.release_name);
|
|
1093
|
+
} else {
|
|
1094
|
+
this.logError(`Missing release_name in parsed command: ${JSON.stringify(parsed)}`);
|
|
1095
|
+
}
|
|
1096
|
+
break;
|
|
1097
|
+
|
|
1098
|
+
case 'message':
|
|
1099
|
+
default:
|
|
1100
|
+
this.sendRelayCommand({
|
|
1101
|
+
to: parsed.to,
|
|
1102
|
+
kind: 'message',
|
|
1103
|
+
body: parsed.body,
|
|
1104
|
+
thread: parsed.thread,
|
|
1105
|
+
raw: parsed.raw,
|
|
1106
|
+
});
|
|
1107
|
+
break;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Handle continuity command from Rust relay-pty
|
|
1113
|
+
*
|
|
1114
|
+
* Maps from Rust ContinuityCommand format to TypeScript ContinuityCommand
|
|
1115
|
+
* and forwards to the ContinuityManager.
|
|
1116
|
+
*
|
|
1117
|
+
* Rust format: { type: "continuity", action: string, content: string }
|
|
1118
|
+
* TypeScript format: { type: 'save' | 'load' | 'uncertain', content?: string, item?: string }
|
|
1119
|
+
*/
|
|
1120
|
+
private async handleRustContinuityCommand(parsed: {
|
|
1121
|
+
type: string;
|
|
1122
|
+
action: string;
|
|
1123
|
+
content: string;
|
|
1124
|
+
}): Promise<void> {
|
|
1125
|
+
if (!this.continuity) {
|
|
1126
|
+
this.log('Continuity not initialized, skipping continuity command');
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Map Rust action to TypeScript ContinuityCommand type
|
|
1131
|
+
const action = parsed.action.toLowerCase();
|
|
1132
|
+
if (!['save', 'load', 'uncertain'].includes(action)) {
|
|
1133
|
+
this.logError(`Unknown continuity action: ${parsed.action}`);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Build TypeScript ContinuityCommand
|
|
1138
|
+
const command: { type: 'save' | 'load' | 'uncertain'; content?: string; item?: string } = {
|
|
1139
|
+
type: action as 'save' | 'load' | 'uncertain',
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
if (action === 'save' && parsed.content) {
|
|
1143
|
+
command.content = parsed.content;
|
|
1144
|
+
} else if (action === 'uncertain' && parsed.content) {
|
|
1145
|
+
command.item = parsed.content;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// Deduplication (same logic as base-wrapper)
|
|
1149
|
+
const cmdHash = `${command.type}:${command.content || command.item || 'no-content'}`;
|
|
1150
|
+
if (command.content && this.processedContinuityCommands.has(cmdHash)) {
|
|
1151
|
+
this.log(`Continuity command already processed: ${cmdHash}`);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
this.processedContinuityCommands.add(cmdHash);
|
|
1155
|
+
|
|
1156
|
+
// Limit dedup set size
|
|
1157
|
+
if (this.processedContinuityCommands.size > 100) {
|
|
1158
|
+
const oldest = this.processedContinuityCommands.values().next().value;
|
|
1159
|
+
if (oldest) this.processedContinuityCommands.delete(oldest);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
try {
|
|
1163
|
+
this.log(`Processing continuity command: ${command.type}`);
|
|
1164
|
+
const response = await this.continuity.handleCommand(this.config.name, command);
|
|
1165
|
+
if (response) {
|
|
1166
|
+
// Queue response for injection (e.g., for 'load' command)
|
|
1167
|
+
this.messageQueue.push({
|
|
1168
|
+
from: 'system',
|
|
1169
|
+
body: response,
|
|
1170
|
+
messageId: `continuity-${Date.now()}`,
|
|
1171
|
+
thread: 'continuity-response',
|
|
1172
|
+
});
|
|
1173
|
+
this.log(`Queued continuity response for injection`);
|
|
1174
|
+
} else {
|
|
1175
|
+
this.log(`Continuity command ${command.type} completed (no response)`);
|
|
1176
|
+
}
|
|
1177
|
+
} catch (err: any) {
|
|
1178
|
+
this.logError(`Continuity command failed: ${err.message}`);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* Handle spawn command (from Rust stderr JSON parsing)
|
|
1184
|
+
*
|
|
1185
|
+
* Note: We do NOT send the initial task message here because the spawner
|
|
1186
|
+
* now handles it after waitUntilCliReady(). Sending it here would cause
|
|
1187
|
+
* duplicate task delivery.
|
|
1188
|
+
*/
|
|
1189
|
+
private handleSpawnCommand(name: string, cli: string, task: string): void {
|
|
1190
|
+
const key = `spawn:${name}:${cli}`;
|
|
1191
|
+
if (this.processedSpawnCommands.has(key)) {
|
|
1192
|
+
this.log(`Spawn already processed: ${key}`);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
this.processedSpawnCommands.add(key);
|
|
1196
|
+
|
|
1197
|
+
// Log spawn attempts (only in debug mode to avoid TUI pollution)
|
|
1198
|
+
this.log(`SPAWN REQUEST: ${name} (${cli})`);
|
|
1199
|
+
this.log(` dashboardPort=${this.config.dashboardPort}, onSpawn=${!!this.config.onSpawn}`);
|
|
1200
|
+
|
|
1201
|
+
// Try dashboard API first, fall back to callback
|
|
1202
|
+
// The spawner will send the task after waitUntilCliReady()
|
|
1203
|
+
if (this.config.dashboardPort) {
|
|
1204
|
+
this.log(`Calling dashboard API at port ${this.config.dashboardPort}`);
|
|
1205
|
+
this.spawnViaDashboardApi(name, cli, task)
|
|
1206
|
+
.then(() => {
|
|
1207
|
+
this.log(`SPAWN SUCCESS: ${name} via dashboard API`);
|
|
1208
|
+
})
|
|
1209
|
+
.catch(err => {
|
|
1210
|
+
this.logError(`SPAWN FAILED: ${name} - ${err.message}`);
|
|
1211
|
+
if (this.config.onSpawn) {
|
|
1212
|
+
this.log(`Falling back to onSpawn callback`);
|
|
1213
|
+
Promise.resolve(this.config.onSpawn(name, cli, task))
|
|
1214
|
+
.catch(e => this.logError(`SPAWN CALLBACK FAILED: ${e.message}`));
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
} else if (this.config.onSpawn) {
|
|
1218
|
+
this.log(`Using onSpawn callback directly`);
|
|
1219
|
+
Promise.resolve(this.config.onSpawn(name, cli, task))
|
|
1220
|
+
.catch(e => this.logError(`SPAWN CALLBACK FAILED: ${e.message}`));
|
|
1221
|
+
} else {
|
|
1222
|
+
this.logError(`SPAWN FAILED: No spawn mechanism available! (dashboardPort=${this.config.dashboardPort}, onSpawn=${!!this.config.onSpawn})`);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Handle release command
|
|
1228
|
+
*/
|
|
1229
|
+
private handleReleaseCommand(name: string): void {
|
|
1230
|
+
const key = `release:${name}`;
|
|
1231
|
+
if (this.processedReleaseCommands.has(key)) {
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
this.processedReleaseCommands.add(key);
|
|
1235
|
+
|
|
1236
|
+
this.log(` Release: ${name}`);
|
|
1237
|
+
|
|
1238
|
+
// Try dashboard API first, fall back to callback
|
|
1239
|
+
if (this.config.dashboardPort) {
|
|
1240
|
+
this.releaseViaDashboardApi(name).catch(err => {
|
|
1241
|
+
this.logError(` Dashboard release failed: ${err.message}`);
|
|
1242
|
+
this.config.onRelease?.(name);
|
|
1243
|
+
});
|
|
1244
|
+
} else if (this.config.onRelease) {
|
|
1245
|
+
this.config.onRelease(name);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Spawn agent via dashboard API
|
|
1251
|
+
*/
|
|
1252
|
+
private async spawnViaDashboardApi(name: string, cli: string, task: string): Promise<void> {
|
|
1253
|
+
const url = `http://localhost:${this.config.dashboardPort}/api/spawn`;
|
|
1254
|
+
const body = {
|
|
1255
|
+
name,
|
|
1256
|
+
cli,
|
|
1257
|
+
task,
|
|
1258
|
+
spawnerName: this.config.name, // Include spawner name so task appears from correct agent
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
try {
|
|
1262
|
+
const response = await fetch(url, {
|
|
1263
|
+
method: 'POST',
|
|
1264
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1265
|
+
body: JSON.stringify(body),
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
if (!response.ok) {
|
|
1269
|
+
const errorBody = await response.text().catch(() => 'unknown');
|
|
1270
|
+
throw new Error(`HTTP ${response.status}: ${errorBody}`);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const result = await response.json().catch(() => ({})) as { success?: boolean; error?: string };
|
|
1274
|
+
if (result.success === false) {
|
|
1275
|
+
throw new Error(result.error || 'Spawn failed without specific error');
|
|
1276
|
+
}
|
|
1277
|
+
} catch (err: any) {
|
|
1278
|
+
// Enhance error with context
|
|
1279
|
+
if (err.code === 'ECONNREFUSED') {
|
|
1280
|
+
throw new Error(`Dashboard not reachable at ${url} (connection refused)`);
|
|
1281
|
+
}
|
|
1282
|
+
throw err;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* Release agent via dashboard API
|
|
1288
|
+
*/
|
|
1289
|
+
private async releaseViaDashboardApi(name: string): Promise<void> {
|
|
1290
|
+
const response = await fetch(`http://localhost:${this.config.dashboardPort}/api/spawned/${encodeURIComponent(name)}`, {
|
|
1291
|
+
method: 'DELETE',
|
|
1292
|
+
});
|
|
1293
|
+
if (!response.ok) {
|
|
1294
|
+
const body = await response.json().catch(() => ({ error: 'Unknown' })) as { error?: string };
|
|
1295
|
+
throw new Error(`HTTP ${response.status}: ${body.error || 'Unknown error'}`);
|
|
1296
|
+
}
|
|
1297
|
+
this.log(`Released ${name} via dashboard API`);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// =========================================================================
|
|
1301
|
+
// Socket communication
|
|
1302
|
+
// =========================================================================
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Check if the relay-pty process is still alive
|
|
1306
|
+
*/
|
|
1307
|
+
private isProcessAlive(): boolean {
|
|
1308
|
+
if (!this.relayPtyProcess || this.relayPtyProcess.exitCode !== null) {
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
try {
|
|
1312
|
+
// Signal 0 checks if process exists without killing it
|
|
1313
|
+
process.kill(this.relayPtyProcess.pid!, 0);
|
|
1314
|
+
return true;
|
|
1315
|
+
} catch {
|
|
1316
|
+
return false;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Connect to the relay-pty socket
|
|
1322
|
+
*/
|
|
1323
|
+
private async connectToSocket(): Promise<void> {
|
|
1324
|
+
const timeout = this.config.socketConnectTimeoutMs ?? 5000;
|
|
1325
|
+
const maxAttempts = this.config.socketReconnectAttempts ?? 3;
|
|
1326
|
+
|
|
1327
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1328
|
+
// Check if relay-pty process died before attempting connection
|
|
1329
|
+
if (!this.isProcessAlive()) {
|
|
1330
|
+
const exitInfo = this.earlyExitInfo;
|
|
1331
|
+
if (exitInfo) {
|
|
1332
|
+
const exitReason = exitInfo.signal
|
|
1333
|
+
? `signal ${exitInfo.signal}`
|
|
1334
|
+
: `code ${exitInfo.code ?? 'unknown'}`;
|
|
1335
|
+
const stderrHint = exitInfo.stderr
|
|
1336
|
+
? `\n stderr: ${exitInfo.stderr.trim().slice(0, 500)}`
|
|
1337
|
+
: '';
|
|
1338
|
+
throw new Error(`relay-pty process died early (${exitReason}).${stderrHint}`);
|
|
1339
|
+
}
|
|
1340
|
+
throw new Error('relay-pty process died before socket could be created');
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
try {
|
|
1344
|
+
await this.attemptSocketConnection(timeout);
|
|
1345
|
+
this.log(` Socket connected`);
|
|
1346
|
+
return;
|
|
1347
|
+
} catch (err: any) {
|
|
1348
|
+
this.logError(` Socket connect attempt ${attempt}/${maxAttempts} failed: ${err.message}`);
|
|
1349
|
+
if (attempt < maxAttempts) {
|
|
1350
|
+
await sleep(1000 * attempt); // Exponential backoff
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Final check for process death after all attempts
|
|
1356
|
+
if (!this.isProcessAlive() && this.earlyExitInfo) {
|
|
1357
|
+
const exitInfo = this.earlyExitInfo;
|
|
1358
|
+
const exitReason = exitInfo.signal
|
|
1359
|
+
? `signal ${exitInfo.signal}`
|
|
1360
|
+
: `code ${exitInfo.code ?? 'unknown'}`;
|
|
1361
|
+
const stderrHint = exitInfo.stderr
|
|
1362
|
+
? `\n stderr: ${exitInfo.stderr.trim().slice(0, 500)}`
|
|
1363
|
+
: '';
|
|
1364
|
+
throw new Error(`relay-pty process died during socket connection (${exitReason}).${stderrHint}`);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
throw new Error(`Failed to connect to socket after ${maxAttempts} attempts`);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Attempt a single socket connection
|
|
1372
|
+
*/
|
|
1373
|
+
private attemptSocketConnection(timeout: number): Promise<void> {
|
|
1374
|
+
return new Promise((resolve, reject) => {
|
|
1375
|
+
// Clean up any existing socket before creating new one
|
|
1376
|
+
// This prevents orphaned sockets with stale event handlers
|
|
1377
|
+
if (this.socket) {
|
|
1378
|
+
// Remove all listeners to prevent the old socket's 'close' event
|
|
1379
|
+
// from triggering another reconnect cycle
|
|
1380
|
+
this.socket.removeAllListeners();
|
|
1381
|
+
this.socket.destroy();
|
|
1382
|
+
this.socket = undefined;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
const timer = setTimeout(() => {
|
|
1386
|
+
reject(new Error('Socket connection timeout'));
|
|
1387
|
+
}, timeout);
|
|
1388
|
+
|
|
1389
|
+
this.socket = createConnection(this.socketPath, () => {
|
|
1390
|
+
clearTimeout(timer);
|
|
1391
|
+
this.socketConnected = true;
|
|
1392
|
+
resolve();
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
this.socket.on('error', (err) => {
|
|
1396
|
+
clearTimeout(timer);
|
|
1397
|
+
this.socketConnected = false;
|
|
1398
|
+
reject(err);
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
// Handle 'end' event - server closed its write side (half-close)
|
|
1402
|
+
this.socket.on('end', () => {
|
|
1403
|
+
this.socketConnected = false;
|
|
1404
|
+
this.log(` Socket received end (server closed write side)`);
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
this.socket.on('close', () => {
|
|
1408
|
+
this.socketConnected = false;
|
|
1409
|
+
this.log(` Socket closed`);
|
|
1410
|
+
// Auto-reconnect if not intentionally stopped
|
|
1411
|
+
if (this.running && !this.isGracefulStop) {
|
|
1412
|
+
this.scheduleSocketReconnect();
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
// Handle incoming data (responses)
|
|
1417
|
+
let buffer = '';
|
|
1418
|
+
this.socket.on('data', (data: Buffer) => {
|
|
1419
|
+
buffer += data.toString();
|
|
1420
|
+
|
|
1421
|
+
// Process complete lines
|
|
1422
|
+
const lines = buffer.split('\n');
|
|
1423
|
+
buffer = lines.pop() ?? ''; // Keep incomplete line in buffer
|
|
1424
|
+
|
|
1425
|
+
for (const line of lines) {
|
|
1426
|
+
if (line.trim()) {
|
|
1427
|
+
this.handleSocketResponse(line);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Disconnect from socket
|
|
1436
|
+
*/
|
|
1437
|
+
private disconnectSocket(): void {
|
|
1438
|
+
if (this.socket) {
|
|
1439
|
+
this.socket.destroy();
|
|
1440
|
+
this.socket = undefined;
|
|
1441
|
+
this.socketConnected = false;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// Reject all pending injections
|
|
1445
|
+
for (const [_id, pending] of this.pendingInjections) {
|
|
1446
|
+
clearTimeout(pending.timeout);
|
|
1447
|
+
pending.reject(new Error('Socket disconnected'));
|
|
1448
|
+
}
|
|
1449
|
+
this.pendingInjections.clear();
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/** Timer for socket reconnection */
|
|
1453
|
+
private socketReconnectTimer?: NodeJS.Timeout;
|
|
1454
|
+
/** Current reconnection attempt count */
|
|
1455
|
+
private socketReconnectAttempt = 0;
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Schedule a socket reconnection attempt with exponential backoff
|
|
1459
|
+
*/
|
|
1460
|
+
private scheduleSocketReconnect(): void {
|
|
1461
|
+
const maxAttempts = this.config.socketReconnectAttempts ?? 3;
|
|
1462
|
+
|
|
1463
|
+
// Clear any existing timer
|
|
1464
|
+
if (this.socketReconnectTimer) {
|
|
1465
|
+
clearTimeout(this.socketReconnectTimer);
|
|
1466
|
+
this.socketReconnectTimer = undefined;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
if (this.socketReconnectAttempt >= maxAttempts) {
|
|
1470
|
+
this.logError(` Socket reconnect failed after ${maxAttempts} attempts`);
|
|
1471
|
+
// Reset counter for future reconnects (processMessageQueue can trigger new cycle)
|
|
1472
|
+
this.socketReconnectAttempt = 0;
|
|
1473
|
+
// Note: socketReconnectTimer is already undefined, allowing processMessageQueue
|
|
1474
|
+
// to trigger a new reconnection cycle when new messages arrive
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
this.socketReconnectAttempt++;
|
|
1479
|
+
const delay = Math.min(1000 * Math.pow(2, this.socketReconnectAttempt - 1), 10000); // Max 10s
|
|
1480
|
+
|
|
1481
|
+
this.log(` Scheduling socket reconnect in ${delay}ms (attempt ${this.socketReconnectAttempt}/${maxAttempts})`);
|
|
1482
|
+
|
|
1483
|
+
this.socketReconnectTimer = setTimeout(async () => {
|
|
1484
|
+
// Clear timer reference now that callback is executing
|
|
1485
|
+
this.socketReconnectTimer = undefined;
|
|
1486
|
+
|
|
1487
|
+
if (!this.running || this.isGracefulStop) {
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
try {
|
|
1492
|
+
const timeout = this.config.socketConnectTimeoutMs ?? 5000;
|
|
1493
|
+
await this.attemptSocketConnection(timeout);
|
|
1494
|
+
this.log(` Socket reconnected successfully`);
|
|
1495
|
+
this.socketReconnectAttempt = 0; // Reset on success
|
|
1496
|
+
|
|
1497
|
+
// Process any queued messages that were waiting
|
|
1498
|
+
if (this.messageQueue.length > 0 && !this.isInjecting) {
|
|
1499
|
+
this.log(` Processing ${this.messageQueue.length} queued messages after reconnect`);
|
|
1500
|
+
this.processMessageQueue();
|
|
1501
|
+
}
|
|
1502
|
+
} catch (err: any) {
|
|
1503
|
+
this.logError(` Socket reconnect attempt ${this.socketReconnectAttempt} failed: ${err.message}`);
|
|
1504
|
+
// Schedule another attempt
|
|
1505
|
+
this.scheduleSocketReconnect();
|
|
1506
|
+
}
|
|
1507
|
+
}, delay);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* Send a request to the socket and optionally wait for response
|
|
1512
|
+
*/
|
|
1513
|
+
private sendSocketRequest(request: RelayPtyRequest): Promise<void> {
|
|
1514
|
+
return new Promise((resolve, reject) => {
|
|
1515
|
+
if (!this.socket || !this.socketConnected) {
|
|
1516
|
+
reject(new Error('Socket not connected'));
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
const json = JSON.stringify(request) + '\n';
|
|
1521
|
+
this.socket.write(json, (err) => {
|
|
1522
|
+
if (err) {
|
|
1523
|
+
reject(err);
|
|
1524
|
+
} else {
|
|
1525
|
+
resolve();
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* Handle a response from the socket
|
|
1533
|
+
*/
|
|
1534
|
+
private handleSocketResponse(line: string): void {
|
|
1535
|
+
try {
|
|
1536
|
+
const response = JSON.parse(line) as RelayPtyResponse;
|
|
1537
|
+
|
|
1538
|
+
switch (response.type) {
|
|
1539
|
+
case 'inject_result':
|
|
1540
|
+
// handleInjectResult is async (does verification), but we don't await here
|
|
1541
|
+
// Errors are handled internally by the method
|
|
1542
|
+
this.handleInjectResult(response).catch((err: Error) => {
|
|
1543
|
+
this.logError(` Error handling inject result: ${err.message}`);
|
|
1544
|
+
});
|
|
1545
|
+
break;
|
|
1546
|
+
|
|
1547
|
+
case 'status':
|
|
1548
|
+
// Status responses are typically requested explicitly
|
|
1549
|
+
this.log(` Status: idle=${response.agent_idle} queue=${response.queue_length}`);
|
|
1550
|
+
break;
|
|
1551
|
+
|
|
1552
|
+
case 'backpressure':
|
|
1553
|
+
this.handleBackpressure(response);
|
|
1554
|
+
break;
|
|
1555
|
+
|
|
1556
|
+
case 'error':
|
|
1557
|
+
this.logError(` Socket error: ${response.message}`);
|
|
1558
|
+
break;
|
|
1559
|
+
|
|
1560
|
+
case 'shutdown_ack':
|
|
1561
|
+
this.log(` Shutdown acknowledged`);
|
|
1562
|
+
break;
|
|
1563
|
+
|
|
1564
|
+
case 'send_enter_result':
|
|
1565
|
+
// SendEnter is no longer used - trust Rust delivery confirmation
|
|
1566
|
+
this.log(` Received send_enter_result (deprecated)`);
|
|
1567
|
+
break;
|
|
1568
|
+
}
|
|
1569
|
+
} catch (err: any) {
|
|
1570
|
+
this.logError(` Failed to parse socket response: ${err.message}`);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/**
|
|
1575
|
+
* Handle injection result response
|
|
1576
|
+
* After Rust reports 'delivered', verifies the message appeared in output.
|
|
1577
|
+
* If verification fails, retries up to MAX_RETRIES times.
|
|
1578
|
+
*/
|
|
1579
|
+
private async handleInjectResult(response: InjectResultResponse): Promise<void> {
|
|
1580
|
+
this.log(` handleInjectResult: id=${response.id.substring(0, 8)} status=${response.status}`);
|
|
1581
|
+
|
|
1582
|
+
const pending = this.pendingInjections.get(response.id);
|
|
1583
|
+
if (!pending) {
|
|
1584
|
+
// Response for unknown message - might be from a previous session
|
|
1585
|
+
this.log(` No pending injection found for ${response.id.substring(0, 8)}`);
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (response.status === 'delivered') {
|
|
1590
|
+
// Rust says it sent the message + Enter key
|
|
1591
|
+
// Trust Rust's delivery confirmation - relay-pty writes directly to PTY which is very reliable.
|
|
1592
|
+
//
|
|
1593
|
+
// IMPORTANT: We don't verify by looking for the message in output because:
|
|
1594
|
+
// 1. TUI CLIs (Claude, Codex, Gemini) don't echo input like traditional terminals
|
|
1595
|
+
// 2. The injected text appears as INPUT to the PTY, not OUTPUT
|
|
1596
|
+
// 3. Output-based verification always fails for TUIs, causing unnecessary retries
|
|
1597
|
+
//
|
|
1598
|
+
// This is different from tmux-wrapper where we inject via tmux send-keys
|
|
1599
|
+
// and can observe the echoed input in the pane output.
|
|
1600
|
+
this.log(` Message ${pending.shortId} delivered by Rust ✓`);
|
|
1601
|
+
|
|
1602
|
+
clearTimeout(pending.timeout);
|
|
1603
|
+
this.pendingInjections.delete(response.id);
|
|
1604
|
+
if (pending.retryCount === 0) {
|
|
1605
|
+
this.injectionMetrics.successFirstTry++;
|
|
1606
|
+
} else {
|
|
1607
|
+
this.injectionMetrics.successWithRetry++;
|
|
1608
|
+
}
|
|
1609
|
+
this.injectionMetrics.total++;
|
|
1610
|
+
pending.resolve(true);
|
|
1611
|
+
} else if (response.status === 'failed') {
|
|
1612
|
+
clearTimeout(pending.timeout);
|
|
1613
|
+
this.pendingInjections.delete(response.id);
|
|
1614
|
+
this.injectionMetrics.failed++;
|
|
1615
|
+
this.injectionMetrics.total++;
|
|
1616
|
+
pending.resolve(false);
|
|
1617
|
+
this.logError(` Message ${pending.shortId} failed: ${response.error}`);
|
|
1618
|
+
this.emit('injection-failed', {
|
|
1619
|
+
messageId: response.id,
|
|
1620
|
+
from: pending.from,
|
|
1621
|
+
error: response.error ?? 'Unknown error',
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
// queued/injecting are intermediate states - wait for final status
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* Handle backpressure notification
|
|
1629
|
+
*/
|
|
1630
|
+
private handleBackpressure(response: BackpressureResponse): void {
|
|
1631
|
+
const wasActive = this.backpressureActive;
|
|
1632
|
+
this.backpressureActive = !response.accept;
|
|
1633
|
+
|
|
1634
|
+
if (this.backpressureActive !== wasActive) {
|
|
1635
|
+
this.log(` Backpressure: ${this.backpressureActive ? 'ACTIVE' : 'cleared'} (queue=${response.queue_length})`);
|
|
1636
|
+
this.emit('backpressure', { queueLength: response.queue_length, accept: response.accept });
|
|
1637
|
+
|
|
1638
|
+
// Resume processing if backpressure cleared
|
|
1639
|
+
if (!this.backpressureActive) {
|
|
1640
|
+
this.processMessageQueue();
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// =========================================================================
|
|
1646
|
+
// Message handling
|
|
1647
|
+
// =========================================================================
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* Inject a message into the agent via socket
|
|
1651
|
+
*/
|
|
1652
|
+
private async injectMessage(msg: QueuedMessage, retryCount = 0): Promise<boolean> {
|
|
1653
|
+
const shortId = msg.messageId.substring(0, 8);
|
|
1654
|
+
this.log(` === INJECT START: ${shortId} from ${msg.from} (attempt ${retryCount + 1}) ===`);
|
|
1655
|
+
|
|
1656
|
+
if (!this.socket || !this.socketConnected) {
|
|
1657
|
+
this.logError(` Cannot inject - socket not connected`);
|
|
1658
|
+
return false;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// Build injection content
|
|
1662
|
+
const content = buildInjectionString(msg);
|
|
1663
|
+
this.log(` Injection content (${content.length} bytes): ${content.substring(0, 100)}...`);
|
|
1664
|
+
|
|
1665
|
+
// Create request
|
|
1666
|
+
const request: InjectRequest = {
|
|
1667
|
+
type: 'inject',
|
|
1668
|
+
id: msg.messageId,
|
|
1669
|
+
from: msg.from,
|
|
1670
|
+
body: content,
|
|
1671
|
+
priority: msg.importance ?? 0,
|
|
1672
|
+
};
|
|
1673
|
+
|
|
1674
|
+
this.log(` Sending inject request to socket...`);
|
|
1675
|
+
|
|
1676
|
+
// Create promise for result
|
|
1677
|
+
return new Promise<boolean>((resolve, reject) => {
|
|
1678
|
+
const timeout = setTimeout(() => {
|
|
1679
|
+
this.logError(` Inject timeout for ${shortId} after 30s`);
|
|
1680
|
+
this.pendingInjections.delete(msg.messageId);
|
|
1681
|
+
resolve(false); // Timeout = failure
|
|
1682
|
+
}, 30000); // 30 second timeout for injection
|
|
1683
|
+
|
|
1684
|
+
this.pendingInjections.set(msg.messageId, {
|
|
1685
|
+
resolve,
|
|
1686
|
+
reject,
|
|
1687
|
+
timeout,
|
|
1688
|
+
from: msg.from,
|
|
1689
|
+
shortId,
|
|
1690
|
+
retryCount,
|
|
1691
|
+
originalBody: content,
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
// Send request
|
|
1695
|
+
this.sendSocketRequest(request)
|
|
1696
|
+
.then(() => {
|
|
1697
|
+
this.log(` Socket request sent for ${shortId}`);
|
|
1698
|
+
})
|
|
1699
|
+
.catch((err) => {
|
|
1700
|
+
this.logError(` Socket request failed for ${shortId}: ${err.message}`);
|
|
1701
|
+
clearTimeout(timeout);
|
|
1702
|
+
this.pendingInjections.delete(msg.messageId);
|
|
1703
|
+
resolve(false);
|
|
1704
|
+
});
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* Process queued messages
|
|
1710
|
+
*/
|
|
1711
|
+
private async processMessageQueue(): Promise<void> {
|
|
1712
|
+
// Debug: Log blocking conditions when queue has messages
|
|
1713
|
+
if (this.messageQueue.length > 0) {
|
|
1714
|
+
if (!this.readyForMessages) {
|
|
1715
|
+
this.log(` Queue blocked: readyForMessages=false (queue=${this.messageQueue.length})`);
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
if (this.backpressureActive) {
|
|
1719
|
+
this.log(` Queue blocked: backpressure active (queue=${this.messageQueue.length})`);
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
if (this.isInjecting) {
|
|
1723
|
+
// Already injecting - the finally block will process next message
|
|
1724
|
+
// But add a safety timeout in case injection gets stuck
|
|
1725
|
+
const elapsed = this.injectionStartTime > 0 ? Date.now() - this.injectionStartTime : 0;
|
|
1726
|
+
if (elapsed > 35000) {
|
|
1727
|
+
this.logError(` Injection stuck for ${elapsed}ms, forcing reset`);
|
|
1728
|
+
this.isInjecting = false;
|
|
1729
|
+
this.injectionStartTime = 0;
|
|
1730
|
+
}
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
if (this.messageQueue.length === 0) {
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// Proactively reconnect socket if disconnected and we have messages to send
|
|
1740
|
+
if (!this.socketConnected && !this.socketReconnectTimer) {
|
|
1741
|
+
this.log(` Socket disconnected, triggering reconnect before processing queue`);
|
|
1742
|
+
this.scheduleSocketReconnect();
|
|
1743
|
+
return; // Wait for reconnection to complete
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
if (!this.socketConnected) {
|
|
1747
|
+
// Reconnection in progress, wait for it
|
|
1748
|
+
this.log(` Queue waiting: socket reconnecting (queue=${this.messageQueue.length})`);
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// Check if agent is in editor mode - delay injection if so
|
|
1753
|
+
const idleResult = this.idleDetector.checkIdle();
|
|
1754
|
+
if (idleResult.inEditorMode) {
|
|
1755
|
+
this.log(` Agent in editor mode, delaying injection (queue: ${this.messageQueue.length})`);
|
|
1756
|
+
// Check again in 2 seconds
|
|
1757
|
+
setTimeout(() => this.processMessageQueue(), 2000);
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
this.isInjecting = true;
|
|
1762
|
+
this.injectionStartTime = Date.now();
|
|
1763
|
+
|
|
1764
|
+
const msg = this.messageQueue.shift()!;
|
|
1765
|
+
const bodyPreview = msg.body.substring(0, 50).replace(/\n/g, '\\n');
|
|
1766
|
+
this.log(` Processing message from ${msg.from}: "${bodyPreview}..." (remaining=${this.messageQueue.length})`);
|
|
1767
|
+
|
|
1768
|
+
try {
|
|
1769
|
+
const success = await this.injectMessage(msg);
|
|
1770
|
+
|
|
1771
|
+
// Metrics are now tracked in handleInjectResult which knows about retries
|
|
1772
|
+
if (!success) {
|
|
1773
|
+
// Record failure for adaptive throttling
|
|
1774
|
+
this.throttle.recordFailure();
|
|
1775
|
+
this.logError(` Injection failed for message ${msg.messageId.substring(0, 8)}`);
|
|
1776
|
+
this.config.onInjectionFailed?.(msg.messageId, 'Injection failed');
|
|
1777
|
+
this.sendSyncAck(msg.messageId, msg.sync, 'ERROR', { error: 'injection_failed' });
|
|
1778
|
+
} else {
|
|
1779
|
+
// Record success for adaptive throttling
|
|
1780
|
+
this.throttle.recordSuccess();
|
|
1781
|
+
this.sendSyncAck(msg.messageId, msg.sync, 'OK');
|
|
1782
|
+
}
|
|
1783
|
+
} catch (err: any) {
|
|
1784
|
+
this.logError(` Injection error: ${err.message}`);
|
|
1785
|
+
// Track metrics for exceptions (not handled by handleInjectResult)
|
|
1786
|
+
this.injectionMetrics.failed++;
|
|
1787
|
+
this.injectionMetrics.total++;
|
|
1788
|
+
// Record failure for adaptive throttling
|
|
1789
|
+
this.throttle.recordFailure();
|
|
1790
|
+
this.sendSyncAck(msg.messageId, msg.sync, 'ERROR', { error: err.message });
|
|
1791
|
+
} finally {
|
|
1792
|
+
this.isInjecting = false;
|
|
1793
|
+
this.injectionStartTime = 0;
|
|
1794
|
+
|
|
1795
|
+
// Process next message after adaptive delay (faster when healthy, slower under stress)
|
|
1796
|
+
if (this.messageQueue.length > 0 && !this.backpressureActive) {
|
|
1797
|
+
const delay = this.throttle.getDelay();
|
|
1798
|
+
setTimeout(() => this.processMessageQueue(), delay);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
/**
|
|
1804
|
+
* Override handleIncomingMessage to trigger queue processing
|
|
1805
|
+
*/
|
|
1806
|
+
protected override handleIncomingMessage(
|
|
1807
|
+
from: string,
|
|
1808
|
+
payload: SendPayload,
|
|
1809
|
+
messageId: string,
|
|
1810
|
+
meta?: SendMeta,
|
|
1811
|
+
originalTo?: string
|
|
1812
|
+
): void {
|
|
1813
|
+
this.log(` === MESSAGE RECEIVED: ${messageId.substring(0, 8)} from ${from} ===`);
|
|
1814
|
+
this.log(` Body preview: ${payload.body?.substring(0, 100) ?? '(no body)'}...`);
|
|
1815
|
+
super.handleIncomingMessage(from, payload, messageId, meta, originalTo);
|
|
1816
|
+
this.log(` Queue length after add: ${this.messageQueue.length}`);
|
|
1817
|
+
this.processMessageQueue();
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
/**
|
|
1821
|
+
* Override handleIncomingChannelMessage to trigger queue processing.
|
|
1822
|
+
* Without this override, channel messages would be queued but processMessageQueue()
|
|
1823
|
+
* would never be called, causing messages to get stuck until the queue monitor runs.
|
|
1824
|
+
*/
|
|
1825
|
+
protected override handleIncomingChannelMessage(
|
|
1826
|
+
from: string,
|
|
1827
|
+
channel: string,
|
|
1828
|
+
body: string,
|
|
1829
|
+
envelope: Envelope<ChannelMessagePayload>
|
|
1830
|
+
): void {
|
|
1831
|
+
this.log(` === CHANNEL MESSAGE RECEIVED: ${envelope.id.substring(0, 8)} from ${from} on ${channel} ===`);
|
|
1832
|
+
this.log(` Body preview: ${body?.substring(0, 100) ?? '(no body)'}...`);
|
|
1833
|
+
super.handleIncomingChannelMessage(from, channel, body, envelope);
|
|
1834
|
+
this.log(` Queue length after add: ${this.messageQueue.length}`);
|
|
1835
|
+
this.processMessageQueue();
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// =========================================================================
|
|
1839
|
+
// Queue monitor - Detect and process stuck messages
|
|
1840
|
+
// =========================================================================
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Start the queue monitor to periodically check for stuck messages.
|
|
1844
|
+
* This ensures messages don't get orphaned in the queue when the agent is idle.
|
|
1845
|
+
*/
|
|
1846
|
+
private startQueueMonitor(): void {
|
|
1847
|
+
if (this.queueMonitorTimer) {
|
|
1848
|
+
return; // Already started
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
this.log(` Starting queue monitor (interval: ${this.QUEUE_MONITOR_INTERVAL_MS}ms)`);
|
|
1852
|
+
|
|
1853
|
+
this.queueMonitorTimer = setInterval(() => {
|
|
1854
|
+
this.checkForStuckQueue();
|
|
1855
|
+
}, this.QUEUE_MONITOR_INTERVAL_MS);
|
|
1856
|
+
|
|
1857
|
+
// Don't keep process alive just for queue monitoring
|
|
1858
|
+
this.queueMonitorTimer.unref?.();
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
/**
|
|
1862
|
+
* Stop the queue monitor.
|
|
1863
|
+
*/
|
|
1864
|
+
private stopQueueMonitor(): void {
|
|
1865
|
+
if (this.queueMonitorTimer) {
|
|
1866
|
+
clearInterval(this.queueMonitorTimer);
|
|
1867
|
+
this.queueMonitorTimer = undefined;
|
|
1868
|
+
this.log(` Queue monitor stopped`);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// =========================================================================
|
|
1873
|
+
// Protocol monitoring (detect agent mistakes like empty AGENT_RELAY_NAME)
|
|
1874
|
+
// =========================================================================
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* Start watching for protocol issues in the outbox directory.
|
|
1878
|
+
* Detects common mistakes like:
|
|
1879
|
+
* - Empty AGENT_RELAY_NAME causing files at outbox//
|
|
1880
|
+
* - Files created directly in outbox/ instead of agent subdirectory
|
|
1881
|
+
*/
|
|
1882
|
+
private startProtocolMonitor(): void {
|
|
1883
|
+
// Get the outbox parent directory (one level up from agent's outbox)
|
|
1884
|
+
const parentDir = dirname(this._canonicalOutboxPath);
|
|
1885
|
+
|
|
1886
|
+
// Ensure parent directory exists
|
|
1887
|
+
try {
|
|
1888
|
+
if (!existsSync(parentDir)) {
|
|
1889
|
+
mkdirSync(parentDir, { recursive: true });
|
|
1890
|
+
}
|
|
1891
|
+
} catch {
|
|
1892
|
+
// Ignore - directory may already exist
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
try {
|
|
1896
|
+
this.protocolWatcher = watch(parentDir, (eventType, filename) => {
|
|
1897
|
+
if (eventType === 'rename' && filename) {
|
|
1898
|
+
// Check for files directly in parent (not in agent subdirectory)
|
|
1899
|
+
// This happens when $AGENT_RELAY_NAME is empty
|
|
1900
|
+
const fullPath = join(parentDir, filename);
|
|
1901
|
+
try {
|
|
1902
|
+
// If it's a file (not directory) directly in the parent, that's an issue
|
|
1903
|
+
if (existsSync(fullPath) && !lstatSync(fullPath).isDirectory()) {
|
|
1904
|
+
this.handleProtocolIssue('file_in_root', filename);
|
|
1905
|
+
}
|
|
1906
|
+
// Check for empty-named directory (double slash symptom)
|
|
1907
|
+
if (filename === '' || filename.startsWith('/')) {
|
|
1908
|
+
this.handleProtocolIssue('empty_agent_name', filename);
|
|
1909
|
+
}
|
|
1910
|
+
} catch {
|
|
1911
|
+
// Ignore stat errors
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
});
|
|
1915
|
+
|
|
1916
|
+
// Don't keep process alive just for protocol monitoring
|
|
1917
|
+
this.protocolWatcher.unref?.();
|
|
1918
|
+
this.log(` Protocol monitor started on ${parentDir}`);
|
|
1919
|
+
} catch (err: any) {
|
|
1920
|
+
// Don't fail start() if protocol monitoring fails
|
|
1921
|
+
this.logError(` Failed to start protocol monitor: ${err.message}`);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// Also do an initial scan for existing issues
|
|
1925
|
+
this.scanForProtocolIssues();
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
/**
|
|
1929
|
+
* Stop the protocol monitor.
|
|
1930
|
+
*/
|
|
1931
|
+
private stopProtocolMonitor(): void {
|
|
1932
|
+
if (this.protocolWatcher) {
|
|
1933
|
+
this.protocolWatcher.close();
|
|
1934
|
+
this.protocolWatcher = undefined;
|
|
1935
|
+
this.log(` Protocol monitor stopped`);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
/**
|
|
1940
|
+
* Scan for existing protocol issues (called once at startup).
|
|
1941
|
+
*/
|
|
1942
|
+
private scanForProtocolIssues(): void {
|
|
1943
|
+
const parentDir = dirname(this._canonicalOutboxPath);
|
|
1944
|
+
try {
|
|
1945
|
+
if (!existsSync(parentDir)) return;
|
|
1946
|
+
|
|
1947
|
+
const entries = readdirSync(parentDir);
|
|
1948
|
+
for (const entry of entries) {
|
|
1949
|
+
const fullPath = join(parentDir, entry);
|
|
1950
|
+
try {
|
|
1951
|
+
// Check for files directly in parent (should only be directories)
|
|
1952
|
+
if (!lstatSync(fullPath).isDirectory()) {
|
|
1953
|
+
this.handleProtocolIssue('file_in_root', entry);
|
|
1954
|
+
break; // Only report once
|
|
1955
|
+
}
|
|
1956
|
+
} catch {
|
|
1957
|
+
// Ignore stat errors
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
} catch {
|
|
1961
|
+
// Ignore scan errors
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
/**
|
|
1966
|
+
* Handle a detected protocol issue by injecting a helpful reminder.
|
|
1967
|
+
*/
|
|
1968
|
+
private handleProtocolIssue(issue: 'empty_agent_name' | 'file_in_root', filename: string): void {
|
|
1969
|
+
const now = Date.now();
|
|
1970
|
+
|
|
1971
|
+
// Respect cooldown to avoid spamming
|
|
1972
|
+
if (now - this.protocolReminderCooldown < this.PROTOCOL_REMINDER_COOLDOWN_MS) {
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
this.protocolReminderCooldown = now;
|
|
1976
|
+
|
|
1977
|
+
this.log(` Protocol issue detected: ${issue} (${filename})`);
|
|
1978
|
+
|
|
1979
|
+
const reminders: Record<string, string> = {
|
|
1980
|
+
empty_agent_name: `⚠️ **Protocol Issue Detected**
|
|
1981
|
+
|
|
1982
|
+
Your \`$AGENT_RELAY_NAME\` environment variable appears to be empty or unset.
|
|
1983
|
+
Your agent name is: **${this.config.name}**
|
|
1984
|
+
|
|
1985
|
+
Correct outbox path: \`$AGENT_RELAY_OUTBOX\`
|
|
1986
|
+
|
|
1987
|
+
When writing relay files, use:
|
|
1988
|
+
\`\`\`bash
|
|
1989
|
+
cat > $AGENT_RELAY_OUTBOX/msg << 'EOF'
|
|
1990
|
+
TO: TargetAgent
|
|
1991
|
+
|
|
1992
|
+
Your message here
|
|
1993
|
+
EOF
|
|
1994
|
+
\`\`\`
|
|
1995
|
+
Then output: \`->relay-file:msg\``,
|
|
1996
|
+
|
|
1997
|
+
file_in_root: `⚠️ **Protocol Issue Detected**
|
|
1998
|
+
|
|
1999
|
+
Found file "${filename}" directly in the outbox root instead of using the proper path.
|
|
2000
|
+
Your agent name is: **${this.config.name}**
|
|
2001
|
+
|
|
2002
|
+
The \`$AGENT_RELAY_OUTBOX\` path already points to your agent's directory.
|
|
2003
|
+
Write files directly inside it:
|
|
2004
|
+
|
|
2005
|
+
\`\`\`bash
|
|
2006
|
+
cat > $AGENT_RELAY_OUTBOX/msg << 'EOF'
|
|
2007
|
+
TO: TargetAgent
|
|
2008
|
+
|
|
2009
|
+
Your message here
|
|
2010
|
+
EOF
|
|
2011
|
+
\`\`\`
|
|
2012
|
+
Then output: \`->relay-file:msg\``,
|
|
2013
|
+
};
|
|
2014
|
+
|
|
2015
|
+
const reminder = reminders[issue];
|
|
2016
|
+
if (reminder) {
|
|
2017
|
+
this.injectProtocolReminder(reminder);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
/**
|
|
2022
|
+
* Inject a protocol reminder message to the agent.
|
|
2023
|
+
*/
|
|
2024
|
+
private injectProtocolReminder(message: string): void {
|
|
2025
|
+
const queuedMsg: QueuedMessage = {
|
|
2026
|
+
from: 'system',
|
|
2027
|
+
body: message,
|
|
2028
|
+
messageId: `protocol-reminder-${Date.now()}`,
|
|
2029
|
+
importance: 2, // Higher priority
|
|
2030
|
+
};
|
|
2031
|
+
|
|
2032
|
+
this.messageQueue.unshift(queuedMsg); // Add to front of queue
|
|
2033
|
+
this.log(` Queued protocol reminder (queue size: ${this.messageQueue.length})`);
|
|
2034
|
+
|
|
2035
|
+
// Trigger processing if not already in progress
|
|
2036
|
+
if (!this.isInjecting && this.readyForMessages) {
|
|
2037
|
+
this.processMessageQueue();
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// =========================================================================
|
|
2042
|
+
// Periodic protocol reminders (for long sessions where agents forget protocol)
|
|
2043
|
+
// =========================================================================
|
|
2044
|
+
|
|
2045
|
+
/**
|
|
2046
|
+
* Start sending periodic protocol reminders.
|
|
2047
|
+
* Agents in long sessions sometimes forget the relay protocol - these
|
|
2048
|
+
* reminders help them stay on track without user intervention.
|
|
2049
|
+
*/
|
|
2050
|
+
private startPeriodicReminder(): void {
|
|
2051
|
+
this.sessionStartTime = Date.now();
|
|
2052
|
+
|
|
2053
|
+
this.periodicReminderTimer = setInterval(() => {
|
|
2054
|
+
this.sendPeriodicProtocolReminder();
|
|
2055
|
+
}, this.PERIODIC_REMINDER_INTERVAL_MS);
|
|
2056
|
+
|
|
2057
|
+
// Don't keep process alive just for reminders
|
|
2058
|
+
this.periodicReminderTimer.unref?.();
|
|
2059
|
+
|
|
2060
|
+
const intervalMinutes = Math.round(this.PERIODIC_REMINDER_INTERVAL_MS / 60000);
|
|
2061
|
+
this.log(` Periodic protocol reminder started (interval: ${intervalMinutes} minutes)`);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
/**
|
|
2065
|
+
* Stop periodic protocol reminders.
|
|
2066
|
+
*/
|
|
2067
|
+
private stopPeriodicReminder(): void {
|
|
2068
|
+
if (this.periodicReminderTimer) {
|
|
2069
|
+
clearInterval(this.periodicReminderTimer);
|
|
2070
|
+
this.periodicReminderTimer = undefined;
|
|
2071
|
+
this.log(` Periodic protocol reminder stopped`);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
/**
|
|
2076
|
+
* Send a periodic protocol reminder to the agent.
|
|
2077
|
+
* This reminds agents about proper relay communication format after long sessions.
|
|
2078
|
+
*/
|
|
2079
|
+
private sendPeriodicProtocolReminder(): void {
|
|
2080
|
+
// Don't send if not ready
|
|
2081
|
+
if (!this.running || !this.readyForMessages) {
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
const sessionDurationMinutes = Math.round((Date.now() - this.sessionStartTime) / 60000);
|
|
2086
|
+
|
|
2087
|
+
const reminder = `📋 **Protocol Reminder** (Session: ${sessionDurationMinutes} minutes)
|
|
2088
|
+
|
|
2089
|
+
You are **${this.config.name}** in a multi-agent relay system. Here's how to communicate:
|
|
2090
|
+
|
|
2091
|
+
**Sending Messages:**
|
|
2092
|
+
\`\`\`bash
|
|
2093
|
+
cat > $AGENT_RELAY_OUTBOX/msg << 'EOF'
|
|
2094
|
+
TO: *
|
|
2095
|
+
|
|
2096
|
+
Your message here
|
|
2097
|
+
EOF
|
|
2098
|
+
\`\`\`
|
|
2099
|
+
Then output: \`->relay-file:msg\`
|
|
2100
|
+
|
|
2101
|
+
Use \`TO: *\` to broadcast to all agents, or \`TO: AgentName\` for a specific agent.
|
|
2102
|
+
|
|
2103
|
+
**Spawning Agents:**
|
|
2104
|
+
\`\`\`bash
|
|
2105
|
+
cat > $AGENT_RELAY_OUTBOX/spawn << 'EOF'
|
|
2106
|
+
KIND: spawn
|
|
2107
|
+
NAME: WorkerName
|
|
2108
|
+
CLI: claude
|
|
2109
|
+
|
|
2110
|
+
Task description here
|
|
2111
|
+
EOF
|
|
2112
|
+
\`\`\`
|
|
2113
|
+
Then output: \`->relay-file:spawn\`
|
|
2114
|
+
|
|
2115
|
+
**Message Format:**
|
|
2116
|
+
- \`TO: AgentName\` for direct messages
|
|
2117
|
+
- \`TO: *\` to broadcast to all agents
|
|
2118
|
+
- \`TO: #channel\` for channel messages
|
|
2119
|
+
|
|
2120
|
+
📖 See **AGENTS.md** in the project root for full protocol documentation.`;
|
|
2121
|
+
|
|
2122
|
+
this.log(` Sending periodic protocol reminder (session: ${sessionDurationMinutes}m)`);
|
|
2123
|
+
this.injectProtocolReminder(reminder);
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
/**
|
|
2127
|
+
* Check for messages stuck in the queue and process them if the agent is idle.
|
|
2128
|
+
*
|
|
2129
|
+
* This handles cases where:
|
|
2130
|
+
* 1. Messages arrived while the agent was busy and the retry mechanism failed
|
|
2131
|
+
* 2. Socket disconnection/reconnection left messages orphaned
|
|
2132
|
+
* 3. Injection timeouts occurred without proper queue resumption
|
|
2133
|
+
*/
|
|
2134
|
+
private checkForStuckQueue(): void {
|
|
2135
|
+
// Skip if not ready for messages
|
|
2136
|
+
if (!this.readyForMessages || !this.running) {
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// Skip if queue is empty
|
|
2141
|
+
if (this.messageQueue.length === 0) {
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// Check if currently injecting
|
|
2146
|
+
if (this.isInjecting) {
|
|
2147
|
+
// Check if injection has been stuck for too long
|
|
2148
|
+
const stuckDuration = Date.now() - this.injectionStartTime;
|
|
2149
|
+
if (stuckDuration > this.MAX_INJECTION_STUCK_MS) {
|
|
2150
|
+
this.logError(` ⚠️ Injection stuck for ${Math.round(stuckDuration / 1000)}s - force resetting`);
|
|
2151
|
+
this.isInjecting = false;
|
|
2152
|
+
this.injectionStartTime = 0;
|
|
2153
|
+
// Clear any pending injections that might be stuck
|
|
2154
|
+
for (const [id, pending] of this.pendingInjections) {
|
|
2155
|
+
clearTimeout(pending.timeout);
|
|
2156
|
+
this.logError(` Clearing stuck pending injection: ${id.substring(0, 8)}`);
|
|
2157
|
+
}
|
|
2158
|
+
this.pendingInjections.clear();
|
|
2159
|
+
// Continue to process the queue below
|
|
2160
|
+
} else {
|
|
2161
|
+
return; // Still within normal injection time
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
// Skip if backpressure is active
|
|
2166
|
+
if (this.backpressureActive) {
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// Check if the agent is idle (high confidence)
|
|
2171
|
+
const idleResult = this.idleDetector.checkIdle({ minSilenceMs: 2000 });
|
|
2172
|
+
if (!idleResult.isIdle) {
|
|
2173
|
+
// Agent is still working, let it finish
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// We have messages in the queue, agent is idle, not currently injecting
|
|
2178
|
+
// This is a stuck queue situation - trigger processing
|
|
2179
|
+
const senders = [...new Set(this.messageQueue.map(m => m.from))];
|
|
2180
|
+
this.log(` ⚠️ Queue monitor: Found ${this.messageQueue.length} stuck message(s) from [${senders.join(', ')}]`);
|
|
2181
|
+
this.log(` ⚠️ Agent is idle (confidence: ${(idleResult.confidence * 100).toFixed(0)}%), triggering queue processing`);
|
|
2182
|
+
|
|
2183
|
+
// Process the queue
|
|
2184
|
+
this.processMessageQueue();
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
// =========================================================================
|
|
2188
|
+
// Output parsing
|
|
2189
|
+
// =========================================================================
|
|
2190
|
+
|
|
2191
|
+
/**
|
|
2192
|
+
* Parse relay commands from output
|
|
2193
|
+
*/
|
|
2194
|
+
private parseRelayCommands(): void {
|
|
2195
|
+
const cleanContent = stripAnsi(this.rawBuffer);
|
|
2196
|
+
|
|
2197
|
+
if (cleanContent.length <= this.lastParsedLength) {
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// Parse new content with lookback for fenced messages
|
|
2202
|
+
const lookbackStart = Math.max(0, this.lastParsedLength - 500);
|
|
2203
|
+
const contentToParse = cleanContent.substring(lookbackStart);
|
|
2204
|
+
|
|
2205
|
+
// Parse fenced messages
|
|
2206
|
+
this.parseFencedMessages(contentToParse);
|
|
2207
|
+
|
|
2208
|
+
// Parse single-line messages
|
|
2209
|
+
this.parseSingleLineMessages(contentToParse);
|
|
2210
|
+
|
|
2211
|
+
// Parse spawn/release commands
|
|
2212
|
+
this.parseSpawnReleaseCommands(contentToParse);
|
|
2213
|
+
|
|
2214
|
+
this.lastParsedLength = cleanContent.length;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
/**
|
|
2218
|
+
* Parse fenced multi-line messages
|
|
2219
|
+
*/
|
|
2220
|
+
private parseFencedMessages(content: string): void {
|
|
2221
|
+
const escapedPrefix = this.relayPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2222
|
+
const fencePattern = new RegExp(
|
|
2223
|
+
`${escapedPrefix}(\\S+)(?:\\s+\\[thread:([\\w-]+)\\])?\\s*<<<([\\s\\S]*?)>>>`,
|
|
2224
|
+
'g'
|
|
2225
|
+
);
|
|
2226
|
+
|
|
2227
|
+
let match;
|
|
2228
|
+
while ((match = fencePattern.exec(content)) !== null) {
|
|
2229
|
+
const target = match[1];
|
|
2230
|
+
const thread = match[2];
|
|
2231
|
+
const body = match[3].trim();
|
|
2232
|
+
|
|
2233
|
+
if (!body || target === 'spawn' || target === 'release') {
|
|
2234
|
+
continue;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
this.sendRelayCommand({
|
|
2238
|
+
to: target,
|
|
2239
|
+
kind: 'message',
|
|
2240
|
+
body,
|
|
2241
|
+
thread,
|
|
2242
|
+
raw: match[0],
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
/**
|
|
2248
|
+
* Parse single-line messages
|
|
2249
|
+
*/
|
|
2250
|
+
private parseSingleLineMessages(content: string): void {
|
|
2251
|
+
const lines = content.split('\n');
|
|
2252
|
+
const escapedPrefix = this.relayPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2253
|
+
const pattern = new RegExp(`${escapedPrefix}(\\S+)(?:\\s+\\[thread:([\\w-]+)\\])?\\s+(.+)$`);
|
|
2254
|
+
|
|
2255
|
+
for (const line of lines) {
|
|
2256
|
+
// Skip fenced messages
|
|
2257
|
+
if (line.includes('<<<') || line.includes('>>>')) {
|
|
2258
|
+
continue;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
const match = line.match(pattern);
|
|
2262
|
+
if (!match) {
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
const target = match[1];
|
|
2267
|
+
const thread = match[2];
|
|
2268
|
+
const body = match[3].trim();
|
|
2269
|
+
|
|
2270
|
+
if (!body || target === 'spawn' || target === 'release') {
|
|
2271
|
+
continue;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
this.sendRelayCommand({
|
|
2275
|
+
to: target,
|
|
2276
|
+
kind: 'message',
|
|
2277
|
+
body,
|
|
2278
|
+
thread,
|
|
2279
|
+
raw: line,
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// =========================================================================
|
|
2285
|
+
// Summary and session end detection
|
|
2286
|
+
// =========================================================================
|
|
2287
|
+
|
|
2288
|
+
/**
|
|
2289
|
+
* Check for [[SUMMARY]] blocks
|
|
2290
|
+
*/
|
|
2291
|
+
private checkForSummary(content: string): void {
|
|
2292
|
+
const result = parseSummaryWithDetails(content);
|
|
2293
|
+
if (!result.found || !result.valid) {
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
if (result.rawContent === this.lastSummaryRawContent) {
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
this.lastSummaryRawContent = result.rawContent ?? '';
|
|
2301
|
+
|
|
2302
|
+
this.emit('summary', {
|
|
2303
|
+
agentName: this.config.name,
|
|
2304
|
+
summary: result.summary,
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
/**
|
|
2309
|
+
* Check for [[SESSION_END]] blocks
|
|
2310
|
+
*/
|
|
2311
|
+
private checkForSessionEnd(content: string): void {
|
|
2312
|
+
if (this.sessionEndProcessed) {
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
const sessionEnd = parseSessionEndFromOutput(content);
|
|
2317
|
+
if (!sessionEnd) {
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
this.sessionEndProcessed = true;
|
|
2322
|
+
this.emit('session-end', {
|
|
2323
|
+
agentName: this.config.name,
|
|
2324
|
+
marker: sessionEnd,
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// =========================================================================
|
|
2329
|
+
// Public API
|
|
2330
|
+
// =========================================================================
|
|
2331
|
+
|
|
2332
|
+
/**
|
|
2333
|
+
* Query status from relay-pty
|
|
2334
|
+
*/
|
|
2335
|
+
async queryStatus(): Promise<StatusResponse | null> {
|
|
2336
|
+
if (!this.socket || !this.socketConnected) {
|
|
2337
|
+
return null;
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
try {
|
|
2341
|
+
await this.sendSocketRequest({ type: 'status' });
|
|
2342
|
+
// Response will come asynchronously via handleSocketResponse
|
|
2343
|
+
// For now, return null - could implement request/response matching
|
|
2344
|
+
return null;
|
|
2345
|
+
} catch {
|
|
2346
|
+
return null;
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
/**
|
|
2351
|
+
* Wait for the CLI to be ready to receive messages.
|
|
2352
|
+
* This waits for:
|
|
2353
|
+
* 1. The CLI to produce at least one output (it has started)
|
|
2354
|
+
* 2. The CLI to become idle (it's ready for input)
|
|
2355
|
+
*
|
|
2356
|
+
* This is more reliable than a random sleep because it waits for
|
|
2357
|
+
* actual signals from the CLI rather than guessing how long it takes to start.
|
|
2358
|
+
*
|
|
2359
|
+
* @param timeoutMs Maximum time to wait (default: 30s)
|
|
2360
|
+
* @param pollMs Polling interval (default: 100ms)
|
|
2361
|
+
* @returns true if CLI is ready, false if timeout
|
|
2362
|
+
*/
|
|
2363
|
+
async waitUntilCliReady(timeoutMs = 30000, pollMs = 100): Promise<boolean> {
|
|
2364
|
+
const startTime = Date.now();
|
|
2365
|
+
this.log(` Waiting for CLI to be ready (timeout: ${timeoutMs}ms)`);
|
|
2366
|
+
|
|
2367
|
+
// In interactive mode, stdout is inherited (not captured), so hasReceivedOutput
|
|
2368
|
+
// will never be set. Trust that the process is ready if it's running.
|
|
2369
|
+
if (this.isInteractive) {
|
|
2370
|
+
this.log(` Interactive mode - trusting process is ready`);
|
|
2371
|
+
// Give a brief moment for the CLI to initialize its TUI.
|
|
2372
|
+
// 500ms is a conservative estimate based on typical CLI startup times:
|
|
2373
|
+
// - Claude CLI: ~200-300ms to show initial prompt
|
|
2374
|
+
// - Codex/Gemini: ~300-400ms
|
|
2375
|
+
// This delay is only used in interactive mode where we can't detect output.
|
|
2376
|
+
// In non-interactive mode, we poll for actual output instead.
|
|
2377
|
+
await sleep(500);
|
|
2378
|
+
return this.running;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
// Phase 1: Wait for first output (CLI has started)
|
|
2382
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2383
|
+
if (this.hasReceivedOutput) {
|
|
2384
|
+
this.log(` CLI has started producing output`);
|
|
2385
|
+
break;
|
|
2386
|
+
}
|
|
2387
|
+
await sleep(pollMs);
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
if (!this.hasReceivedOutput) {
|
|
2391
|
+
this.log(` Timeout waiting for CLI to produce output`);
|
|
2392
|
+
return false;
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
// Phase 2: Wait for idle state (CLI is ready for input)
|
|
2396
|
+
const remainingTime = timeoutMs - (Date.now() - startTime);
|
|
2397
|
+
if (remainingTime <= 0) {
|
|
2398
|
+
return false;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
const idleResult = await this.waitForIdleState(remainingTime, pollMs);
|
|
2402
|
+
if (idleResult.isIdle) {
|
|
2403
|
+
this.log(` CLI is idle and ready (confidence: ${idleResult.confidence.toFixed(2)})`);
|
|
2404
|
+
return true;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
this.log(` Timeout waiting for CLI to become idle`);
|
|
2408
|
+
return false;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
/**
|
|
2412
|
+
* Check if the CLI has produced any output yet.
|
|
2413
|
+
* Useful for checking if the CLI has started without blocking.
|
|
2414
|
+
* In interactive mode, returns true if process is running (output isn't captured).
|
|
2415
|
+
*/
|
|
2416
|
+
hasCliStarted(): boolean {
|
|
2417
|
+
// In interactive mode, stdout isn't captured so hasReceivedOutput is never set
|
|
2418
|
+
if (this.isInteractive) {
|
|
2419
|
+
return this.running;
|
|
2420
|
+
}
|
|
2421
|
+
return this.hasReceivedOutput;
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
/**
|
|
2425
|
+
* Check if the orchestrator is ready to receive and inject messages.
|
|
2426
|
+
* This requires:
|
|
2427
|
+
* 1. relay-pty process spawned
|
|
2428
|
+
* 2. Socket connected to relay-pty
|
|
2429
|
+
* 3. running flag set
|
|
2430
|
+
*
|
|
2431
|
+
* Use this to verify the agent can actually receive injected messages,
|
|
2432
|
+
* not just that the CLI is running.
|
|
2433
|
+
*/
|
|
2434
|
+
isReadyForMessages(): boolean {
|
|
2435
|
+
return this.readyForMessages && this.running && this.socketConnected;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
/**
|
|
2439
|
+
* Wait until the orchestrator is ready to receive and inject messages.
|
|
2440
|
+
* This is more comprehensive than waitUntilCliReady because it ensures:
|
|
2441
|
+
* 1. CLI is ready (has output and is idle)
|
|
2442
|
+
* 2. Orchestrator is ready (socket connected, can inject)
|
|
2443
|
+
*
|
|
2444
|
+
* @param timeoutMs Maximum time to wait (default: 30s)
|
|
2445
|
+
* @param pollMs Polling interval (default: 100ms)
|
|
2446
|
+
* @returns true if ready, false if timeout
|
|
2447
|
+
*/
|
|
2448
|
+
async waitUntilReadyForMessages(timeoutMs = 30000, pollMs = 100): Promise<boolean> {
|
|
2449
|
+
const startTime = Date.now();
|
|
2450
|
+
this.log(` Waiting for orchestrator to be ready for messages (timeout: ${timeoutMs}ms)`);
|
|
2451
|
+
|
|
2452
|
+
// First wait for CLI to be ready (output + idle)
|
|
2453
|
+
const cliReady = await this.waitUntilCliReady(timeoutMs, pollMs);
|
|
2454
|
+
if (!cliReady) {
|
|
2455
|
+
this.log(` CLI not ready within timeout`);
|
|
2456
|
+
return false;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// Then wait for readyForMessages flag
|
|
2460
|
+
const remainingTime = timeoutMs - (Date.now() - startTime);
|
|
2461
|
+
if (remainingTime <= 0) {
|
|
2462
|
+
this.log(` No time remaining to wait for readyForMessages`);
|
|
2463
|
+
return this.isReadyForMessages();
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2467
|
+
if (this.isReadyForMessages()) {
|
|
2468
|
+
this.log(` Orchestrator is ready for messages`);
|
|
2469
|
+
return true;
|
|
2470
|
+
}
|
|
2471
|
+
await sleep(pollMs);
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
this.log(` Timeout waiting for orchestrator to be ready for messages`);
|
|
2475
|
+
return false;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
/**
|
|
2479
|
+
* Get raw output buffer
|
|
2480
|
+
*/
|
|
2481
|
+
getRawOutput(): string {
|
|
2482
|
+
return this.rawBuffer;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
/**
|
|
2486
|
+
* Check if backpressure is active
|
|
2487
|
+
*/
|
|
2488
|
+
isBackpressureActive(): boolean {
|
|
2489
|
+
return this.backpressureActive;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
/**
|
|
2493
|
+
* Get the socket path
|
|
2494
|
+
*/
|
|
2495
|
+
getSocketPath(): string {
|
|
2496
|
+
return this.socketPath;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
/**
|
|
2500
|
+
* Get the relay-pty process PID
|
|
2501
|
+
*/
|
|
2502
|
+
get pid(): number | undefined {
|
|
2503
|
+
return this.relayPtyProcess?.pid;
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
/**
|
|
2507
|
+
* Get the log file path (not used by relay-pty, returns undefined)
|
|
2508
|
+
*/
|
|
2509
|
+
get logPath(): string | undefined {
|
|
2510
|
+
return this._logPath;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
/**
|
|
2514
|
+
* Kill the process forcefully
|
|
2515
|
+
*/
|
|
2516
|
+
async kill(): Promise<void> {
|
|
2517
|
+
this.isGracefulStop = true; // Mark as intentional to prevent crash broadcast
|
|
2518
|
+
if (this.socketReconnectTimer) {
|
|
2519
|
+
clearTimeout(this.socketReconnectTimer);
|
|
2520
|
+
this.socketReconnectTimer = undefined;
|
|
2521
|
+
}
|
|
2522
|
+
if (this.relayPtyProcess && !this.relayPtyProcess.killed) {
|
|
2523
|
+
this.relayPtyProcess.kill('SIGKILL');
|
|
2524
|
+
}
|
|
2525
|
+
this.running = false;
|
|
2526
|
+
this.disconnectSocket();
|
|
2527
|
+
this.destroyClient();
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
/**
|
|
2531
|
+
* Get output lines (for compatibility with PtyWrapper)
|
|
2532
|
+
* @param limit Maximum number of lines to return
|
|
2533
|
+
*/
|
|
2534
|
+
getOutput(limit?: number): string[] {
|
|
2535
|
+
const lines = this.rawBuffer.split('\n');
|
|
2536
|
+
if (limit && limit > 0) {
|
|
2537
|
+
return lines.slice(-limit);
|
|
2538
|
+
}
|
|
2539
|
+
return lines;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
/**
|
|
2543
|
+
* Write data directly to the process stdin
|
|
2544
|
+
* @param data Data to write
|
|
2545
|
+
*/
|
|
2546
|
+
async write(data: string | Buffer): Promise<void> {
|
|
2547
|
+
if (!this.relayPtyProcess || !this.relayPtyProcess.stdin) {
|
|
2548
|
+
throw new Error('Process not running');
|
|
2549
|
+
}
|
|
2550
|
+
const buffer = typeof data === 'string' ? Buffer.from(data) : data;
|
|
2551
|
+
this.relayPtyProcess.stdin.write(buffer);
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
/**
|
|
2555
|
+
* Inject a task using the socket-based injection system with verification.
|
|
2556
|
+
* This is the preferred method for spawned agent task delivery.
|
|
2557
|
+
*
|
|
2558
|
+
* @param task The task text to inject
|
|
2559
|
+
* @param from The sender name (default: "spawner")
|
|
2560
|
+
* @returns Promise resolving to true if injection succeeded, false otherwise
|
|
2561
|
+
*/
|
|
2562
|
+
async injectTask(task: string, from = 'spawner'): Promise<boolean> {
|
|
2563
|
+
if (!this.socket || !this.socketConnected) {
|
|
2564
|
+
this.log(` Socket not connected for task injection, falling back to stdin write`);
|
|
2565
|
+
// Fallback to direct write if socket not available
|
|
2566
|
+
try {
|
|
2567
|
+
await this.write(task + '\n');
|
|
2568
|
+
return true;
|
|
2569
|
+
} catch (err: any) {
|
|
2570
|
+
this.logError(` Stdin write fallback failed: ${err.message}`);
|
|
2571
|
+
return false;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
const messageId = `task-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
2576
|
+
const shortId = messageId.substring(0, 8);
|
|
2577
|
+
|
|
2578
|
+
this.log(` Injecting task via socket: ${shortId}`);
|
|
2579
|
+
|
|
2580
|
+
// Create request
|
|
2581
|
+
const request: InjectRequest = {
|
|
2582
|
+
type: 'inject',
|
|
2583
|
+
id: messageId,
|
|
2584
|
+
from,
|
|
2585
|
+
body: task,
|
|
2586
|
+
priority: 0, // High priority for initial task
|
|
2587
|
+
};
|
|
2588
|
+
|
|
2589
|
+
// Send with timeout and verification
|
|
2590
|
+
return new Promise<boolean>((resolve) => {
|
|
2591
|
+
const timeout = setTimeout(() => {
|
|
2592
|
+
this.logError(` Task inject timeout for ${shortId} after 30s`);
|
|
2593
|
+
this.pendingInjections.delete(messageId);
|
|
2594
|
+
resolve(false);
|
|
2595
|
+
}, 30000);
|
|
2596
|
+
|
|
2597
|
+
this.pendingInjections.set(messageId, {
|
|
2598
|
+
resolve,
|
|
2599
|
+
reject: () => resolve(false),
|
|
2600
|
+
timeout,
|
|
2601
|
+
from,
|
|
2602
|
+
shortId,
|
|
2603
|
+
retryCount: 0,
|
|
2604
|
+
originalBody: task,
|
|
2605
|
+
});
|
|
2606
|
+
|
|
2607
|
+
this.sendSocketRequest(request)
|
|
2608
|
+
.then(() => {
|
|
2609
|
+
this.log(` Task inject request sent: ${shortId}`);
|
|
2610
|
+
})
|
|
2611
|
+
.catch((err) => {
|
|
2612
|
+
this.logError(` Task inject socket request failed: ${err.message}`);
|
|
2613
|
+
clearTimeout(timeout);
|
|
2614
|
+
this.pendingInjections.delete(messageId);
|
|
2615
|
+
resolve(false);
|
|
2616
|
+
});
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
/**
|
|
2621
|
+
* Get the agent ID (from continuity if available)
|
|
2622
|
+
*/
|
|
2623
|
+
getAgentId(): string | undefined {
|
|
2624
|
+
return this.agentId;
|
|
2625
|
+
}
|
|
2626
|
+
}
|