agent-relay 1.0.22 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bridge/shadow-cli.d.ts +17 -0
- package/dist/bridge/shadow-cli.d.ts.map +1 -0
- package/dist/bridge/shadow-cli.js +75 -0
- package/dist/bridge/shadow-cli.js.map +1 -0
- package/dist/bridge/shadow-config.d.ts +87 -0
- package/dist/bridge/shadow-config.d.ts.map +1 -0
- package/dist/bridge/shadow-config.js +134 -0
- package/dist/bridge/shadow-config.js.map +1 -0
- package/dist/bridge/spawner.d.ts +68 -1
- package/dist/bridge/spawner.d.ts.map +1 -1
- package/dist/bridge/spawner.js +360 -16
- package/dist/bridge/spawner.js.map +1 -1
- package/dist/bridge/types.d.ts +67 -0
- package/dist/bridge/types.d.ts.map +1 -1
- package/dist/cli/index.js +1196 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/cloud/api/auth.d.ts +20 -0
- package/dist/cloud/api/auth.d.ts.map +1 -0
- package/dist/cloud/api/auth.js +128 -0
- package/dist/cloud/api/auth.js.map +1 -0
- package/dist/cloud/api/billing.d.ts +17 -0
- package/dist/cloud/api/billing.d.ts.map +1 -0
- package/dist/cloud/api/billing.js +353 -0
- package/dist/cloud/api/billing.js.map +1 -0
- package/dist/cloud/api/cli-pty-runner.d.ts +54 -0
- package/dist/cloud/api/cli-pty-runner.d.ts.map +1 -0
- package/dist/cloud/api/cli-pty-runner.js +119 -0
- package/dist/cloud/api/cli-pty-runner.js.map +1 -0
- package/dist/cloud/api/coordinators.d.ts +8 -0
- package/dist/cloud/api/coordinators.d.ts.map +1 -0
- package/dist/cloud/api/coordinators.js +347 -0
- package/dist/cloud/api/coordinators.js.map +1 -0
- package/dist/cloud/api/daemons.d.ts +12 -0
- package/dist/cloud/api/daemons.d.ts.map +1 -0
- package/dist/cloud/api/daemons.js +320 -0
- package/dist/cloud/api/daemons.js.map +1 -0
- package/dist/cloud/api/generic-webhooks.d.ts +8 -0
- package/dist/cloud/api/generic-webhooks.d.ts.map +1 -0
- package/dist/cloud/api/generic-webhooks.js +129 -0
- package/dist/cloud/api/generic-webhooks.js.map +1 -0
- package/dist/cloud/api/git.d.ts +8 -0
- package/dist/cloud/api/git.d.ts.map +1 -0
- package/dist/cloud/api/git.js +131 -0
- package/dist/cloud/api/git.js.map +1 -0
- package/dist/cloud/api/github-app.d.ts +11 -0
- package/dist/cloud/api/github-app.d.ts.map +1 -0
- package/dist/cloud/api/github-app.js +189 -0
- package/dist/cloud/api/github-app.js.map +1 -0
- package/dist/cloud/api/middleware/planLimits.d.ts +43 -0
- package/dist/cloud/api/middleware/planLimits.d.ts.map +1 -0
- package/dist/cloud/api/middleware/planLimits.js +202 -0
- package/dist/cloud/api/middleware/planLimits.js.map +1 -0
- package/dist/cloud/api/monitoring.d.ts +11 -0
- package/dist/cloud/api/monitoring.d.ts.map +1 -0
- package/dist/cloud/api/monitoring.js +578 -0
- package/dist/cloud/api/monitoring.js.map +1 -0
- package/dist/cloud/api/nango-auth.d.ts +9 -0
- package/dist/cloud/api/nango-auth.d.ts.map +1 -0
- package/dist/cloud/api/nango-auth.js +377 -0
- package/dist/cloud/api/nango-auth.js.map +1 -0
- package/dist/cloud/api/onboarding.d.ts +15 -0
- package/dist/cloud/api/onboarding.d.ts.map +1 -0
- package/dist/cloud/api/onboarding.js +588 -0
- package/dist/cloud/api/onboarding.js.map +1 -0
- package/dist/cloud/api/policy.d.ts +8 -0
- package/dist/cloud/api/policy.d.ts.map +1 -0
- package/dist/cloud/api/policy.js +229 -0
- package/dist/cloud/api/policy.js.map +1 -0
- package/dist/cloud/api/providers.d.ts +7 -0
- package/dist/cloud/api/providers.d.ts.map +1 -0
- package/dist/cloud/api/providers.js +507 -0
- package/dist/cloud/api/providers.js.map +1 -0
- package/dist/cloud/api/repos.d.ts +7 -0
- package/dist/cloud/api/repos.d.ts.map +1 -0
- package/dist/cloud/api/repos.js +314 -0
- package/dist/cloud/api/repos.js.map +1 -0
- package/dist/cloud/api/teams.d.ts +7 -0
- package/dist/cloud/api/teams.d.ts.map +1 -0
- package/dist/cloud/api/teams.js +279 -0
- package/dist/cloud/api/teams.js.map +1 -0
- package/dist/cloud/api/test-helpers.d.ts +10 -0
- package/dist/cloud/api/test-helpers.d.ts.map +1 -0
- package/dist/cloud/api/test-helpers.js +575 -0
- package/dist/cloud/api/test-helpers.js.map +1 -0
- package/dist/cloud/api/usage.d.ts +7 -0
- package/dist/cloud/api/usage.d.ts.map +1 -0
- package/dist/cloud/api/usage.js +98 -0
- package/dist/cloud/api/usage.js.map +1 -0
- package/dist/cloud/api/webhooks.d.ts +7 -0
- package/dist/cloud/api/webhooks.d.ts.map +1 -0
- package/dist/cloud/api/webhooks.js +496 -0
- package/dist/cloud/api/webhooks.js.map +1 -0
- package/dist/cloud/api/workspaces.d.ts +7 -0
- package/dist/cloud/api/workspaces.d.ts.map +1 -0
- package/dist/cloud/api/workspaces.js +727 -0
- package/dist/cloud/api/workspaces.js.map +1 -0
- package/dist/cloud/billing/index.d.ts +9 -0
- package/dist/cloud/billing/index.d.ts.map +1 -0
- package/dist/cloud/billing/index.js +9 -0
- package/dist/cloud/billing/index.js.map +1 -0
- package/dist/cloud/billing/plans.d.ts +39 -0
- package/dist/cloud/billing/plans.d.ts.map +1 -0
- package/dist/cloud/billing/plans.js +245 -0
- package/dist/cloud/billing/plans.js.map +1 -0
- package/dist/cloud/billing/service.d.ts +80 -0
- package/dist/cloud/billing/service.d.ts.map +1 -0
- package/dist/cloud/billing/service.js +388 -0
- package/dist/cloud/billing/service.js.map +1 -0
- package/dist/cloud/billing/types.d.ts +141 -0
- package/dist/cloud/billing/types.d.ts.map +1 -0
- package/dist/cloud/billing/types.js +7 -0
- package/dist/cloud/billing/types.js.map +1 -0
- package/dist/cloud/config.d.ts +66 -0
- package/dist/cloud/config.d.ts.map +1 -0
- package/dist/cloud/config.js +92 -0
- package/dist/cloud/config.js.map +1 -0
- package/dist/cloud/db/drizzle.d.ts +215 -0
- package/dist/cloud/db/drizzle.d.ts.map +1 -0
- package/dist/cloud/db/drizzle.js +1083 -0
- package/dist/cloud/db/drizzle.js.map +1 -0
- package/dist/cloud/db/index.d.ts +35 -0
- package/dist/cloud/db/index.d.ts.map +1 -0
- package/dist/cloud/db/index.js +52 -0
- package/dist/cloud/db/index.js.map +1 -0
- package/dist/cloud/db/schema.d.ts +4519 -0
- package/dist/cloud/db/schema.d.ts.map +1 -0
- package/dist/cloud/db/schema.js +547 -0
- package/dist/cloud/db/schema.js.map +1 -0
- package/dist/cloud/index.d.ts +12 -0
- package/dist/cloud/index.d.ts.map +1 -0
- package/dist/cloud/index.js +39 -0
- package/dist/cloud/index.js.map +1 -0
- package/dist/cloud/provisioner/index.d.ts +75 -0
- package/dist/cloud/provisioner/index.d.ts.map +1 -0
- package/dist/cloud/provisioner/index.js +977 -0
- package/dist/cloud/provisioner/index.js.map +1 -0
- package/dist/cloud/server.d.ts +17 -0
- package/dist/cloud/server.d.ts.map +1 -0
- package/dist/cloud/server.js +534 -0
- package/dist/cloud/server.js.map +1 -0
- package/dist/cloud/services/auto-scaler.d.ts +152 -0
- package/dist/cloud/services/auto-scaler.d.ts.map +1 -0
- package/dist/cloud/services/auto-scaler.js +439 -0
- package/dist/cloud/services/auto-scaler.js.map +1 -0
- package/dist/cloud/services/capacity-manager.d.ts +148 -0
- package/dist/cloud/services/capacity-manager.d.ts.map +1 -0
- package/dist/cloud/services/capacity-manager.js +449 -0
- package/dist/cloud/services/capacity-manager.js.map +1 -0
- package/dist/cloud/services/ci-agent-spawner.d.ts +49 -0
- package/dist/cloud/services/ci-agent-spawner.d.ts.map +1 -0
- package/dist/cloud/services/ci-agent-spawner.js +373 -0
- package/dist/cloud/services/ci-agent-spawner.js.map +1 -0
- package/dist/cloud/services/coordinator.d.ts +62 -0
- package/dist/cloud/services/coordinator.d.ts.map +1 -0
- package/dist/cloud/services/coordinator.js +389 -0
- package/dist/cloud/services/coordinator.js.map +1 -0
- package/dist/cloud/services/index.d.ts +12 -0
- package/dist/cloud/services/index.d.ts.map +1 -0
- package/dist/cloud/services/index.js +15 -0
- package/dist/cloud/services/index.js.map +1 -0
- package/dist/cloud/services/mention-handler.d.ts +65 -0
- package/dist/cloud/services/mention-handler.d.ts.map +1 -0
- package/dist/cloud/services/mention-handler.js +405 -0
- package/dist/cloud/services/mention-handler.js.map +1 -0
- package/dist/cloud/services/nango.d.ts +126 -0
- package/dist/cloud/services/nango.d.ts.map +1 -0
- package/dist/cloud/services/nango.js +191 -0
- package/dist/cloud/services/nango.js.map +1 -0
- package/dist/cloud/services/persistence.d.ts +131 -0
- package/dist/cloud/services/persistence.d.ts.map +1 -0
- package/dist/cloud/services/persistence.js +200 -0
- package/dist/cloud/services/persistence.js.map +1 -0
- package/dist/cloud/services/planLimits.d.ts +125 -0
- package/dist/cloud/services/planLimits.d.ts.map +1 -0
- package/dist/cloud/services/planLimits.js +282 -0
- package/dist/cloud/services/planLimits.js.map +1 -0
- package/dist/cloud/services/scaling-orchestrator.d.ts +159 -0
- package/dist/cloud/services/scaling-orchestrator.d.ts.map +1 -0
- package/dist/cloud/services/scaling-orchestrator.js +502 -0
- package/dist/cloud/services/scaling-orchestrator.js.map +1 -0
- package/dist/cloud/services/scaling-policy.d.ts +121 -0
- package/dist/cloud/services/scaling-policy.d.ts.map +1 -0
- package/dist/cloud/services/scaling-policy.js +415 -0
- package/dist/cloud/services/scaling-policy.js.map +1 -0
- package/dist/cloud/vault/index.d.ts +76 -0
- package/dist/cloud/vault/index.d.ts.map +1 -0
- package/dist/cloud/vault/index.js +219 -0
- package/dist/cloud/vault/index.js.map +1 -0
- package/dist/cloud/webhooks/index.d.ts +24 -0
- package/dist/cloud/webhooks/index.d.ts.map +1 -0
- package/dist/cloud/webhooks/index.js +29 -0
- package/dist/cloud/webhooks/index.js.map +1 -0
- package/dist/cloud/webhooks/parsers/github.d.ts +8 -0
- package/dist/cloud/webhooks/parsers/github.d.ts.map +1 -0
- package/dist/cloud/webhooks/parsers/github.js +234 -0
- package/dist/cloud/webhooks/parsers/github.js.map +1 -0
- package/dist/cloud/webhooks/parsers/index.d.ts +23 -0
- package/dist/cloud/webhooks/parsers/index.d.ts.map +1 -0
- package/dist/cloud/webhooks/parsers/index.js +30 -0
- package/dist/cloud/webhooks/parsers/index.js.map +1 -0
- package/dist/cloud/webhooks/parsers/linear.d.ts +9 -0
- package/dist/cloud/webhooks/parsers/linear.d.ts.map +1 -0
- package/dist/cloud/webhooks/parsers/linear.js +258 -0
- package/dist/cloud/webhooks/parsers/linear.js.map +1 -0
- package/dist/cloud/webhooks/parsers/slack.d.ts +9 -0
- package/dist/cloud/webhooks/parsers/slack.d.ts.map +1 -0
- package/dist/cloud/webhooks/parsers/slack.js +214 -0
- package/dist/cloud/webhooks/parsers/slack.js.map +1 -0
- package/dist/cloud/webhooks/responders/github.d.ts +8 -0
- package/dist/cloud/webhooks/responders/github.d.ts.map +1 -0
- package/dist/cloud/webhooks/responders/github.js +73 -0
- package/dist/cloud/webhooks/responders/github.js.map +1 -0
- package/dist/cloud/webhooks/responders/index.d.ts +23 -0
- package/dist/cloud/webhooks/responders/index.d.ts.map +1 -0
- package/dist/cloud/webhooks/responders/index.js +30 -0
- package/dist/cloud/webhooks/responders/index.js.map +1 -0
- package/dist/cloud/webhooks/responders/linear.d.ts +9 -0
- package/dist/cloud/webhooks/responders/linear.d.ts.map +1 -0
- package/dist/cloud/webhooks/responders/linear.js +149 -0
- package/dist/cloud/webhooks/responders/linear.js.map +1 -0
- package/dist/cloud/webhooks/responders/slack.d.ts +20 -0
- package/dist/cloud/webhooks/responders/slack.d.ts.map +1 -0
- package/dist/cloud/webhooks/responders/slack.js +178 -0
- package/dist/cloud/webhooks/responders/slack.js.map +1 -0
- package/dist/cloud/webhooks/router.d.ts +25 -0
- package/dist/cloud/webhooks/router.d.ts.map +1 -0
- package/dist/cloud/webhooks/router.js +504 -0
- package/dist/cloud/webhooks/router.js.map +1 -0
- package/dist/cloud/webhooks/rules-engine.d.ts +24 -0
- package/dist/cloud/webhooks/rules-engine.d.ts.map +1 -0
- package/dist/cloud/webhooks/rules-engine.js +287 -0
- package/dist/cloud/webhooks/rules-engine.js.map +1 -0
- package/dist/cloud/webhooks/types.d.ts +186 -0
- package/dist/cloud/webhooks/types.d.ts.map +1 -0
- package/dist/cloud/webhooks/types.js +8 -0
- package/dist/cloud/webhooks/types.js.map +1 -0
- package/dist/continuity/formatter.d.ts +51 -0
- package/dist/continuity/formatter.d.ts.map +1 -0
- package/dist/continuity/formatter.js +313 -0
- package/dist/continuity/formatter.js.map +1 -0
- package/dist/continuity/handoff-store.d.ts +67 -0
- package/dist/continuity/handoff-store.d.ts.map +1 -0
- package/dist/continuity/handoff-store.js +472 -0
- package/dist/continuity/handoff-store.js.map +1 -0
- package/dist/continuity/index.d.ts +45 -0
- package/dist/continuity/index.d.ts.map +1 -0
- package/dist/continuity/index.js +48 -0
- package/dist/continuity/index.js.map +1 -0
- package/dist/continuity/ledger-store.d.ts +110 -0
- package/dist/continuity/ledger-store.d.ts.map +1 -0
- package/dist/continuity/ledger-store.js +500 -0
- package/dist/continuity/ledger-store.js.map +1 -0
- package/dist/continuity/manager.d.ts +178 -0
- package/dist/continuity/manager.d.ts.map +1 -0
- package/dist/continuity/manager.js +562 -0
- package/dist/continuity/manager.js.map +1 -0
- package/dist/continuity/parser.d.ts +76 -0
- package/dist/continuity/parser.d.ts.map +1 -0
- package/dist/continuity/parser.js +579 -0
- package/dist/continuity/parser.js.map +1 -0
- package/dist/continuity/types.d.ts +180 -0
- package/dist/continuity/types.d.ts.map +1 -0
- package/dist/continuity/types.js +9 -0
- package/dist/continuity/types.js.map +1 -0
- package/dist/daemon/agent-manager.d.ts +114 -0
- package/dist/daemon/agent-manager.d.ts.map +1 -0
- package/dist/daemon/agent-manager.js +513 -0
- package/dist/daemon/agent-manager.js.map +1 -0
- package/dist/daemon/agent-registry.d.ts +34 -0
- package/dist/daemon/agent-registry.d.ts.map +1 -1
- package/dist/daemon/agent-registry.js +45 -2
- package/dist/daemon/agent-registry.js.map +1 -1
- package/dist/daemon/api.d.ts +81 -0
- package/dist/daemon/api.d.ts.map +1 -0
- package/dist/daemon/api.js +554 -0
- package/dist/daemon/api.js.map +1 -0
- package/dist/daemon/cli-auth.d.ts +67 -0
- package/dist/daemon/cli-auth.d.ts.map +1 -0
- package/dist/daemon/cli-auth.js +537 -0
- package/dist/daemon/cli-auth.js.map +1 -0
- package/dist/daemon/cloud-sync.d.ts +101 -0
- package/dist/daemon/cloud-sync.d.ts.map +1 -0
- package/dist/daemon/cloud-sync.js +263 -0
- package/dist/daemon/cloud-sync.js.map +1 -0
- package/dist/daemon/index.d.ts +4 -0
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +6 -0
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/orchestrator.d.ts +155 -0
- package/dist/daemon/orchestrator.d.ts.map +1 -0
- package/dist/daemon/orchestrator.js +766 -0
- package/dist/daemon/orchestrator.js.map +1 -0
- package/dist/daemon/router.d.ts +29 -0
- package/dist/daemon/router.d.ts.map +1 -1
- package/dist/daemon/router.js +143 -21
- package/dist/daemon/router.js.map +1 -1
- package/dist/daemon/server.d.ts +42 -0
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +199 -16
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/services/browser-testing.d.ts +88 -0
- package/dist/daemon/services/browser-testing.d.ts.map +1 -0
- package/dist/daemon/services/browser-testing.js +244 -0
- package/dist/daemon/services/browser-testing.js.map +1 -0
- package/dist/daemon/services/container-spawner.d.ts +135 -0
- package/dist/daemon/services/container-spawner.d.ts.map +1 -0
- package/dist/daemon/services/container-spawner.js +313 -0
- package/dist/daemon/services/container-spawner.js.map +1 -0
- package/dist/daemon/types.d.ts +131 -0
- package/dist/daemon/types.d.ts.map +1 -0
- package/dist/daemon/types.js +6 -0
- package/dist/daemon/types.js.map +1 -0
- package/dist/daemon/workspace-manager.d.ts +75 -0
- package/dist/daemon/workspace-manager.d.ts.map +1 -0
- package/dist/daemon/workspace-manager.js +289 -0
- package/dist/daemon/workspace-manager.js.map +1 -0
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/480-2d4111711d4e473c.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/724-73c1ee5f60abe860.js +9 -0
- package/dist/dashboard/out/_next/static/chunks/766-c3a14283c88d815b.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/page-7120be68bea622f3.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-dc2e3a1a22478efc.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/history/page-56a8b4616a90dc43.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/login/page-3eac37ea6f5dd153.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-1081dd190a331a91.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/page-daf87e86f783f980.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/pricing/page-4d72d5a5d8a9b618.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-b68a681526eb145e.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/signup/page-fee4ed1709070bcd.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
- package/dist/dashboard/out/_next/static/chunks/{main-e0a1f53fe0617a63.js → main-97850e03d723ea8c.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/main-app-5d692157a8eb1fd9.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
- package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +1 -0
- package/dist/dashboard/out/_next/static/css/411ce23ffeae9f76.css +1 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +45 -0
- package/dist/dashboard/out/alt-logos/logo.svg +38 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo.svg +38 -0
- package/dist/dashboard/out/app.html +1 -0
- package/dist/dashboard/out/app.txt +7 -0
- package/dist/dashboard/out/connect-repos.html +1 -0
- package/dist/dashboard/out/connect-repos.txt +7 -0
- package/dist/dashboard/out/history.html +1 -0
- package/dist/dashboard/out/history.txt +7 -0
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +2 -2
- package/dist/dashboard/out/login.html +6 -0
- package/dist/dashboard/out/login.txt +7 -0
- package/dist/dashboard/out/metrics.html +1 -515
- package/dist/dashboard/out/metrics.txt +2 -2
- package/dist/dashboard/out/pricing.html +13 -0
- package/dist/dashboard/out/pricing.txt +7 -0
- package/dist/dashboard/out/providers.html +1 -0
- package/dist/dashboard/out/providers.txt +7 -0
- package/dist/dashboard/out/signup.html +6 -0
- package/dist/dashboard/out/signup.txt +7 -0
- package/dist/dashboard-server/metrics.d.ts.map +1 -1
- package/dist/dashboard-server/metrics.js +3 -2
- package/dist/dashboard-server/metrics.js.map +1 -1
- package/dist/dashboard-server/server.d.ts.map +1 -1
- package/dist/dashboard-server/server.js +2653 -130
- package/dist/dashboard-server/server.js.map +1 -1
- package/dist/hooks/emitter.d.ts +40 -0
- package/dist/hooks/emitter.d.ts.map +1 -0
- package/dist/hooks/emitter.js +63 -0
- package/dist/hooks/emitter.js.map +1 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/registry.d.ts +173 -0
- package/dist/hooks/registry.d.ts.map +1 -0
- package/dist/hooks/registry.js +476 -0
- package/dist/hooks/registry.js.map +1 -0
- package/dist/hooks/trajectory-hooks.d.ts +52 -0
- package/dist/hooks/trajectory-hooks.d.ts.map +1 -0
- package/dist/hooks/trajectory-hooks.js +183 -0
- package/dist/hooks/trajectory-hooks.js.map +1 -0
- package/dist/hooks/types.d.ts +141 -0
- package/dist/hooks/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/memory/adapters/index.d.ts +8 -0
- package/dist/memory/adapters/index.d.ts.map +1 -0
- package/dist/memory/adapters/index.js +8 -0
- package/dist/memory/adapters/index.js.map +1 -0
- package/dist/memory/adapters/inmemory.d.ts +59 -0
- package/dist/memory/adapters/inmemory.d.ts.map +1 -0
- package/dist/memory/adapters/inmemory.js +195 -0
- package/dist/memory/adapters/inmemory.js.map +1 -0
- package/dist/memory/adapters/supermemory.d.ts +71 -0
- package/dist/memory/adapters/supermemory.d.ts.map +1 -0
- package/dist/memory/adapters/supermemory.js +338 -0
- package/dist/memory/adapters/supermemory.js.map +1 -0
- package/dist/memory/factory.d.ts +48 -0
- package/dist/memory/factory.d.ts.map +1 -0
- package/dist/memory/factory.js +143 -0
- package/dist/memory/factory.js.map +1 -0
- package/dist/memory/index.d.ts +32 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +32 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/memory-hooks.d.ts +60 -0
- package/dist/memory/memory-hooks.d.ts.map +1 -0
- package/dist/memory/memory-hooks.js +313 -0
- package/dist/memory/memory-hooks.js.map +1 -0
- package/dist/memory/service.d.ts +49 -0
- package/dist/memory/service.d.ts.map +1 -0
- package/dist/memory/service.js +146 -0
- package/dist/memory/service.js.map +1 -0
- package/dist/memory/types.d.ts +195 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +8 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/policy/agent-policy.d.ts +225 -0
- package/dist/policy/agent-policy.d.ts.map +1 -0
- package/dist/policy/agent-policy.js +665 -0
- package/dist/policy/agent-policy.js.map +1 -0
- package/dist/policy/cloud-policy-fetcher.d.ts +12 -0
- package/dist/policy/cloud-policy-fetcher.d.ts.map +1 -0
- package/dist/policy/cloud-policy-fetcher.js +64 -0
- package/dist/policy/cloud-policy-fetcher.js.map +1 -0
- package/dist/protocol/types.d.ts +10 -1
- package/dist/protocol/types.d.ts.map +1 -1
- package/dist/resiliency/context-persistence.d.ts +140 -0
- package/dist/resiliency/context-persistence.d.ts.map +1 -0
- package/dist/resiliency/context-persistence.js +397 -0
- package/dist/resiliency/context-persistence.js.map +1 -0
- package/dist/resiliency/crash-insights.d.ts +156 -0
- package/dist/resiliency/crash-insights.d.ts.map +1 -0
- package/dist/resiliency/crash-insights.js +492 -0
- package/dist/resiliency/crash-insights.js.map +1 -0
- package/dist/resiliency/gossip-health.d.ts +137 -0
- package/dist/resiliency/gossip-health.d.ts.map +1 -0
- package/dist/resiliency/gossip-health.js +241 -0
- package/dist/resiliency/gossip-health.js.map +1 -0
- package/dist/resiliency/health-monitor.d.ts +97 -0
- package/dist/resiliency/health-monitor.d.ts.map +1 -0
- package/dist/resiliency/health-monitor.js +291 -0
- package/dist/resiliency/health-monitor.js.map +1 -0
- package/dist/resiliency/index.d.ts +68 -0
- package/dist/resiliency/index.d.ts.map +1 -0
- package/dist/resiliency/index.js +68 -0
- package/dist/resiliency/index.js.map +1 -0
- package/dist/resiliency/leader-watchdog.d.ts +109 -0
- package/dist/resiliency/leader-watchdog.d.ts.map +1 -0
- package/dist/resiliency/leader-watchdog.js +189 -0
- package/dist/resiliency/leader-watchdog.js.map +1 -0
- package/dist/resiliency/logger.d.ts +114 -0
- package/dist/resiliency/logger.d.ts.map +1 -0
- package/dist/resiliency/logger.js +250 -0
- package/dist/resiliency/logger.js.map +1 -0
- package/dist/resiliency/memory-monitor.d.ts +172 -0
- package/dist/resiliency/memory-monitor.d.ts.map +1 -0
- package/dist/resiliency/memory-monitor.js +593 -0
- package/dist/resiliency/memory-monitor.js.map +1 -0
- package/dist/resiliency/metrics.d.ts +115 -0
- package/dist/resiliency/metrics.d.ts.map +1 -0
- package/dist/resiliency/metrics.js +239 -0
- package/dist/resiliency/metrics.js.map +1 -0
- package/dist/resiliency/provider-context.d.ts +100 -0
- package/dist/resiliency/provider-context.d.ts.map +1 -0
- package/dist/resiliency/provider-context.js +360 -0
- package/dist/resiliency/provider-context.js.map +1 -0
- package/dist/resiliency/stateless-lead.d.ts +149 -0
- package/dist/resiliency/stateless-lead.d.ts.map +1 -0
- package/dist/resiliency/stateless-lead.js +308 -0
- package/dist/resiliency/stateless-lead.js.map +1 -0
- package/dist/resiliency/supervisor.d.ts +147 -0
- package/dist/resiliency/supervisor.d.ts.map +1 -0
- package/dist/resiliency/supervisor.js +459 -0
- package/dist/resiliency/supervisor.js.map +1 -0
- package/dist/shared/cli-auth-config.d.ts +91 -0
- package/dist/shared/cli-auth-config.d.ts.map +1 -0
- package/dist/shared/cli-auth-config.js +264 -0
- package/dist/shared/cli-auth-config.js.map +1 -0
- package/dist/storage/adapter.d.ts +3 -1
- package/dist/storage/adapter.d.ts.map +1 -1
- package/dist/storage/adapter.js +12 -2
- package/dist/storage/adapter.js.map +1 -1
- package/dist/storage/sqlite-adapter.d.ts.map +1 -1
- package/dist/storage/sqlite-adapter.js +18 -14
- package/dist/storage/sqlite-adapter.js.map +1 -1
- package/dist/trajectory/config.d.ts +84 -0
- package/dist/trajectory/config.d.ts.map +1 -0
- package/dist/trajectory/config.js +163 -0
- package/dist/trajectory/config.js.map +1 -0
- package/dist/trajectory/index.d.ts +8 -0
- package/dist/trajectory/index.d.ts.map +1 -0
- package/dist/trajectory/index.js +8 -0
- package/dist/trajectory/index.js.map +1 -0
- package/dist/trajectory/integration.d.ts +292 -0
- package/dist/trajectory/integration.d.ts.map +1 -0
- package/dist/trajectory/integration.js +834 -0
- package/dist/trajectory/integration.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.d.ts +40 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +84 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/project-namespace.d.ts +24 -0
- package/dist/utils/project-namespace.d.ts.map +1 -1
- package/dist/utils/project-namespace.js +84 -0
- package/dist/utils/project-namespace.js.map +1 -1
- package/dist/wrapper/client.d.ts +16 -1
- package/dist/wrapper/client.d.ts.map +1 -1
- package/dist/wrapper/client.js +32 -1
- package/dist/wrapper/client.js.map +1 -1
- package/dist/wrapper/parser.d.ts +13 -0
- package/dist/wrapper/parser.d.ts.map +1 -1
- package/dist/wrapper/parser.js +217 -47
- package/dist/wrapper/parser.js.map +1 -1
- package/dist/wrapper/pty-wrapper.d.ts +219 -17
- package/dist/wrapper/pty-wrapper.d.ts.map +1 -1
- package/dist/wrapper/pty-wrapper.js +1050 -104
- package/dist/wrapper/pty-wrapper.js.map +1 -1
- package/dist/wrapper/shared.d.ts +165 -0
- package/dist/wrapper/shared.d.ts.map +1 -0
- package/dist/wrapper/shared.js +270 -0
- package/dist/wrapper/shared.js.map +1 -0
- package/dist/wrapper/tmux-wrapper.d.ts +78 -11
- package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
- package/dist/wrapper/tmux-wrapper.js +567 -106
- package/dist/wrapper/tmux-wrapper.js.map +1 -1
- package/docs/CLOUD-ARCHITECTURE.md +804 -0
- package/docs/CLOUD-ONBOARDING-DESIGN.md +1983 -0
- package/docs/HOOKS_API.md +394 -0
- package/docs/WRAPPER_EVENTS.md +358 -0
- package/docs/agent-policy-snippet.md +40 -0
- package/docs/agent-relay-protocol.md +238 -0
- package/docs/agent-relay-snippet.md +115 -6
- package/docs/archive/EXECUTIVE_SUMMARY.md +358 -0
- package/docs/archive/ROADMAP.md +329 -0
- package/docs/archive/TESTING_PRESENCE_FEATURES.md +327 -0
- package/docs/competitive/GASTOWN.md +451 -0
- package/docs/{COMPETITIVE_ANALYSIS.md → competitive/OVERVIEW.md} +1 -0
- package/docs/competitive/README.md +34 -0
- package/docs/competitive/TMUX_ORCHESTRATOR.md +605 -0
- package/docs/dashboard.png +0 -0
- package/docs/design/ci-failure-webhooks.md +812 -0
- package/docs/design/comprehensive-integrations.md +238 -0
- package/docs/design/e2b-sandbox-integration.md +504 -0
- package/docs/design/github-app-permissions.md +264 -0
- package/docs/guides/CLOUD.md +236 -0
- package/docs/guides/LOCAL.md +535 -0
- package/docs/guides/SELF-HOSTED.md +494 -0
- package/docs/local-testing.md +428 -0
- package/docs/proposals/continuous-claude-integration.md +622 -0
- package/docs/proposals/custom-commands.md +368 -0
- package/docs/proposals/shadow-as-subagent.md +765 -0
- package/docs/proposals/slack-bot-integration.md +1457 -0
- package/docs/tasks/global-skills-system.tasks.md +230 -0
- package/docs/tasks/webhook-integrations.tasks.md +184 -0
- package/docs/tasks/workspace-capabilities.tasks.md +121 -0
- package/docs/testing/RESILIENCY-TEST-PLAN-2026-01-01.md +366 -0
- package/package.json +45 -7
- package/scripts/cloud-setup.sh +96 -0
- package/scripts/manual-qa.sh +293 -0
- package/scripts/postinstall.js +60 -0
- package/scripts/run-cloud-qa.sh +220 -0
- package/scripts/test-cli-auth/Dockerfile +44 -0
- package/scripts/test-cli-auth/Dockerfile.real +79 -0
- package/scripts/test-cli-auth/README.md +286 -0
- package/scripts/test-cli-auth/ci-test-real-clis.ts +251 -0
- package/scripts/test-cli-auth/ci-test-runner.ts +263 -0
- package/scripts/test-cli-auth/mock-cli.sh +147 -0
- package/scripts/test-cli-auth/package.json +14 -0
- package/scripts/test-cli-auth/test-oauth-flow.ts +220 -0
- package/scripts/test-pty-input-auto.js +222 -0
- package/scripts/test-pty-input.js +150 -0
- package/dist/dashboard/out/_next/static/chunks/app/layout-c9d8c5d95e48c6bf.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-8aa9936bc6c771ab.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/page-4498be09a5157759.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/main-app-bae2e535de00de50.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/webpack-c81f7fd28659d64f.js +0 -1
- package/dist/dashboard/out/_next/static/css/50ed6996e3df7bdd.css +0 -1
- /package/dist/dashboard/out/_next/static/{DXFA-jj8wb3PcY5DX2xcU → H5aWG0udPB4iOUIl_gytz}/_buildManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/{DXFA-jj8wb3PcY5DX2xcU → H5aWG0udPB4iOUIl_gytz}/_ssgManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{117-3bef7b19f3e60751.js → 117-b100311aff8d5c61.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{648-6cf686106c891ad3.js → 648-a13d3c2b1be45466.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/app/_not-found/{page-8ff6572bc7c9bc61.js → page-a4973f3e3c82fb67.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{fd9d1056-26bd8d656b496dba.js → fd9d1056-bf46c09eb57e019c.js} +0 -0
- /package/docs/{CHANGELOG.md → archive/CHANGELOG.md} +0 -0
- /package/docs/{CLI-SIMPLIFICATION-COMPLETE.md → archive/CLI-SIMPLIFICATION-COMPLETE.md} +0 -0
- /package/docs/{DESIGN_BRIDGE_STAFFING.md → archive/DESIGN_BRIDGE_STAFFING.md} +0 -0
- /package/docs/{DESIGN_V2.md → archive/DESIGN_V2.md} +0 -0
- /package/docs/{MONETIZATION.md → archive/MONETIZATION.md} +0 -0
- /package/docs/{PROPOSAL-trajectories.md → archive/PROPOSAL-trajectories.md} +0 -0
- /package/docs/{SCALING_ANALYSIS.md → archive/SCALING_ANALYSIS.md} +0 -0
- /package/docs/{TMUX_IMPLEMENTATION_NOTES.md → archive/TMUX_IMPLEMENTATION_NOTES.md} +0 -0
- /package/docs/{TMUX_IMPROVEMENTS.md → archive/TMUX_IMPROVEMENTS.md} +0 -0
- /package/docs/{dashboard-v2-plan.md → archive/dashboard-v2-plan.md} +0 -0
- /package/docs/{removable-code-analysis.md → archive/removable-code-analysis.md} +0 -0
- /package/docs/{competitive-analysis-mcp-agent-mail.md → competitive/MCP_AGENT_MAIL.md} +0 -0
|
@@ -10,6 +10,13 @@ import fs from 'node:fs';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { EventEmitter } from 'node:events';
|
|
12
12
|
import { RelayClient } from './client.js';
|
|
13
|
+
import { parseSummaryWithDetails, parseSessionEndFromOutput, isPlaceholderTarget } from './parser.js';
|
|
14
|
+
import { getProjectPaths } from '../utils/project-namespace.js';
|
|
15
|
+
import { getTrailEnvVars } from '../trajectory/integration.js';
|
|
16
|
+
import { findAgentConfig } from '../utils/agent-config.js';
|
|
17
|
+
import { HookRegistry, createTrajectoryHooks } from '../hooks/index.js';
|
|
18
|
+
import { getContinuityManager, parseContinuityCommand, hasContinuityCommand } from '../continuity/index.js';
|
|
19
|
+
import { INJECTION_CONSTANTS, stripAnsi, sleep, buildInjectionString, injectWithRetry as sharedInjectWithRetry, calculateSuccessRate, createInjectionMetrics, getDefaultRelayPrefix, detectCliType, CLI_QUIRKS, } from './shared.js';
|
|
13
20
|
/** Maximum lines to keep in output buffer */
|
|
14
21
|
const MAX_BUFFER_LINES = 10000;
|
|
15
22
|
export class PtyWrapper extends EventEmitter {
|
|
@@ -20,28 +27,96 @@ export class PtyWrapper extends EventEmitter {
|
|
|
20
27
|
outputBuffer = [];
|
|
21
28
|
rawBuffer = '';
|
|
22
29
|
relayPrefix;
|
|
30
|
+
cliType;
|
|
23
31
|
sentMessageHashes = new Set();
|
|
32
|
+
receivedMessageIds = new Set(); // Dedup incoming messages
|
|
24
33
|
processedSpawnCommands = new Set();
|
|
25
34
|
processedReleaseCommands = new Set();
|
|
35
|
+
pendingFencedSpawn = null;
|
|
26
36
|
messageQueue = [];
|
|
27
37
|
isInjecting = false;
|
|
38
|
+
readyForMessages = false;
|
|
39
|
+
lastOutputTime = 0;
|
|
40
|
+
injectionMetrics = createInjectionMetrics();
|
|
28
41
|
logFilePath;
|
|
29
42
|
logStream;
|
|
30
|
-
|
|
43
|
+
acceptedPrompts = new Set(); // Track which prompts have been accepted
|
|
44
|
+
hookRegistry;
|
|
45
|
+
sessionStartTime = Date.now();
|
|
46
|
+
continuity;
|
|
47
|
+
agentId;
|
|
48
|
+
processedContinuityCommands = new Set();
|
|
49
|
+
lastSummaryRawContent = ''; // Dedup summary event emissions
|
|
50
|
+
sessionEndProcessed = false; // Track if we've already emitted session-end
|
|
51
|
+
inThinkingBlock = false; // Track if inside <thinking>...</thinking>
|
|
52
|
+
lastSummaryTime = Date.now(); // Track when last summary was output
|
|
53
|
+
outputsSinceSummary = 0; // Count outputs since last summary
|
|
54
|
+
detectedTask; // Auto-detected task from agent config
|
|
55
|
+
sessionEndData; // Store SESSION_END data for handoff
|
|
56
|
+
instructionsInjected = false; // Track if init instructions have been injected
|
|
57
|
+
continuityInjected = false; // Track if continuity context has been injected
|
|
58
|
+
recentLogChunks = new Map(); // Dedup log streaming (hash -> timestamp)
|
|
59
|
+
static LOG_DEDUP_WINDOW_MS = 500; // Window for considering logs as duplicates
|
|
60
|
+
static LOG_DEDUP_MAX_SIZE = 100; // Max entries in dedup map
|
|
61
|
+
lastParsedLength = 0; // Track last parsed position to avoid re-parsing entire buffer
|
|
62
|
+
lastContinuityParsedLength = 0; // Same for continuity commands
|
|
31
63
|
constructor(config) {
|
|
32
64
|
super();
|
|
33
65
|
this.config = config;
|
|
34
|
-
this.relayPrefix = config.relayPrefix ??
|
|
66
|
+
this.relayPrefix = config.relayPrefix ?? getDefaultRelayPrefix();
|
|
67
|
+
// Detect CLI type from command for special handling
|
|
68
|
+
this.cliType = config.cliType ?? detectCliType(config.command);
|
|
69
|
+
// Auto-detect agent role from .claude/agents/ or .openagents/ if task not provided
|
|
70
|
+
let detectedTask = config.task;
|
|
71
|
+
if (!detectedTask) {
|
|
72
|
+
const agentConfig = findAgentConfig(config.name, config.cwd);
|
|
73
|
+
if (agentConfig?.description) {
|
|
74
|
+
detectedTask = agentConfig.description;
|
|
75
|
+
// Use stderr for consistency with TmuxWrapper's logStderr pattern
|
|
76
|
+
process.stderr.write(`[pty:${config.name}] Auto-detected role: ${detectedTask.substring(0, 60)}...\n`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Store detected task for use in hook registry
|
|
80
|
+
this.detectedTask = detectedTask;
|
|
35
81
|
this.client = new RelayClient({
|
|
36
82
|
agentName: config.name,
|
|
37
83
|
socketPath: config.socketPath,
|
|
38
|
-
cli:
|
|
84
|
+
cli: this.cliType,
|
|
85
|
+
task: detectedTask,
|
|
39
86
|
workingDirectory: config.cwd ?? process.cwd(),
|
|
40
87
|
quiet: true,
|
|
41
88
|
});
|
|
89
|
+
// Initialize hook registry
|
|
90
|
+
const projectPaths = getProjectPaths();
|
|
91
|
+
this.hookRegistry = new HookRegistry({
|
|
92
|
+
agentName: config.name,
|
|
93
|
+
workingDir: config.cwd ?? process.cwd(),
|
|
94
|
+
projectId: projectPaths.projectId,
|
|
95
|
+
task: this.detectedTask,
|
|
96
|
+
env: config.env,
|
|
97
|
+
inject: (text) => this.write(text + '\r'),
|
|
98
|
+
send: async (to, body) => {
|
|
99
|
+
this.client.sendMessage(to, body, 'message');
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
// Register trajectory hooks if enabled (default: true if task provided or auto-detected)
|
|
103
|
+
const enableTrajectory = config.trajectoryTracking ?? !!this.detectedTask;
|
|
104
|
+
if (enableTrajectory) {
|
|
105
|
+
const trajectoryHooks = createTrajectoryHooks({
|
|
106
|
+
projectId: projectPaths.projectId,
|
|
107
|
+
agentName: config.name,
|
|
108
|
+
});
|
|
109
|
+
this.hookRegistry.registerLifecycleHooks(trajectoryHooks);
|
|
110
|
+
}
|
|
111
|
+
// Register custom hooks if provided
|
|
112
|
+
if (config.hooks) {
|
|
113
|
+
this.hookRegistry.registerLifecycleHooks(config.hooks);
|
|
114
|
+
}
|
|
115
|
+
// Initialize continuity manager
|
|
116
|
+
this.continuity = getContinuityManager({ defaultCli: 'spawned' });
|
|
42
117
|
// Handle incoming messages
|
|
43
|
-
this.client.onMessage = (from, payload, messageId) => {
|
|
44
|
-
this.handleIncomingMessage(from, payload, messageId);
|
|
118
|
+
this.client.onMessage = (from, payload, messageId, meta, originalTo) => {
|
|
119
|
+
this.handleIncomingMessage(from, payload, messageId, meta, originalTo);
|
|
45
120
|
};
|
|
46
121
|
}
|
|
47
122
|
/**
|
|
@@ -65,6 +140,17 @@ export class PtyWrapper extends EventEmitter {
|
|
|
65
140
|
// Connect to relay daemon
|
|
66
141
|
try {
|
|
67
142
|
await this.client.connect();
|
|
143
|
+
// If this is a shadow agent, bind to the primary after connecting
|
|
144
|
+
if (this.config.shadowOf) {
|
|
145
|
+
const speakOn = this.config.shadowSpeakOn ?? ['EXPLICIT_ASK'];
|
|
146
|
+
const bound = this.client.bindAsShadow(this.config.shadowOf, { speakOn });
|
|
147
|
+
if (bound) {
|
|
148
|
+
console.log(`[pty:${this.config.name}] Bound as shadow of ${this.config.shadowOf} (speakOn: ${speakOn.join(', ')})`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
console.error(`[pty:${this.config.name}] Failed to bind as shadow of ${this.config.shadowOf}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
68
154
|
}
|
|
69
155
|
catch (err) {
|
|
70
156
|
console.error(`[pty:${this.config.name}] Relay connect failed: ${err.message}`);
|
|
@@ -75,6 +161,9 @@ export class PtyWrapper extends EventEmitter {
|
|
|
75
161
|
// Log spawn details for debugging
|
|
76
162
|
console.log(`[pty:${this.config.name}] Spawning: ${this.config.command} ${args.join(' ')}`);
|
|
77
163
|
console.log(`[pty:${this.config.name}] CWD: ${cwd}`);
|
|
164
|
+
// Get trail environment variables
|
|
165
|
+
const projectPaths = getProjectPaths();
|
|
166
|
+
const trailEnvVars = getTrailEnvVars(projectPaths.projectId, this.config.name, projectPaths.dataDir);
|
|
78
167
|
// Spawn the process with error handling
|
|
79
168
|
try {
|
|
80
169
|
this.ptyProcess = pty.spawn(this.config.command, args, {
|
|
@@ -85,6 +174,7 @@ export class PtyWrapper extends EventEmitter {
|
|
|
85
174
|
env: {
|
|
86
175
|
...process.env,
|
|
87
176
|
...this.config.env,
|
|
177
|
+
...trailEnvVars,
|
|
88
178
|
AGENT_RELAY_NAME: this.config.name,
|
|
89
179
|
TERM: 'xterm-256color',
|
|
90
180
|
},
|
|
@@ -97,6 +187,17 @@ export class PtyWrapper extends EventEmitter {
|
|
|
97
187
|
throw spawnError;
|
|
98
188
|
}
|
|
99
189
|
this.running = true;
|
|
190
|
+
this.sessionStartTime = Date.now();
|
|
191
|
+
// Dispatch session start hook (handles trajectory initialization)
|
|
192
|
+
this.hookRegistry.dispatchSessionStart().catch(err => {
|
|
193
|
+
console.error(`[pty:${this.config.name}] Session start hook error:`, err);
|
|
194
|
+
});
|
|
195
|
+
// Initialize continuity and get agentId, then inject context
|
|
196
|
+
this.initializeAgentId()
|
|
197
|
+
.then(() => this.injectContinuityContext())
|
|
198
|
+
.catch(err => {
|
|
199
|
+
console.error(`[pty:${this.config.name}] Agent ID/continuity initialization error:`, err);
|
|
200
|
+
});
|
|
100
201
|
// Capture output
|
|
101
202
|
this.ptyProcess.onData((data) => {
|
|
102
203
|
this.handleOutput(data);
|
|
@@ -108,13 +209,147 @@ export class PtyWrapper extends EventEmitter {
|
|
|
108
209
|
this.config.onExit?.(exitCode);
|
|
109
210
|
this.client.destroy();
|
|
110
211
|
});
|
|
111
|
-
// Inject initial instructions after a delay
|
|
112
|
-
setTimeout(() =>
|
|
212
|
+
// Inject initial instructions after a delay, then mark ready for messages
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
this.injectInstructions();
|
|
215
|
+
this.readyForMessages = true;
|
|
216
|
+
// Process any messages that arrived while waiting
|
|
217
|
+
this.processMessageQueue();
|
|
218
|
+
}, 2000);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Initialize agent ID for continuity/resume functionality
|
|
222
|
+
*/
|
|
223
|
+
async initializeAgentId() {
|
|
224
|
+
if (!this.continuity)
|
|
225
|
+
return;
|
|
226
|
+
try {
|
|
227
|
+
let ledger;
|
|
228
|
+
// If resuming from a previous agent ID, try to find that ledger
|
|
229
|
+
if (this.config.resumeAgentId) {
|
|
230
|
+
ledger = await this.continuity.findLedgerByAgentId(this.config.resumeAgentId);
|
|
231
|
+
if (ledger) {
|
|
232
|
+
console.log(`[pty:${this.config.name}] Resuming agent ID: ${ledger.agentId} (from previous session)`);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
console.error(`[pty:${this.config.name}] Resume agent ID ${this.config.resumeAgentId} not found, creating new`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// If not resuming or resume ID not found, get or create ledger
|
|
239
|
+
if (!ledger) {
|
|
240
|
+
ledger = await this.continuity.getOrCreateLedger(this.config.name, 'spawned');
|
|
241
|
+
console.log(`[pty:${this.config.name}] Agent ID: ${ledger.agentId} (use this to resume if agent dies)`);
|
|
242
|
+
}
|
|
243
|
+
this.agentId = ledger.agentId;
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
console.error(`[pty:${this.config.name}] Failed to initialize agent ID: ${err.message}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Get the current agent ID
|
|
251
|
+
*/
|
|
252
|
+
getAgentId() {
|
|
253
|
+
return this.agentId;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Inject continuity context from previous session.
|
|
257
|
+
* Called after agent ID initialization to restore state from ledger.
|
|
258
|
+
*/
|
|
259
|
+
async injectContinuityContext() {
|
|
260
|
+
if (!this.continuity || !this.running)
|
|
261
|
+
return;
|
|
262
|
+
// Guard: Only inject once per session
|
|
263
|
+
if (this.continuityInjected) {
|
|
264
|
+
console.log(`[pty:${this.config.name}] Continuity context already injected, skipping`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
this.continuityInjected = true;
|
|
268
|
+
try {
|
|
269
|
+
const context = await this.continuity.getStartupContext(this.config.name);
|
|
270
|
+
// Skip if no meaningful context (empty ledger or just boilerplate)
|
|
271
|
+
if (!context?.formatted || context.formatted.length < 50) {
|
|
272
|
+
console.log(`[pty:${this.config.name}] Skipping continuity injection (no meaningful context)`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (context?.formatted) {
|
|
276
|
+
// Build context notification similar to TmuxWrapper
|
|
277
|
+
const taskInfo = context.ledger?.currentTask
|
|
278
|
+
? `Task: ${context.ledger.currentTask.slice(0, 50)}`
|
|
279
|
+
: '';
|
|
280
|
+
const handoffInfo = context.handoff
|
|
281
|
+
? `Last handoff: ${context.handoff.createdAt.toISOString().split('T')[0]}`
|
|
282
|
+
: '';
|
|
283
|
+
const statusParts = [taskInfo, handoffInfo].filter(Boolean).join(' | ');
|
|
284
|
+
const notification = `[Continuity] Previous session context loaded.${statusParts ? ` ${statusParts}` : ''}\n\n${context.formatted}`;
|
|
285
|
+
// Queue continuity context directly to messageQueue with 'system' as sender
|
|
286
|
+
// This avoids creating confusing "Agent -> Agent" self-messages in the dashboard
|
|
287
|
+
// Fix for Lead communication issue: continuity checkpoints were creating self-messages
|
|
288
|
+
this.messageQueue.push({
|
|
289
|
+
from: 'system',
|
|
290
|
+
body: notification,
|
|
291
|
+
messageId: `continuity-startup-${Date.now()}`,
|
|
292
|
+
thread: 'continuity-context',
|
|
293
|
+
});
|
|
294
|
+
this.processMessageQueue();
|
|
295
|
+
const mode = context.handoff ? 'resume' : 'continue';
|
|
296
|
+
console.log(`[pty:${this.config.name}] Continuity context injected (${mode})`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
console.error(`[pty:${this.config.name}] Failed to inject continuity context: ${err.message}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Parse ->continuity: commands from output and handle them.
|
|
305
|
+
*
|
|
306
|
+
* Supported commands:
|
|
307
|
+
* ->continuity:save <<<...>>> - Save session state to ledger
|
|
308
|
+
* ->continuity:load - Request context injection
|
|
309
|
+
* ->continuity:search "query" - Search past handoffs
|
|
310
|
+
* ->continuity:uncertain "..." - Mark item as uncertain
|
|
311
|
+
* ->continuity:handoff <<<...>>> - Create explicit handoff
|
|
312
|
+
*/
|
|
313
|
+
async parseContinuityCommands(content) {
|
|
314
|
+
if (!this.continuity)
|
|
315
|
+
return;
|
|
316
|
+
if (!hasContinuityCommand(content))
|
|
317
|
+
return;
|
|
318
|
+
const command = parseContinuityCommand(content);
|
|
319
|
+
if (!command)
|
|
320
|
+
return;
|
|
321
|
+
// Deduplication: use type + content hash for all commands
|
|
322
|
+
// Fixed: content-less commands (like load) now use static hash to prevent infinite loops
|
|
323
|
+
const cmdHash = `${command.type}:${command.content || command.query || command.item || 'no-content'}`;
|
|
324
|
+
if (this.processedContinuityCommands.has(cmdHash))
|
|
325
|
+
return;
|
|
326
|
+
this.processedContinuityCommands.add(cmdHash);
|
|
327
|
+
// Limit dedup set size
|
|
328
|
+
if (this.processedContinuityCommands.size > 100) {
|
|
329
|
+
const oldest = this.processedContinuityCommands.values().next().value;
|
|
330
|
+
if (oldest)
|
|
331
|
+
this.processedContinuityCommands.delete(oldest);
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const response = await this.continuity.handleCommand(this.config.name, command);
|
|
335
|
+
if (response) {
|
|
336
|
+
// Inject response via relay message to self
|
|
337
|
+
this.client.sendMessage(this.config.name, response, 'message', {
|
|
338
|
+
thread: 'continuity-response',
|
|
339
|
+
});
|
|
340
|
+
console.log(`[pty:${this.config.name}] Continuity command handled: ${command.type}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
console.error(`[pty:${this.config.name}] Continuity command error: ${err.message}`);
|
|
345
|
+
}
|
|
113
346
|
}
|
|
114
347
|
/**
|
|
115
348
|
* Handle output from the process
|
|
116
349
|
*/
|
|
117
350
|
handleOutput(data) {
|
|
351
|
+
// Track output timing for stability checks
|
|
352
|
+
this.lastOutputTime = Date.now();
|
|
118
353
|
// Append to raw buffer
|
|
119
354
|
this.rawBuffer += data;
|
|
120
355
|
// Write to log file if available
|
|
@@ -123,9 +358,20 @@ export class PtyWrapper extends EventEmitter {
|
|
|
123
358
|
}
|
|
124
359
|
// Emit for external listeners
|
|
125
360
|
this.emit('output', data);
|
|
361
|
+
// Stream to daemon for dashboard log viewing (if connected)
|
|
362
|
+
// Filter out Claude's extended thinking blocks before streaming
|
|
363
|
+
// Also deduplicate to prevent terminal redraws from causing duplicate log entries
|
|
364
|
+
if (this.config.streamLogs !== false && this.client.state === 'READY') {
|
|
365
|
+
const filteredData = this.filterThinkingBlocks(data);
|
|
366
|
+
if (filteredData && !this.isDuplicateLogChunk(filteredData)) {
|
|
367
|
+
this.client.sendLog(filteredData);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
126
370
|
// Auto-accept Claude's first-run prompt for --dangerously-skip-permissions
|
|
127
371
|
// The prompt shows: "2. Yes, I accept" - we send "2" to accept
|
|
128
372
|
this.handleAutoAcceptPrompts(data);
|
|
373
|
+
// Handle terminal escape sequences that require responses (e.g., cursor position query)
|
|
374
|
+
this.handleTerminalEscapeSequences(data);
|
|
129
375
|
// Store in line buffer for logs
|
|
130
376
|
const lines = data.split('\n');
|
|
131
377
|
for (const line of lines) {
|
|
@@ -139,54 +385,250 @@ export class PtyWrapper extends EventEmitter {
|
|
|
139
385
|
}
|
|
140
386
|
// Parse for relay commands
|
|
141
387
|
this.parseRelayCommands();
|
|
388
|
+
// Dispatch output hook (handles phase detection, etc.)
|
|
389
|
+
const cleanData = stripAnsi(data);
|
|
390
|
+
this.hookRegistry.dispatchOutput(cleanData, data).catch(err => {
|
|
391
|
+
console.error(`[pty:${this.config.name}] Output hook error:`, err);
|
|
392
|
+
});
|
|
393
|
+
// Check for [[SUMMARY]] and [[SESSION_END]] blocks and emit events
|
|
394
|
+
// This allows cloud services to handle persistence without hardcoding storage
|
|
395
|
+
const cleanContent = stripAnsi(this.rawBuffer);
|
|
396
|
+
this.checkForSummaryAndEmit(cleanContent);
|
|
397
|
+
this.checkForSessionEndAndEmit(cleanContent);
|
|
398
|
+
// Parse for continuity commands (->continuity:save, ->continuity:load, etc.)
|
|
399
|
+
// Use rawBuffer (accumulated content) not immediate chunk, since multi-line
|
|
400
|
+
// fenced commands like ->continuity:save <<<...>>> span multiple output events
|
|
401
|
+
// Optimization: Only parse new content with lookback for incomplete fenced commands
|
|
402
|
+
if (cleanContent.length > this.lastContinuityParsedLength) {
|
|
403
|
+
const lookbackStart = Math.max(0, this.lastContinuityParsedLength - 500);
|
|
404
|
+
const contentToParse = cleanContent.substring(lookbackStart);
|
|
405
|
+
this.parseContinuityCommands(contentToParse).catch(err => {
|
|
406
|
+
console.error(`[pty:${this.config.name}] Continuity command parsing error:`, err);
|
|
407
|
+
});
|
|
408
|
+
this.lastContinuityParsedLength = cleanContent.length;
|
|
409
|
+
}
|
|
410
|
+
// Track outputs and potentially remind about summaries
|
|
411
|
+
this.trackOutputAndRemind(data);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Filter Claude's extended thinking blocks from output.
|
|
415
|
+
* Thinking blocks are wrapped in <thinking>...</thinking> tags and should
|
|
416
|
+
* not be streamed to the dashboard or stored in output buffers.
|
|
417
|
+
*
|
|
418
|
+
* This method tracks state across calls to handle multi-line thinking blocks.
|
|
419
|
+
*/
|
|
420
|
+
filterThinkingBlocks(data) {
|
|
421
|
+
const THINKING_START = /<thinking>/;
|
|
422
|
+
const THINKING_END = /<\/thinking>/;
|
|
423
|
+
const lines = data.split('\n');
|
|
424
|
+
const outputLines = [];
|
|
425
|
+
for (const line of lines) {
|
|
426
|
+
// If in thinking block, check for end
|
|
427
|
+
if (this.inThinkingBlock) {
|
|
428
|
+
if (THINKING_END.test(line)) {
|
|
429
|
+
this.inThinkingBlock = false;
|
|
430
|
+
// If there's content after </thinking> on the same line, keep it
|
|
431
|
+
const afterEnd = line.split('</thinking>')[1];
|
|
432
|
+
if (afterEnd && afterEnd.trim()) {
|
|
433
|
+
outputLines.push(afterEnd);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// Skip this line - inside thinking block
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
// Check for thinking start
|
|
440
|
+
if (THINKING_START.test(line)) {
|
|
441
|
+
this.inThinkingBlock = true;
|
|
442
|
+
// Check if it ends on the same line
|
|
443
|
+
if (THINKING_END.test(line)) {
|
|
444
|
+
this.inThinkingBlock = false;
|
|
445
|
+
}
|
|
446
|
+
// Keep content before <thinking> if any
|
|
447
|
+
const beforeStart = line.split('<thinking>')[0];
|
|
448
|
+
if (beforeStart && beforeStart.trim()) {
|
|
449
|
+
outputLines.push(beforeStart);
|
|
450
|
+
}
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
// Normal line - keep it
|
|
454
|
+
outputLines.push(line);
|
|
455
|
+
}
|
|
456
|
+
return outputLines.join('\n');
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Check if a log chunk is a duplicate (recently streamed).
|
|
460
|
+
* Prevents terminal redraws from causing duplicate log entries in the dashboard.
|
|
461
|
+
*
|
|
462
|
+
* Uses content normalization and time-based deduplication:
|
|
463
|
+
* - Strips whitespace and normalizes content for comparison
|
|
464
|
+
* - Considers chunks with same normalized content within LOG_DEDUP_WINDOW_MS as duplicates
|
|
465
|
+
* - Cleans up old entries to prevent memory growth
|
|
466
|
+
*/
|
|
467
|
+
isDuplicateLogChunk(data) {
|
|
468
|
+
// Normalize: strip excessive whitespace, limit to first 200 chars for hash
|
|
469
|
+
// This helps catch redraws that might have slight formatting differences
|
|
470
|
+
const normalized = stripAnsi(data).replace(/\s+/g, ' ').trim().substring(0, 200);
|
|
471
|
+
// Very short chunks (likely control chars or partial output) - allow through
|
|
472
|
+
if (normalized.length < 10) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
// Simple hash using string as key
|
|
476
|
+
const hash = normalized;
|
|
477
|
+
const now = Date.now();
|
|
478
|
+
// Check if this chunk was recently streamed
|
|
479
|
+
const lastSeen = this.recentLogChunks.get(hash);
|
|
480
|
+
if (lastSeen && (now - lastSeen) < PtyWrapper.LOG_DEDUP_WINDOW_MS) {
|
|
481
|
+
return true; // Duplicate
|
|
482
|
+
}
|
|
483
|
+
// Record this chunk
|
|
484
|
+
this.recentLogChunks.set(hash, now);
|
|
485
|
+
// Cleanup: remove old entries if map is getting large
|
|
486
|
+
if (this.recentLogChunks.size > PtyWrapper.LOG_DEDUP_MAX_SIZE) {
|
|
487
|
+
const cutoff = now - PtyWrapper.LOG_DEDUP_WINDOW_MS * 2;
|
|
488
|
+
for (const [key, timestamp] of this.recentLogChunks) {
|
|
489
|
+
if (timestamp < cutoff) {
|
|
490
|
+
this.recentLogChunks.delete(key);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return false; // Not a duplicate
|
|
142
495
|
}
|
|
143
496
|
/**
|
|
144
|
-
* Auto-accept Claude's first-run prompts
|
|
145
|
-
*
|
|
497
|
+
* Auto-accept Claude's first-run prompts
|
|
498
|
+
* Handles multiple prompts in sequence:
|
|
499
|
+
* 1. --dangerously-skip-permissions acceptance ("Yes, I accept")
|
|
500
|
+
* 2. Trust directory prompt ("Yes, I trust this folder")
|
|
501
|
+
* 3. "Ready to code here?" permission prompt ("Yes, continue")
|
|
502
|
+
*
|
|
503
|
+
* Uses a Set to track which prompts have been accepted, allowing
|
|
504
|
+
* multiple different prompts to be handled in sequence.
|
|
146
505
|
*/
|
|
147
506
|
handleAutoAcceptPrompts(data) {
|
|
148
|
-
if (this.hasAcceptedPrompt)
|
|
149
|
-
return;
|
|
150
507
|
if (!this.ptyProcess || !this.running)
|
|
151
508
|
return;
|
|
152
|
-
|
|
509
|
+
const cleanData = stripAnsi(data);
|
|
510
|
+
// Check for the permission acceptance prompt (--dangerously-skip-permissions)
|
|
153
511
|
// Pattern: "2. Yes, I accept" in the output
|
|
154
|
-
|
|
155
|
-
|
|
512
|
+
if (!this.acceptedPrompts.has('permission') &&
|
|
513
|
+
cleanData.includes('Yes, I accept') && cleanData.includes('No, exit')) {
|
|
156
514
|
console.log(`[pty:${this.config.name}] Detected permission prompt, auto-accepting...`);
|
|
157
|
-
this.
|
|
515
|
+
this.acceptedPrompts.add('permission');
|
|
158
516
|
// Send "2" to select "Yes, I accept" and Enter to confirm
|
|
159
517
|
setTimeout(() => {
|
|
160
518
|
if (this.ptyProcess && this.running) {
|
|
161
519
|
this.ptyProcess.write('2');
|
|
162
520
|
}
|
|
163
521
|
}, 100);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
// Check for the trust directory prompt
|
|
525
|
+
// Pattern: "1. Yes, I trust this folder" with "No, exit"
|
|
526
|
+
if (!this.acceptedPrompts.has('trust') &&
|
|
527
|
+
(cleanData.includes('trust this folder') || cleanData.includes('safety check'))
|
|
528
|
+
&& cleanData.includes('No, exit')) {
|
|
529
|
+
console.log(`[pty:${this.config.name}] Detected trust directory prompt, auto-accepting...`);
|
|
530
|
+
this.acceptedPrompts.add('trust');
|
|
531
|
+
// Send Enter to accept first option (already selected)
|
|
532
|
+
setTimeout(() => {
|
|
533
|
+
if (this.ptyProcess && this.running) {
|
|
534
|
+
this.ptyProcess.write('\r');
|
|
535
|
+
}
|
|
536
|
+
}, 300);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
// Check for "Ready to code here?" permission prompt
|
|
540
|
+
// Pattern: "Yes, continue" with "No, exit" and "Ready to code here?"
|
|
541
|
+
// This prompt asks for permission to work with files in the workspace
|
|
542
|
+
if (!this.acceptedPrompts.has('ready-to-code') &&
|
|
543
|
+
cleanData.includes('Yes, continue') && cleanData.includes('No, exit')
|
|
544
|
+
&& (cleanData.includes('Ready to code here') || cleanData.includes('permission to work with your files'))) {
|
|
545
|
+
console.log(`[pty:${this.config.name}] Detected "Ready to code here?" prompt, auto-accepting...`);
|
|
546
|
+
this.acceptedPrompts.add('ready-to-code');
|
|
547
|
+
// Send Enter to accept first option (already selected with ❯)
|
|
548
|
+
setTimeout(() => {
|
|
549
|
+
if (this.ptyProcess && this.running) {
|
|
550
|
+
this.ptyProcess.write('\r');
|
|
551
|
+
}
|
|
552
|
+
}, 300);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Handle terminal escape sequences that require responses.
|
|
558
|
+
*
|
|
559
|
+
* Some CLI tools (like Codex) query terminal capabilities and expect responses.
|
|
560
|
+
* Without proper responses, they timeout and crash.
|
|
561
|
+
*
|
|
562
|
+
* Supported sequences:
|
|
563
|
+
* - CSI 6 n (DSR - Device Status Report for cursor position)
|
|
564
|
+
* Response: CSI row ; col R (we report position 1;1)
|
|
565
|
+
*/
|
|
566
|
+
handleTerminalEscapeSequences(data) {
|
|
567
|
+
if (!this.ptyProcess || !this.running)
|
|
568
|
+
return;
|
|
569
|
+
// Check for cursor position query: ESC [ 6 n
|
|
570
|
+
// This can appear as \x1b[6n or \x1b[?6n
|
|
571
|
+
// eslint-disable-next-line no-control-regex
|
|
572
|
+
if (/\x1b\[\??6n/.test(data)) {
|
|
573
|
+
// Respond with cursor at position (1, 1)
|
|
574
|
+
// Format: ESC [ row ; col R
|
|
575
|
+
const response = '\x1b[1;1R';
|
|
576
|
+
// Small delay to ensure the query has been fully processed
|
|
577
|
+
setTimeout(() => {
|
|
578
|
+
if (this.ptyProcess && this.running) {
|
|
579
|
+
this.ptyProcess.write(response);
|
|
580
|
+
}
|
|
581
|
+
}, 10);
|
|
164
582
|
}
|
|
165
583
|
}
|
|
166
584
|
/**
|
|
167
585
|
* Parse relay commands from output.
|
|
168
586
|
* Handles both single-line and multi-line (fenced) formats.
|
|
169
587
|
* Deduplication via sentMessageHashes.
|
|
588
|
+
*
|
|
589
|
+
* Optimization: Only parses new content since last parse to avoid O(n²) behavior.
|
|
590
|
+
* Uses lookback buffer for incomplete fenced messages that span output chunks.
|
|
170
591
|
*/
|
|
171
592
|
parseRelayCommands() {
|
|
172
|
-
const cleanContent =
|
|
593
|
+
const cleanContent = stripAnsi(this.rawBuffer);
|
|
594
|
+
// Skip if no new content
|
|
595
|
+
if (cleanContent.length <= this.lastParsedLength)
|
|
596
|
+
return;
|
|
597
|
+
// For fenced messages, need some lookback for incomplete fences that span chunks
|
|
598
|
+
// 500 chars is enough to capture most relay message headers
|
|
599
|
+
const lookbackStart = Math.max(0, this.lastParsedLength - 500);
|
|
600
|
+
const contentToParse = cleanContent.substring(lookbackStart);
|
|
173
601
|
// First, try to find fenced multi-line messages: ->relay:Target <<<\n...\n>>>
|
|
174
|
-
this.parseFencedMessages(
|
|
602
|
+
this.parseFencedMessages(contentToParse);
|
|
175
603
|
// Then parse single-line messages
|
|
176
|
-
this.parseSingleLineMessages(
|
|
604
|
+
this.parseSingleLineMessages(contentToParse);
|
|
177
605
|
// Parse spawn/release commands
|
|
178
|
-
this.parseSpawnReleaseCommands(
|
|
606
|
+
this.parseSpawnReleaseCommands(contentToParse);
|
|
607
|
+
// Update parsed position
|
|
608
|
+
this.lastParsedLength = cleanContent.length;
|
|
179
609
|
}
|
|
180
610
|
/**
|
|
181
|
-
* Parse fenced multi-line messages: ->relay:Target <<<\n...\n>>>
|
|
611
|
+
* Parse fenced multi-line messages: ->relay:Target [thread:xxx] <<<\n...\n>>>
|
|
182
612
|
*/
|
|
183
613
|
parseFencedMessages(content) {
|
|
184
|
-
// Pattern: ->relay:Target <<< (with content on same or following lines until >>>)
|
|
185
|
-
|
|
614
|
+
// Pattern: ->relay:Target [thread:xxx] <<< (with content on same or following lines until >>>)
|
|
615
|
+
// Thread is optional, can be [thread:id] or [thread:project:id] for cross-project
|
|
616
|
+
const escapedPrefix = this.relayPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
617
|
+
const fenceStartPattern = new RegExp(`${escapedPrefix}(\\S+)(?:\\s+\\[thread:(?:([\\w-]+):)?([\\w-]+)\\])?\\s*<<<`, 'g');
|
|
186
618
|
let match;
|
|
187
619
|
while ((match = fenceStartPattern.exec(content)) !== null) {
|
|
188
620
|
const target = match[1];
|
|
621
|
+
const threadProject = match[2]; // Optional: project part of thread
|
|
622
|
+
const threadId = match[3]; // Thread ID
|
|
189
623
|
const startIdx = match.index + match[0].length;
|
|
624
|
+
// Skip spawn/release commands - they are handled by parseSpawnReleaseCommands
|
|
625
|
+
if (/^spawn$/i.test(target) || /^release$/i.test(target)) {
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
// Skip placeholder targets (documentation examples like "AgentName", "Lead", etc.)
|
|
629
|
+
if (isPlaceholderTarget(target)) {
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
190
632
|
// Find the closing >>>
|
|
191
633
|
const endIdx = content.indexOf('>>>', startIdx);
|
|
192
634
|
if (endIdx === -1)
|
|
@@ -203,17 +645,24 @@ export class PtyWrapper extends EventEmitter {
|
|
|
203
645
|
project = target.substring(0, colonIdx);
|
|
204
646
|
to = target.substring(colonIdx + 1);
|
|
205
647
|
}
|
|
648
|
+
// Skip placeholder targets after parsing cross-project syntax
|
|
649
|
+
if (isPlaceholderTarget(to)) {
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
206
652
|
this.sendRelayCommand({
|
|
207
653
|
to,
|
|
208
654
|
kind: 'message',
|
|
209
655
|
body,
|
|
210
656
|
project,
|
|
657
|
+
thread: threadId || undefined,
|
|
658
|
+
threadProject: threadProject || undefined,
|
|
211
659
|
raw: match[0],
|
|
212
660
|
});
|
|
213
661
|
}
|
|
214
662
|
}
|
|
215
663
|
/**
|
|
216
664
|
* Parse single-line messages (no fenced format)
|
|
665
|
+
* Format: ->relay:Target [thread:xxx] message body
|
|
217
666
|
*/
|
|
218
667
|
parseSingleLineMessages(content) {
|
|
219
668
|
const lines = content.split('\n');
|
|
@@ -225,18 +674,53 @@ export class PtyWrapper extends EventEmitter {
|
|
|
225
674
|
const prefixIdx = line.indexOf(this.relayPrefix);
|
|
226
675
|
if (prefixIdx === -1)
|
|
227
676
|
continue;
|
|
677
|
+
// Skip spawn/release commands - they are handled by parseSpawnReleaseCommands
|
|
678
|
+
const afterPrefixForCheck = line.substring(prefixIdx + this.relayPrefix.length);
|
|
679
|
+
if (/^spawn\s+/i.test(afterPrefixForCheck) || /^release\s+/i.test(afterPrefixForCheck)) {
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
228
682
|
// Extract everything after the prefix
|
|
229
683
|
const afterPrefix = line.substring(prefixIdx + this.relayPrefix.length);
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
684
|
+
// Pattern: Target [thread:project:id] body or Target [thread:id] body or Target body
|
|
685
|
+
// Thread is optional, can include project prefix
|
|
686
|
+
const targetMatch = afterPrefix.match(/^(\S+)(?:\s+\[thread:(?:([\w-]+):)?([\w-]+)\])?\s+(.+)$/);
|
|
687
|
+
if (!targetMatch) {
|
|
688
|
+
// Fallback: try simpler pattern without thread
|
|
689
|
+
const simpleMatch = afterPrefix.match(/^(\S+)\s+(.+)$/);
|
|
690
|
+
if (!simpleMatch)
|
|
691
|
+
continue;
|
|
692
|
+
const [, target, body] = simpleMatch;
|
|
693
|
+
if (!body)
|
|
694
|
+
continue;
|
|
695
|
+
// Skip placeholder targets (documentation examples)
|
|
696
|
+
if (isPlaceholderTarget(target))
|
|
697
|
+
continue;
|
|
698
|
+
// Parse target for cross-project syntax
|
|
699
|
+
const colonIdx = target.indexOf(':');
|
|
700
|
+
let to = target;
|
|
701
|
+
let project;
|
|
702
|
+
if (colonIdx > 0 && colonIdx < target.length - 1) {
|
|
703
|
+
project = target.substring(0, colonIdx);
|
|
704
|
+
to = target.substring(colonIdx + 1);
|
|
705
|
+
}
|
|
706
|
+
// Skip placeholder targets after parsing cross-project syntax
|
|
707
|
+
if (isPlaceholderTarget(to))
|
|
708
|
+
continue;
|
|
709
|
+
this.sendRelayCommand({
|
|
710
|
+
to,
|
|
711
|
+
kind: 'message',
|
|
712
|
+
body,
|
|
713
|
+
project,
|
|
714
|
+
raw: line,
|
|
715
|
+
});
|
|
233
716
|
continue;
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
const body = afterPrefix.substring(bodyStart).trim();
|
|
237
|
-
// Skip if no body
|
|
717
|
+
}
|
|
718
|
+
const [, target, threadProject, threadId, body] = targetMatch;
|
|
238
719
|
if (!body)
|
|
239
720
|
continue;
|
|
721
|
+
// Skip placeholder targets (documentation examples)
|
|
722
|
+
if (isPlaceholderTarget(target))
|
|
723
|
+
continue;
|
|
240
724
|
// Parse target for cross-project syntax
|
|
241
725
|
const colonIdx = target.indexOf(':');
|
|
242
726
|
let to = target;
|
|
@@ -245,80 +729,219 @@ export class PtyWrapper extends EventEmitter {
|
|
|
245
729
|
project = target.substring(0, colonIdx);
|
|
246
730
|
to = target.substring(colonIdx + 1);
|
|
247
731
|
}
|
|
732
|
+
// Skip placeholder targets after parsing cross-project syntax
|
|
733
|
+
if (isPlaceholderTarget(to))
|
|
734
|
+
continue;
|
|
248
735
|
this.sendRelayCommand({
|
|
249
736
|
to,
|
|
250
737
|
kind: 'message',
|
|
251
738
|
body,
|
|
252
739
|
project,
|
|
740
|
+
thread: threadId || undefined,
|
|
741
|
+
threadProject: threadProject || undefined,
|
|
253
742
|
raw: line,
|
|
254
743
|
});
|
|
255
744
|
}
|
|
256
745
|
}
|
|
257
|
-
/**
|
|
258
|
-
* Strip ANSI escape codes from string.
|
|
259
|
-
* Converts cursor movements to spaces to preserve visual layout.
|
|
260
|
-
*/
|
|
261
|
-
stripAnsi(str) {
|
|
262
|
-
// Convert cursor forward movements to spaces (CSI n C)
|
|
263
|
-
// \x1B[nC means move cursor right n columns
|
|
264
|
-
// eslint-disable-next-line no-control-regex
|
|
265
|
-
str = str.replace(/\x1B\[(\d+)C/g, (_m, n) => ' '.repeat(parseInt(n, 10) || 1));
|
|
266
|
-
// Convert single cursor right (CSI C) to space
|
|
267
|
-
// eslint-disable-next-line no-control-regex
|
|
268
|
-
str = str.replace(/\x1B\[C/g, ' ');
|
|
269
|
-
// Remove carriage returns (causes text overwriting issues)
|
|
270
|
-
str = str.replace(/\r(?!\n)/g, '');
|
|
271
|
-
// Strip remaining ANSI escape sequences
|
|
272
|
-
// eslint-disable-next-line no-control-regex
|
|
273
|
-
return str.replace(/\x1B(?:\[[0-9;]*[A-Za-z]|\].*?(?:\x07|\x1B\\)|[@-Z\\-_])/g, '');
|
|
274
|
-
}
|
|
275
746
|
/**
|
|
276
747
|
* Send relay command to daemon
|
|
277
748
|
*/
|
|
278
749
|
sendRelayCommand(cmd) {
|
|
279
750
|
const msgHash = `${cmd.to}:${cmd.body}`;
|
|
280
751
|
if (this.sentMessageHashes.has(msgHash)) {
|
|
752
|
+
console.log(`[pty:${this.config.name}] Skipping duplicate message to ${cmd.to}`);
|
|
281
753
|
return;
|
|
282
754
|
}
|
|
283
755
|
if (this.client.state !== 'READY') {
|
|
756
|
+
console.log(`[pty:${this.config.name}] Cannot send to ${cmd.to} - relay not ready (state: ${this.client.state})`);
|
|
284
757
|
return;
|
|
285
758
|
}
|
|
286
759
|
const success = this.client.sendMessage(cmd.to, cmd.body, cmd.kind, cmd.data, cmd.thread);
|
|
760
|
+
console.log(`[pty:${this.config.name}] Sent message to ${cmd.to}: ${success ? 'success' : 'failed'}`);
|
|
287
761
|
if (success) {
|
|
288
762
|
this.sentMessageHashes.add(msgHash);
|
|
763
|
+
// Dispatch message sent hook
|
|
764
|
+
this.hookRegistry.dispatchMessageSent(cmd.to, cmd.body, cmd.thread).catch(err => {
|
|
765
|
+
console.error(`[pty:${this.config.name}] Message sent hook error:`, err);
|
|
766
|
+
});
|
|
289
767
|
}
|
|
290
768
|
}
|
|
769
|
+
/** Valid CLI types for spawn commands */
|
|
770
|
+
static VALID_CLI_TYPES = new Set([
|
|
771
|
+
'claude', 'codex', 'gemini', 'droid', 'aider', 'cursor', 'cline', 'opencode',
|
|
772
|
+
]);
|
|
773
|
+
/** Validate agent name format (PascalCase, alphanumeric, 2-30 chars) */
|
|
774
|
+
isValidAgentName(name) {
|
|
775
|
+
// Must start with uppercase letter, contain only alphanumeric chars
|
|
776
|
+
// Length 2-30 characters
|
|
777
|
+
return /^[A-Z][a-zA-Z0-9]{1,29}$/.test(name);
|
|
778
|
+
}
|
|
779
|
+
/** Validate CLI type */
|
|
780
|
+
isValidCliType(cli) {
|
|
781
|
+
return PtyWrapper.VALID_CLI_TYPES.has(cli.toLowerCase());
|
|
782
|
+
}
|
|
291
783
|
/**
|
|
292
784
|
* Parse spawn/release commands from output
|
|
293
785
|
* Uses string-based parsing for robustness with PTY output.
|
|
786
|
+
* Supports two formats:
|
|
787
|
+
* Single-line: ->relay:spawn WorkerName cli "task description"
|
|
788
|
+
* Multi-line (fenced): ->relay:spawn WorkerName cli <<<
|
|
789
|
+
* task description here
|
|
790
|
+
* can span multiple lines>>>
|
|
294
791
|
* Delegates to dashboard API if dashboardPort is set (for nested spawns).
|
|
792
|
+
*
|
|
793
|
+
* STRICT VALIDATION:
|
|
794
|
+
* - Command must be at start of line (after whitespace)
|
|
795
|
+
* - Agent name must be PascalCase (e.g., Backend, Frontend, Worker1)
|
|
796
|
+
* - CLI must be a known type (claude, codex, gemini, etc.)
|
|
295
797
|
*/
|
|
296
798
|
parseSpawnReleaseCommands(content) {
|
|
297
799
|
// Need either API port or callbacks to handle spawn/release
|
|
298
|
-
|
|
800
|
+
// Also check allowSpawn config - spawned workers should not spawn other agents
|
|
801
|
+
const spawnAllowed = this.config.allowSpawn !== false;
|
|
802
|
+
const canSpawn = spawnAllowed && (this.config.dashboardPort || this.config.onSpawn);
|
|
299
803
|
const canRelease = this.config.dashboardPort || this.config.onRelease;
|
|
804
|
+
// Debug: always log spawn detection for debugging
|
|
805
|
+
if (content.includes('->relay:spawn')) {
|
|
806
|
+
console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] Spawn pattern detected in content`);
|
|
807
|
+
console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] canSpawn=${canSpawn} (allowSpawn=${spawnAllowed}, dashboardPort=${this.config.dashboardPort}, hasOnSpawn=${!!this.config.onSpawn})`);
|
|
808
|
+
// Log the actual lines containing spawn
|
|
809
|
+
const spawnLines = content.split('\n').filter(l => l.includes('->relay:spawn'));
|
|
810
|
+
spawnLines.forEach((line, i) => {
|
|
811
|
+
console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] Line ${i}: "${line.substring(0, 100)}"`);
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
// Debug: always log release detection for debugging
|
|
815
|
+
if (content.includes('->relay:release')) {
|
|
816
|
+
console.log(`[pty:${this.config.name}] [RELEASE-DEBUG] Release pattern detected in content`);
|
|
817
|
+
console.log(`[pty:${this.config.name}] [RELEASE-DEBUG] canRelease=${canRelease} (dashboardPort=${this.config.dashboardPort}, hasOnRelease=${!!this.config.onRelease})`);
|
|
818
|
+
}
|
|
300
819
|
if (!canSpawn && !canRelease)
|
|
301
820
|
return;
|
|
302
821
|
const lines = content.split('\n');
|
|
303
822
|
const spawnPrefix = '->relay:spawn';
|
|
304
823
|
const releasePrefix = '->relay:release';
|
|
305
824
|
for (const line of lines) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
825
|
+
let trimmed = line.trim();
|
|
826
|
+
// Strip bullet/prompt prefixes but PRESERVE the ->relay: pattern
|
|
827
|
+
// Look for ->relay: in the line and only strip what comes before it
|
|
828
|
+
const relayIdx = trimmed.indexOf('->relay:');
|
|
829
|
+
if (relayIdx > 0) {
|
|
830
|
+
// There's content before ->relay: - check if it's just prefix chars
|
|
831
|
+
const beforeRelay = trimmed.substring(0, relayIdx);
|
|
832
|
+
// Only strip if the prefix is just bullets/prompts/whitespace
|
|
833
|
+
if (/^[\s●•◦‣⁃⏺◆◇○□■│┃┆┇┊┋╎╏✦→➜›»$%#*]+$/.test(beforeRelay)) {
|
|
834
|
+
const originalTrimmed = trimmed;
|
|
835
|
+
trimmed = trimmed.substring(relayIdx);
|
|
836
|
+
console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] Stripped prefix: "${originalTrimmed.substring(0, 60)}" -> "${trimmed.substring(0, 60)}"`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
// Skip escaped commands: \->relay:spawn should not trigger
|
|
840
|
+
if (trimmed.includes('\\->relay:')) {
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
// If we're in fenced spawn mode, accumulate lines until we see >>>
|
|
844
|
+
if (this.pendingFencedSpawn) {
|
|
845
|
+
const closeIdx = trimmed.indexOf('>>>');
|
|
846
|
+
if (closeIdx !== -1) {
|
|
847
|
+
// Add content before >>> to task
|
|
848
|
+
const contentBeforeClose = trimmed.substring(0, closeIdx);
|
|
849
|
+
if (contentBeforeClose) {
|
|
850
|
+
this.pendingFencedSpawn.taskLines.push(contentBeforeClose);
|
|
851
|
+
}
|
|
852
|
+
// Execute the spawn with accumulated task
|
|
853
|
+
const { name, cli, taskLines } = this.pendingFencedSpawn;
|
|
854
|
+
const taskStr = taskLines.join('\n').trim();
|
|
855
|
+
const spawnKey = `${name}:${cli}`;
|
|
856
|
+
if (!this.processedSpawnCommands.has(spawnKey)) {
|
|
857
|
+
this.processedSpawnCommands.add(spawnKey);
|
|
858
|
+
console.log(`[pty:${this.config.name}] Spawn command (fenced): ${name} (${cli}) - "${taskStr.substring(0, 50)}..."`);
|
|
859
|
+
this.executeSpawn(name, cli, taskStr);
|
|
860
|
+
}
|
|
861
|
+
this.pendingFencedSpawn = null;
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
// Accumulate line as part of task
|
|
865
|
+
this.pendingFencedSpawn.taskLines.push(line);
|
|
866
|
+
}
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
// Check for fenced spawn start: ->relay:spawn Name [cli] <<<
|
|
870
|
+
// STRICT: Must be at start of line (after whitespace)
|
|
871
|
+
if (canSpawn && trimmed.startsWith(spawnPrefix)) {
|
|
872
|
+
const afterSpawn = trimmed.substring(spawnPrefix.length).trim();
|
|
873
|
+
console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] Detected spawn prefix, afterSpawn: "${afterSpawn.substring(0, 60)}"`);
|
|
874
|
+
// Check for fenced format: Name [cli] <<< (CLI optional, defaults to 'claude')
|
|
875
|
+
const fencedMatch = afterSpawn.match(/^(\S+)(?:\s+(\S+))?\s+<<<(.*)$/);
|
|
876
|
+
console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] Fenced match result: ${fencedMatch ? 'MATCHED' : 'NO MATCH'}`);
|
|
877
|
+
if (fencedMatch) {
|
|
878
|
+
const [, name, cliOrUndefined, inlineContent] = fencedMatch;
|
|
879
|
+
let cli = cliOrUndefined || 'claude';
|
|
880
|
+
// STRICT: Validate agent name (PascalCase) and CLI type
|
|
881
|
+
if (!this.isValidAgentName(name)) {
|
|
882
|
+
console.warn(`[pty:${this.config.name}] Invalid agent name format, skipping: name=${name} (must be PascalCase)`);
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
if (!this.isValidCliType(cli)) {
|
|
886
|
+
console.warn(`[pty:${this.config.name}] Unknown CLI type, using default: cli=${cli}`);
|
|
887
|
+
cli = 'claude';
|
|
888
|
+
}
|
|
889
|
+
// Check if fence closes on same line
|
|
890
|
+
const inlineCloseIdx = inlineContent.indexOf('>>>');
|
|
891
|
+
if (inlineCloseIdx !== -1) {
|
|
892
|
+
// Single line fenced: extract task between <<< and >>>
|
|
893
|
+
const taskStr = inlineContent.substring(0, inlineCloseIdx).trim();
|
|
894
|
+
const spawnKey = `${name}:${cli}`;
|
|
895
|
+
if (!this.processedSpawnCommands.has(spawnKey)) {
|
|
896
|
+
this.processedSpawnCommands.add(spawnKey);
|
|
897
|
+
console.log(`[pty:${this.config.name}] Spawn command (fenced): ${name} (${cli}) - "${taskStr.substring(0, 50)}..."`);
|
|
898
|
+
this.executeSpawn(name, cli, taskStr);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
// Start multi-line fenced mode - but only if not already processed
|
|
903
|
+
const spawnKey = `${name}:${cli}`;
|
|
904
|
+
if (this.processedSpawnCommands.has(spawnKey)) {
|
|
905
|
+
// Already processed this spawn, skip the fenced capture
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
this.pendingFencedSpawn = {
|
|
909
|
+
name,
|
|
910
|
+
cli,
|
|
911
|
+
taskLines: inlineContent.trim() ? [inlineContent.trim()] : [],
|
|
912
|
+
};
|
|
913
|
+
console.log(`[pty:${this.config.name}] Starting fenced spawn capture: ${name} (${cli})`);
|
|
914
|
+
}
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
// Parse single-line format: WorkerName [cli] [task]
|
|
918
|
+
// CLI defaults to 'claude' if not provided
|
|
311
919
|
const parts = afterSpawn.split(/\s+/);
|
|
312
|
-
if (parts.length >=
|
|
920
|
+
if (parts.length >= 1) {
|
|
313
921
|
const name = parts[0];
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
922
|
+
// CLI is optional - defaults to 'claude'
|
|
923
|
+
let cli = parts[1] || 'claude';
|
|
924
|
+
// Task is everything after cli (if cli was provided) or after name (if cli was omitted)
|
|
925
|
+
let task = '';
|
|
926
|
+
const taskStartIndex = parts[1] ? 2 : 1;
|
|
927
|
+
if (parts.length > taskStartIndex) {
|
|
928
|
+
const taskPart = parts.slice(taskStartIndex).join(' ');
|
|
929
|
+
// Remove surrounding quotes if present
|
|
930
|
+
const quoteMatch = taskPart.match(/^["'](.*)["']$/);
|
|
931
|
+
task = quoteMatch ? quoteMatch[1] : taskPart;
|
|
932
|
+
}
|
|
933
|
+
if (name) {
|
|
934
|
+
// STRICT: Validate agent name (PascalCase) and CLI type
|
|
935
|
+
if (!this.isValidAgentName(name)) {
|
|
936
|
+
// Don't log warning for documentation text - just silently skip
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
if (!this.isValidCliType(cli)) {
|
|
940
|
+
// Default CLI 'claude' should always be valid, but validate anyway
|
|
941
|
+
console.warn(`[pty:${this.config.name}] Unknown CLI type, using default: cli=${cli}, defaulting to 'claude'`);
|
|
942
|
+
cli = 'claude';
|
|
943
|
+
}
|
|
944
|
+
const spawnKey = `${name}:${cli}`;
|
|
322
945
|
if (!this.processedSpawnCommands.has(spawnKey)) {
|
|
323
946
|
this.processedSpawnCommands.add(spawnKey);
|
|
324
947
|
this.executeSpawn(name, cli, task);
|
|
@@ -328,13 +951,18 @@ export class PtyWrapper extends EventEmitter {
|
|
|
328
951
|
continue;
|
|
329
952
|
}
|
|
330
953
|
// Check for release command
|
|
331
|
-
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
this.
|
|
954
|
+
// STRICT: Must be at start of line (after whitespace)
|
|
955
|
+
if (trimmed.startsWith(releasePrefix)) {
|
|
956
|
+
console.log(`[pty:${this.config.name}] [RELEASE-DEBUG] Release prefix detected, canRelease=${canRelease}`);
|
|
957
|
+
if (canRelease) {
|
|
958
|
+
const afterRelease = trimmed.substring(releasePrefix.length).trim();
|
|
959
|
+
const name = afterRelease.split(/\s+/)[0];
|
|
960
|
+
console.log(`[pty:${this.config.name}] [RELEASE-DEBUG] Parsed name: ${name}, isValidName=${name ? this.isValidAgentName(name) : false}, alreadyProcessed=${this.processedReleaseCommands.has(name)}`);
|
|
961
|
+
// STRICT: Validate agent name format
|
|
962
|
+
if (name && this.isValidAgentName(name) && !this.processedReleaseCommands.has(name)) {
|
|
963
|
+
this.processedReleaseCommands.add(name);
|
|
964
|
+
this.executeRelease(name);
|
|
965
|
+
}
|
|
338
966
|
}
|
|
339
967
|
}
|
|
340
968
|
}
|
|
@@ -343,6 +971,8 @@ export class PtyWrapper extends EventEmitter {
|
|
|
343
971
|
* Execute spawn via API or callback
|
|
344
972
|
*/
|
|
345
973
|
async executeSpawn(name, cli, task) {
|
|
974
|
+
console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] executeSpawn called: name=${name}, cli=${cli}, task="${task.substring(0, 50)}..."`);
|
|
975
|
+
console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] dashboardPort=${this.config.dashboardPort}, hasOnSpawn=${!!this.config.onSpawn}`);
|
|
346
976
|
if (this.config.dashboardPort) {
|
|
347
977
|
// Use dashboard API for spawning (works from spawned agents)
|
|
348
978
|
try {
|
|
@@ -380,7 +1010,7 @@ export class PtyWrapper extends EventEmitter {
|
|
|
380
1010
|
if (this.config.dashboardPort) {
|
|
381
1011
|
// Use dashboard API for releasing
|
|
382
1012
|
try {
|
|
383
|
-
const response = await fetch(`http://localhost:${this.config.dashboardPort}/api/spawned/${name}`, {
|
|
1013
|
+
const response = await fetch(`http://localhost:${this.config.dashboardPort}/api/spawned/${encodeURIComponent(name)}`, {
|
|
384
1014
|
method: 'DELETE',
|
|
385
1015
|
});
|
|
386
1016
|
const result = await response.json();
|
|
@@ -407,19 +1037,82 @@ export class PtyWrapper extends EventEmitter {
|
|
|
407
1037
|
}
|
|
408
1038
|
/**
|
|
409
1039
|
* Handle incoming message from relay
|
|
1040
|
+
* @param originalTo - The original 'to' field from sender. '*' indicates this was a broadcast message.
|
|
410
1041
|
*/
|
|
411
|
-
handleIncomingMessage(from, payload, messageId) {
|
|
412
|
-
|
|
1042
|
+
handleIncomingMessage(from, payload, messageId, meta, originalTo) {
|
|
1043
|
+
// Deduplicate: skip if we've already received this message
|
|
1044
|
+
if (this.receivedMessageIds.has(messageId)) {
|
|
1045
|
+
console.log(`[pty:${this.config.name}] Skipping duplicate message: ${messageId.substring(0, 8)}`);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
this.receivedMessageIds.add(messageId);
|
|
1049
|
+
// Limit dedup set size to prevent memory leak
|
|
1050
|
+
if (this.receivedMessageIds.size > 1000) {
|
|
1051
|
+
const oldest = this.receivedMessageIds.values().next().value;
|
|
1052
|
+
if (oldest)
|
|
1053
|
+
this.receivedMessageIds.delete(oldest);
|
|
1054
|
+
}
|
|
1055
|
+
this.messageQueue.push({ from, body: payload.body, messageId, thread: payload.thread, importance: meta?.importance, data: payload.data, originalTo });
|
|
413
1056
|
this.processMessageQueue();
|
|
1057
|
+
// Dispatch message received hook
|
|
1058
|
+
this.hookRegistry.dispatchMessageReceived(from, payload.body, messageId).catch(err => {
|
|
1059
|
+
console.error(`[pty:${this.config.name}] Message received hook error:`, err);
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Wait for output to stabilize before injection.
|
|
1064
|
+
* Returns true if output has been stable for the required duration.
|
|
1065
|
+
*/
|
|
1066
|
+
async waitForOutputStable() {
|
|
1067
|
+
const startTime = Date.now();
|
|
1068
|
+
let stablePolls = 0;
|
|
1069
|
+
let lastBufferLength = this.rawBuffer.length;
|
|
1070
|
+
while (Date.now() - startTime < INJECTION_CONSTANTS.STABILITY_TIMEOUT_MS) {
|
|
1071
|
+
await sleep(INJECTION_CONSTANTS.STABILITY_POLL_MS);
|
|
1072
|
+
const timeSinceOutput = Date.now() - this.lastOutputTime;
|
|
1073
|
+
const bufferUnchanged = this.rawBuffer.length === lastBufferLength;
|
|
1074
|
+
// Consider stable if no output for at least one poll interval
|
|
1075
|
+
if (timeSinceOutput >= INJECTION_CONSTANTS.STABILITY_POLL_MS && bufferUnchanged) {
|
|
1076
|
+
stablePolls++;
|
|
1077
|
+
if (stablePolls >= INJECTION_CONSTANTS.REQUIRED_STABLE_POLLS) {
|
|
1078
|
+
return true;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
stablePolls = 0;
|
|
1083
|
+
lastBufferLength = this.rawBuffer.length;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
// Timeout - return true anyway to avoid blocking forever
|
|
1087
|
+
console.warn(`[pty:${this.config.name}] Stability timeout, proceeding with injection`);
|
|
1088
|
+
return true;
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Check if the agent process is still alive and responsive.
|
|
1092
|
+
*/
|
|
1093
|
+
isAgentAlive() {
|
|
1094
|
+
return this.running && this.ptyProcess !== undefined;
|
|
414
1095
|
}
|
|
415
1096
|
/**
|
|
416
|
-
* Process queued messages
|
|
1097
|
+
* Process queued messages with reliability improvements:
|
|
1098
|
+
* 1. Wait for output stability before injection
|
|
1099
|
+
* 2. Verify injection appeared in output
|
|
1100
|
+
* 3. Retry with backoff on failure
|
|
1101
|
+
* 4. Fall back to logging on complete failure
|
|
1102
|
+
*
|
|
1103
|
+
* Uses shared injection logic with PTY-specific callbacks.
|
|
417
1104
|
*/
|
|
418
1105
|
async processMessageQueue() {
|
|
1106
|
+
// Wait until instructions have been injected and agent is ready
|
|
1107
|
+
if (!this.readyForMessages)
|
|
1108
|
+
return;
|
|
419
1109
|
if (this.isInjecting || this.messageQueue.length === 0)
|
|
420
1110
|
return;
|
|
421
|
-
|
|
1111
|
+
// Health check: is agent still alive?
|
|
1112
|
+
if (!this.isAgentAlive()) {
|
|
1113
|
+
console.error(`[pty:${this.config.name}] Agent not alive, cannot inject messages`);
|
|
422
1114
|
return;
|
|
1115
|
+
}
|
|
423
1116
|
this.isInjecting = true;
|
|
424
1117
|
const msg = this.messageQueue.shift();
|
|
425
1118
|
if (!msg) {
|
|
@@ -427,14 +1120,68 @@ export class PtyWrapper extends EventEmitter {
|
|
|
427
1120
|
return;
|
|
428
1121
|
}
|
|
429
1122
|
try {
|
|
1123
|
+
// Wait for output to stabilize before injecting
|
|
1124
|
+
await this.waitForOutputStable();
|
|
1125
|
+
// For Gemini: check if at shell prompt, skip injection to avoid shell execution
|
|
1126
|
+
if (this.cliType === 'gemini') {
|
|
1127
|
+
const recentOutput = this.rawBuffer.slice(-200);
|
|
1128
|
+
const lastLine = recentOutput.split('\n').filter(l => l.trim()).pop() || '';
|
|
1129
|
+
if (CLI_QUIRKS.isShellPrompt(lastLine)) {
|
|
1130
|
+
console.log(`[pty:${this.config.name}] Gemini at shell prompt, re-queuing message`);
|
|
1131
|
+
this.messageQueue.unshift(msg);
|
|
1132
|
+
this.isInjecting = false;
|
|
1133
|
+
setTimeout(() => this.processMessageQueue(), 2000);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
// Build injection string using shared utility
|
|
1138
|
+
let injection = buildInjectionString(msg);
|
|
1139
|
+
// Gemini-specific: wrap in backticks to prevent shell keyword interpretation
|
|
1140
|
+
if (this.cliType === 'gemini') {
|
|
1141
|
+
// Extract the message body part and wrap it
|
|
1142
|
+
const colonIdx = injection.indexOf(': ');
|
|
1143
|
+
if (colonIdx > 0) {
|
|
1144
|
+
const prefix = injection.substring(0, colonIdx + 2);
|
|
1145
|
+
const body = injection.substring(colonIdx + 2);
|
|
1146
|
+
injection = prefix + CLI_QUIRKS.wrapForGemini(body);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
430
1149
|
const shortId = msg.messageId.substring(0, 8);
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
1150
|
+
// Create callbacks for shared injection logic
|
|
1151
|
+
const callbacks = {
|
|
1152
|
+
getOutput: async () => {
|
|
1153
|
+
// Look at last 2000 chars to avoid scanning entire buffer
|
|
1154
|
+
return this.rawBuffer.slice(-2000);
|
|
1155
|
+
},
|
|
1156
|
+
performInjection: async (inj) => {
|
|
1157
|
+
if (!this.ptyProcess || !this.running) {
|
|
1158
|
+
throw new Error('PTY process not running');
|
|
1159
|
+
}
|
|
1160
|
+
// Write message to PTY, then send Enter separately after a small delay
|
|
1161
|
+
this.ptyProcess.write(inj);
|
|
1162
|
+
await sleep(INJECTION_CONSTANTS.ENTER_DELAY_MS);
|
|
1163
|
+
this.ptyProcess.write('\r');
|
|
1164
|
+
},
|
|
1165
|
+
log: (message) => console.log(`[pty:${this.config.name}] ${message}`),
|
|
1166
|
+
logError: (message) => console.error(`[pty:${this.config.name}] ${message}`),
|
|
1167
|
+
getMetrics: () => this.injectionMetrics,
|
|
1168
|
+
// Skip verification for PTY-based injection - CLIs don't echo input back
|
|
1169
|
+
// so verification will always fail. Trust that pty.write() succeeds.
|
|
1170
|
+
skipVerification: true,
|
|
1171
|
+
};
|
|
1172
|
+
// Inject with retry and verification using shared logic
|
|
1173
|
+
const result = await sharedInjectWithRetry(injection, shortId, msg.from, callbacks);
|
|
1174
|
+
if (!result.success) {
|
|
1175
|
+
// Log the failed message for debugging/recovery
|
|
1176
|
+
console.error(`[pty:${this.config.name}] Message delivery failed after ${result.attempts} attempts: ` +
|
|
1177
|
+
`from=${msg.from} id=${shortId}`);
|
|
1178
|
+
// Emit event for external monitoring (e.g., dashboard)
|
|
1179
|
+
this.emit('injection-failed', {
|
|
1180
|
+
messageId: msg.messageId,
|
|
1181
|
+
from: msg.from,
|
|
1182
|
+
attempts: result.attempts,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
438
1185
|
}
|
|
439
1186
|
catch (err) {
|
|
440
1187
|
console.error(`[pty:${this.config.name}] Injection failed: ${err.message}`);
|
|
@@ -443,24 +1190,34 @@ export class PtyWrapper extends EventEmitter {
|
|
|
443
1190
|
this.isInjecting = false;
|
|
444
1191
|
// Process next message if any
|
|
445
1192
|
if (this.messageQueue.length > 0) {
|
|
446
|
-
setTimeout(() => this.processMessageQueue(),
|
|
1193
|
+
setTimeout(() => this.processMessageQueue(), INJECTION_CONSTANTS.QUEUE_PROCESS_DELAY_MS);
|
|
447
1194
|
}
|
|
448
1195
|
}
|
|
449
1196
|
}
|
|
450
1197
|
/**
|
|
451
|
-
*
|
|
1198
|
+
* Queue minimal agent identity notification as the first message.
|
|
1199
|
+
*
|
|
1200
|
+
* Full protocol instructions are in ~/.claude/CLAUDE.md (set up by entrypoint.sh).
|
|
1201
|
+
* We only inject a brief identity message here to let the agent know its name
|
|
1202
|
+
* and that it's connected to the relay.
|
|
452
1203
|
*/
|
|
453
1204
|
injectInstructions() {
|
|
454
|
-
if (!this.running
|
|
1205
|
+
if (!this.running)
|
|
1206
|
+
return;
|
|
1207
|
+
// Guard: Only inject once per session
|
|
1208
|
+
if (this.instructionsInjected) {
|
|
1209
|
+
console.log(`[pty:${this.config.name}] Init instructions already injected, skipping`);
|
|
455
1210
|
return;
|
|
456
|
-
const escapedPrefix = '\\' + this.relayPrefix;
|
|
457
|
-
const instructions = `[Agent Relay] You are "${this.config.name}" - connected for real-time messaging. SEND: ${escapedPrefix}AgentName message`;
|
|
458
|
-
try {
|
|
459
|
-
this.ptyProcess.write(instructions + '\r');
|
|
460
|
-
}
|
|
461
|
-
catch {
|
|
462
|
-
// Silent fail
|
|
463
1211
|
}
|
|
1212
|
+
this.instructionsInjected = true;
|
|
1213
|
+
// Minimal notification - full protocol is in ~/.claude/CLAUDE.md
|
|
1214
|
+
const notification = `You are agent "${this.config.name}" connected to Agent Relay. See CLAUDE.md for the messaging protocol. ACK messages, do work, send DONE when complete.`;
|
|
1215
|
+
// Queue as first message from "system" - will be injected when CLI is ready
|
|
1216
|
+
this.messageQueue.unshift({
|
|
1217
|
+
from: 'system',
|
|
1218
|
+
body: notification,
|
|
1219
|
+
messageId: `init-${Date.now()}`,
|
|
1220
|
+
});
|
|
464
1221
|
}
|
|
465
1222
|
/**
|
|
466
1223
|
* Write directly to the PTY
|
|
@@ -488,38 +1245,73 @@ export class PtyWrapper extends EventEmitter {
|
|
|
488
1245
|
/**
|
|
489
1246
|
* Stop the agent process
|
|
490
1247
|
*/
|
|
491
|
-
stop() {
|
|
1248
|
+
async stop() {
|
|
492
1249
|
if (!this.running)
|
|
493
1250
|
return;
|
|
494
1251
|
this.running = false;
|
|
1252
|
+
// Auto-save continuity state before stopping
|
|
1253
|
+
// Pass sessionEndData to populate handoff (fixes empty handoff issue)
|
|
1254
|
+
if (this.continuity) {
|
|
1255
|
+
try {
|
|
1256
|
+
await this.continuity.autoSave(this.config.name, 'session_end', this.sessionEndData);
|
|
1257
|
+
}
|
|
1258
|
+
catch (err) {
|
|
1259
|
+
console.error(`[pty:${this.config.name}] Continuity auto-save failed:`, err);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
// Dispatch session end hook (handles trajectory completion)
|
|
1263
|
+
try {
|
|
1264
|
+
await this.hookRegistry.dispatchSessionEnd(0, true);
|
|
1265
|
+
}
|
|
1266
|
+
catch (err) {
|
|
1267
|
+
console.error(`[pty:${this.config.name}] Session end hook error:`, err);
|
|
1268
|
+
}
|
|
495
1269
|
if (this.ptyProcess) {
|
|
496
1270
|
// Try graceful termination first
|
|
497
1271
|
this.ptyProcess.write('\x03'); // Ctrl+C
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}, 1000);
|
|
1272
|
+
await sleep(1000);
|
|
1273
|
+
if (this.ptyProcess) {
|
|
1274
|
+
this.ptyProcess.kill();
|
|
1275
|
+
}
|
|
503
1276
|
}
|
|
504
1277
|
this.closeLogStream();
|
|
505
1278
|
this.client.destroy();
|
|
1279
|
+
this.hookRegistry.destroy();
|
|
506
1280
|
}
|
|
507
1281
|
/**
|
|
508
1282
|
* Kill the process immediately
|
|
509
1283
|
*/
|
|
510
|
-
kill() {
|
|
1284
|
+
async kill() {
|
|
511
1285
|
this.running = false;
|
|
1286
|
+
// Auto-save continuity state before killing (with timeout to avoid hanging)
|
|
1287
|
+
// Pass sessionEndData if available (may have been parsed before kill)
|
|
1288
|
+
if (this.continuity) {
|
|
1289
|
+
try {
|
|
1290
|
+
await Promise.race([
|
|
1291
|
+
this.continuity.autoSave(this.config.name, 'crash', this.sessionEndData),
|
|
1292
|
+
sleep(2000), // 2s timeout for crash saves
|
|
1293
|
+
]);
|
|
1294
|
+
}
|
|
1295
|
+
catch (err) {
|
|
1296
|
+
console.error(`[pty:${this.config.name}] Continuity auto-save failed:`, err);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
// Dispatch session end hook (forced termination, with timeout)
|
|
1300
|
+
try {
|
|
1301
|
+
await Promise.race([
|
|
1302
|
+
this.hookRegistry.dispatchSessionEnd(undefined, false),
|
|
1303
|
+
sleep(1000), // 1s timeout for hooks on kill
|
|
1304
|
+
]);
|
|
1305
|
+
}
|
|
1306
|
+
catch (err) {
|
|
1307
|
+
console.error(`[pty:${this.config.name}] Session end hook error:`, err);
|
|
1308
|
+
}
|
|
512
1309
|
if (this.ptyProcess) {
|
|
513
1310
|
this.ptyProcess.kill();
|
|
514
1311
|
}
|
|
515
1312
|
this.closeLogStream();
|
|
516
1313
|
this.client.destroy();
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Sleep helper
|
|
520
|
-
*/
|
|
521
|
-
sleep(ms) {
|
|
522
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1314
|
+
this.hookRegistry.destroy();
|
|
523
1315
|
}
|
|
524
1316
|
/**
|
|
525
1317
|
* Close the log file stream
|
|
@@ -543,5 +1335,159 @@ export class PtyWrapper extends EventEmitter {
|
|
|
543
1335
|
get logPath() {
|
|
544
1336
|
return this.logFilePath;
|
|
545
1337
|
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Track significant outputs and inject summary reminder if needed.
|
|
1340
|
+
* Works with any CLI (Claude, Gemini, Codex, etc.)
|
|
1341
|
+
*/
|
|
1342
|
+
trackOutputAndRemind(data) {
|
|
1343
|
+
// Disabled if config.summaryReminder === false or env RELAY_SUMMARY_REMINDER_ENABLED=false
|
|
1344
|
+
if (this.config.summaryReminder === false)
|
|
1345
|
+
return;
|
|
1346
|
+
if (process.env.RELAY_SUMMARY_REMINDER_ENABLED === 'false')
|
|
1347
|
+
return;
|
|
1348
|
+
const config = this.config.summaryReminder ?? {};
|
|
1349
|
+
// Env vars take precedence over config, config takes precedence over defaults
|
|
1350
|
+
const intervalMinutes = process.env.RELAY_SUMMARY_INTERVAL_MINUTES
|
|
1351
|
+
? parseInt(process.env.RELAY_SUMMARY_INTERVAL_MINUTES, 10)
|
|
1352
|
+
: (config.intervalMinutes ?? 15);
|
|
1353
|
+
const minOutputs = process.env.RELAY_SUMMARY_MIN_OUTPUTS
|
|
1354
|
+
? parseInt(process.env.RELAY_SUMMARY_MIN_OUTPUTS, 10)
|
|
1355
|
+
: (config.minOutputs ?? 50);
|
|
1356
|
+
// Only count "significant" outputs (more than just whitespace/control chars)
|
|
1357
|
+
const cleanData = stripAnsi(data).trim();
|
|
1358
|
+
if (cleanData.length > 20) {
|
|
1359
|
+
this.outputsSinceSummary++;
|
|
1360
|
+
}
|
|
1361
|
+
// Check if we should remind
|
|
1362
|
+
const minutesSinceSummary = (Date.now() - this.lastSummaryTime) / (1000 * 60);
|
|
1363
|
+
const shouldRemind = minutesSinceSummary >= intervalMinutes &&
|
|
1364
|
+
this.outputsSinceSummary >= minOutputs;
|
|
1365
|
+
if (shouldRemind && this.running && this.ptyProcess) {
|
|
1366
|
+
// Reset counters before injecting (prevent spam)
|
|
1367
|
+
this.lastSummaryTime = Date.now();
|
|
1368
|
+
this.outputsSinceSummary = 0;
|
|
1369
|
+
// Inject reminder as a relay-style message
|
|
1370
|
+
// IMPORTANT: Must be single-line - embedded newlines cause the message to span
|
|
1371
|
+
// multiple lines in the CLI input buffer, and the final Enter only submits
|
|
1372
|
+
// the last (empty) line. Regular relay messages are also single-line (see buildInjectionString).
|
|
1373
|
+
const reminder = `[Agent Relay] It's been ${Math.round(minutesSinceSummary)} minutes. Please output a [[SUMMARY]] block to checkpoint your progress: [[SUMMARY]]{"currentTask": "...", "completedTasks": [...], "context": "..."}[[/SUMMARY]]`;
|
|
1374
|
+
// Delay slightly to not interrupt current output, then write + Enter
|
|
1375
|
+
setTimeout(async () => {
|
|
1376
|
+
if (this.ptyProcess && this.running) {
|
|
1377
|
+
this.ptyProcess.write(reminder);
|
|
1378
|
+
await sleep(INJECTION_CONSTANTS.ENTER_DELAY_MS);
|
|
1379
|
+
this.ptyProcess.write('\r');
|
|
1380
|
+
}
|
|
1381
|
+
}, 1000);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Check for [[SUMMARY]] blocks and emit 'summary' event.
|
|
1386
|
+
* Allows cloud services to persist summaries without hardcoding storage.
|
|
1387
|
+
* Also updates the local continuity ledger for session recovery.
|
|
1388
|
+
*/
|
|
1389
|
+
checkForSummaryAndEmit(content) {
|
|
1390
|
+
const result = parseSummaryWithDetails(content);
|
|
1391
|
+
// No SUMMARY block found
|
|
1392
|
+
if (!result.found)
|
|
1393
|
+
return;
|
|
1394
|
+
// Dedup based on raw content - prevents repeated event emissions for same summary
|
|
1395
|
+
if (result.rawContent === this.lastSummaryRawContent)
|
|
1396
|
+
return;
|
|
1397
|
+
this.lastSummaryRawContent = result.rawContent || '';
|
|
1398
|
+
// Reset reminder counters on any summary (even invalid JSON)
|
|
1399
|
+
this.lastSummaryTime = Date.now();
|
|
1400
|
+
this.outputsSinceSummary = 0;
|
|
1401
|
+
// Invalid JSON - log warning
|
|
1402
|
+
if (!result.valid) {
|
|
1403
|
+
console.warn(`[pty:${this.config.name}] Invalid JSON in SUMMARY block`);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
const summary = result.summary;
|
|
1407
|
+
// Save to local continuity ledger for session recovery
|
|
1408
|
+
// This ensures the ledger has actual data instead of placeholders
|
|
1409
|
+
if (this.continuity) {
|
|
1410
|
+
this.saveSummaryToLedger(summary).catch(err => {
|
|
1411
|
+
console.error(`[pty:${this.config.name}] Failed to save summary to ledger:`, err);
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
// Emit event for external handlers (cloud services, dashboard, etc.)
|
|
1415
|
+
this.emit('summary', {
|
|
1416
|
+
agentName: this.config.name,
|
|
1417
|
+
summary,
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Save a parsed summary to the continuity ledger.
|
|
1422
|
+
* Maps summary fields to ledger fields for session recovery.
|
|
1423
|
+
*/
|
|
1424
|
+
async saveSummaryToLedger(summary) {
|
|
1425
|
+
if (!this.continuity)
|
|
1426
|
+
return;
|
|
1427
|
+
const updates = {};
|
|
1428
|
+
// Map summary fields to ledger fields
|
|
1429
|
+
if (summary.currentTask) {
|
|
1430
|
+
updates.currentTask = summary.currentTask;
|
|
1431
|
+
}
|
|
1432
|
+
if (summary.completedTasks && summary.completedTasks.length > 0) {
|
|
1433
|
+
updates.completed = summary.completedTasks;
|
|
1434
|
+
}
|
|
1435
|
+
if (summary.context) {
|
|
1436
|
+
// Store context in inProgress as "next steps" hint
|
|
1437
|
+
updates.inProgress = [summary.context];
|
|
1438
|
+
}
|
|
1439
|
+
if (summary.files && summary.files.length > 0) {
|
|
1440
|
+
updates.fileContext = summary.files.map((f) => ({ path: f }));
|
|
1441
|
+
}
|
|
1442
|
+
// Only save if we have meaningful updates
|
|
1443
|
+
if (Object.keys(updates).length > 0) {
|
|
1444
|
+
await this.continuity.saveLedger(this.config.name, updates);
|
|
1445
|
+
console.log(`[pty:${this.config.name}] Saved summary to continuity ledger`);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Check for [[SESSION_END]] blocks and emit 'session-end' event.
|
|
1450
|
+
* Allows cloud services to handle session closure without hardcoding storage.
|
|
1451
|
+
* Also stores the data for use in autoSave to populate handoff.
|
|
1452
|
+
*/
|
|
1453
|
+
checkForSessionEndAndEmit(content) {
|
|
1454
|
+
if (this.sessionEndProcessed)
|
|
1455
|
+
return; // Only emit once per session
|
|
1456
|
+
const sessionEnd = parseSessionEndFromOutput(content);
|
|
1457
|
+
if (!sessionEnd)
|
|
1458
|
+
return;
|
|
1459
|
+
this.sessionEndProcessed = true;
|
|
1460
|
+
// Store SESSION_END data for use in autoSave (fixes empty handoff issue)
|
|
1461
|
+
this.sessionEndData = sessionEnd;
|
|
1462
|
+
// Emit event for external handlers
|
|
1463
|
+
this.emit('session-end', {
|
|
1464
|
+
agentName: this.config.name,
|
|
1465
|
+
marker: sessionEnd,
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Reset session-specific state for wrapper reuse.
|
|
1470
|
+
* Call this when starting a new session with the same wrapper instance.
|
|
1471
|
+
*/
|
|
1472
|
+
resetSessionState() {
|
|
1473
|
+
this.sessionEndProcessed = false;
|
|
1474
|
+
this.lastSummaryRawContent = '';
|
|
1475
|
+
this.sessionEndData = undefined;
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Get injection reliability metrics
|
|
1479
|
+
*/
|
|
1480
|
+
getInjectionMetrics() {
|
|
1481
|
+
return {
|
|
1482
|
+
...this.injectionMetrics,
|
|
1483
|
+
successRate: calculateSuccessRate(this.injectionMetrics),
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Get count of pending messages in queue
|
|
1488
|
+
*/
|
|
1489
|
+
get pendingMessageCount() {
|
|
1490
|
+
return this.messageQueue.length;
|
|
1491
|
+
}
|
|
546
1492
|
}
|
|
547
1493
|
//# sourceMappingURL=pty-wrapper.js.map
|