@stoneforge/smithy 0.1.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/LICENSE +13 -0
- package/README.md +114 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +7 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/orchestrator-api.d.ts +153 -0
- package/dist/api/orchestrator-api.d.ts.map +1 -0
- package/dist/api/orchestrator-api.js +374 -0
- package/dist/api/orchestrator-api.js.map +1 -0
- package/dist/bin/sf.d.ts +3 -0
- package/dist/bin/sf.d.ts.map +1 -0
- package/dist/bin/sf.js +10 -0
- package/dist/bin/sf.js.map +1 -0
- package/dist/cli/commands/agent.d.ts +20 -0
- package/dist/cli/commands/agent.d.ts.map +1 -0
- package/dist/cli/commands/agent.js +861 -0
- package/dist/cli/commands/agent.js.map +1 -0
- package/dist/cli/commands/daemon.d.ts +14 -0
- package/dist/cli/commands/daemon.d.ts.map +1 -0
- package/dist/cli/commands/daemon.js +272 -0
- package/dist/cli/commands/daemon.js.map +1 -0
- package/dist/cli/commands/dispatch.d.ts +9 -0
- package/dist/cli/commands/dispatch.d.ts.map +1 -0
- package/dist/cli/commands/dispatch.js +128 -0
- package/dist/cli/commands/dispatch.js.map +1 -0
- package/dist/cli/commands/merge.d.ts +11 -0
- package/dist/cli/commands/merge.d.ts.map +1 -0
- package/dist/cli/commands/merge.js +246 -0
- package/dist/cli/commands/merge.js.map +1 -0
- package/dist/cli/commands/pool.d.ts +21 -0
- package/dist/cli/commands/pool.d.ts.map +1 -0
- package/dist/cli/commands/pool.js +762 -0
- package/dist/cli/commands/pool.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +54 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +57 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/task.d.ts +36 -0
- package/dist/cli/commands/task.d.ts.map +1 -0
- package/dist/cli/commands/task.js +889 -0
- package/dist/cli/commands/task.js.map +1 -0
- package/dist/cli/commands/test-orchestration.d.ts +32 -0
- package/dist/cli/commands/test-orchestration.d.ts.map +1 -0
- package/dist/cli/commands/test-orchestration.js +392 -0
- package/dist/cli/commands/test-orchestration.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +15 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/plugin.d.ts +23 -0
- package/dist/cli/plugin.d.ts.map +1 -0
- package/dist/cli/plugin.js +36 -0
- package/dist/cli/plugin.js.map +1 -0
- package/dist/git/index.d.ts +10 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/index.js +12 -0
- package/dist/git/index.js.map +1 -0
- package/dist/git/merge.d.ts +79 -0
- package/dist/git/merge.d.ts.map +1 -0
- package/dist/git/merge.js +254 -0
- package/dist/git/merge.js.map +1 -0
- package/dist/git/worktree-manager.d.ts +299 -0
- package/dist/git/worktree-manager.d.ts.map +1 -0
- package/dist/git/worktree-manager.js +744 -0
- package/dist/git/worktree-manager.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/director.md +272 -0
- package/dist/prompts/index.d.ts +100 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +294 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/message-triage.md +50 -0
- package/dist/prompts/persistent-worker.md +240 -0
- package/dist/prompts/steward-base.md +64 -0
- package/dist/prompts/steward-docs.md +118 -0
- package/dist/prompts/steward-health.md +39 -0
- package/dist/prompts/steward-merge.md +168 -0
- package/dist/prompts/steward-ops.md +28 -0
- package/dist/prompts/steward-reminder.md +26 -0
- package/dist/prompts/worker.md +282 -0
- package/dist/providers/claude/headless.d.ts +18 -0
- package/dist/providers/claude/headless.d.ts.map +1 -0
- package/dist/providers/claude/headless.js +307 -0
- package/dist/providers/claude/headless.js.map +1 -0
- package/dist/providers/claude/index.d.ts +24 -0
- package/dist/providers/claude/index.d.ts.map +1 -0
- package/dist/providers/claude/index.js +80 -0
- package/dist/providers/claude/index.js.map +1 -0
- package/dist/providers/claude/interactive.d.ts +21 -0
- package/dist/providers/claude/interactive.d.ts.map +1 -0
- package/dist/providers/claude/interactive.js +142 -0
- package/dist/providers/claude/interactive.js.map +1 -0
- package/dist/providers/codex/event-mapper.d.ts +91 -0
- package/dist/providers/codex/event-mapper.d.ts.map +1 -0
- package/dist/providers/codex/event-mapper.js +299 -0
- package/dist/providers/codex/event-mapper.js.map +1 -0
- package/dist/providers/codex/headless.d.ts +20 -0
- package/dist/providers/codex/headless.d.ts.map +1 -0
- package/dist/providers/codex/headless.js +174 -0
- package/dist/providers/codex/headless.js.map +1 -0
- package/dist/providers/codex/index.d.ts +30 -0
- package/dist/providers/codex/index.d.ts.map +1 -0
- package/dist/providers/codex/index.js +55 -0
- package/dist/providers/codex/index.js.map +1 -0
- package/dist/providers/codex/interactive.d.ts +21 -0
- package/dist/providers/codex/interactive.d.ts.map +1 -0
- package/dist/providers/codex/interactive.js +141 -0
- package/dist/providers/codex/interactive.js.map +1 -0
- package/dist/providers/codex/jsonrpc-client.d.ts +52 -0
- package/dist/providers/codex/jsonrpc-client.d.ts.map +1 -0
- package/dist/providers/codex/jsonrpc-client.js +141 -0
- package/dist/providers/codex/jsonrpc-client.js.map +1 -0
- package/dist/providers/codex/server-manager.d.ts +100 -0
- package/dist/providers/codex/server-manager.d.ts.map +1 -0
- package/dist/providers/codex/server-manager.js +153 -0
- package/dist/providers/codex/server-manager.js.map +1 -0
- package/dist/providers/index.d.ts +15 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +19 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/opencode/async-queue.d.ts +21 -0
- package/dist/providers/opencode/async-queue.d.ts.map +1 -0
- package/dist/providers/opencode/async-queue.js +51 -0
- package/dist/providers/opencode/async-queue.js.map +1 -0
- package/dist/providers/opencode/event-mapper.d.ts +132 -0
- package/dist/providers/opencode/event-mapper.d.ts.map +1 -0
- package/dist/providers/opencode/event-mapper.js +204 -0
- package/dist/providers/opencode/event-mapper.js.map +1 -0
- package/dist/providers/opencode/headless.d.ts +25 -0
- package/dist/providers/opencode/headless.d.ts.map +1 -0
- package/dist/providers/opencode/headless.js +190 -0
- package/dist/providers/opencode/headless.js.map +1 -0
- package/dist/providers/opencode/index.d.ts +33 -0
- package/dist/providers/opencode/index.d.ts.map +1 -0
- package/dist/providers/opencode/index.js +42 -0
- package/dist/providers/opencode/index.js.map +1 -0
- package/dist/providers/opencode/interactive.d.ts +21 -0
- package/dist/providers/opencode/interactive.d.ts.map +1 -0
- package/dist/providers/opencode/interactive.js +135 -0
- package/dist/providers/opencode/interactive.js.map +1 -0
- package/dist/providers/opencode/server-manager.d.ts +145 -0
- package/dist/providers/opencode/server-manager.d.ts.map +1 -0
- package/dist/providers/opencode/server-manager.js +163 -0
- package/dist/providers/opencode/server-manager.js.map +1 -0
- package/dist/providers/registry.d.ts +38 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +82 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/types.d.ts +144 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +25 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/runtime/event-utils.d.ts +8 -0
- package/dist/runtime/event-utils.d.ts.map +1 -0
- package/dist/runtime/event-utils.js +23 -0
- package/dist/runtime/event-utils.js.map +1 -0
- package/dist/runtime/handoff.d.ts +195 -0
- package/dist/runtime/handoff.d.ts.map +1 -0
- package/dist/runtime/handoff.js +332 -0
- package/dist/runtime/handoff.js.map +1 -0
- package/dist/runtime/index.d.ts +17 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +60 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/message-mapper.d.ts +99 -0
- package/dist/runtime/message-mapper.d.ts.map +1 -0
- package/dist/runtime/message-mapper.js +202 -0
- package/dist/runtime/message-mapper.js.map +1 -0
- package/dist/runtime/predecessor-query.d.ts +212 -0
- package/dist/runtime/predecessor-query.d.ts.map +1 -0
- package/dist/runtime/predecessor-query.js +283 -0
- package/dist/runtime/predecessor-query.js.map +1 -0
- package/dist/runtime/session-manager.d.ts +466 -0
- package/dist/runtime/session-manager.d.ts.map +1 -0
- package/dist/runtime/session-manager.js +986 -0
- package/dist/runtime/session-manager.js.map +1 -0
- package/dist/runtime/spawner.d.ts +407 -0
- package/dist/runtime/spawner.d.ts.map +1 -0
- package/dist/runtime/spawner.js +781 -0
- package/dist/runtime/spawner.js.map +1 -0
- package/dist/server/config.d.ts +22 -0
- package/dist/server/config.d.ts.map +1 -0
- package/dist/server/config.js +59 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/daemon-state.d.ts +50 -0
- package/dist/server/daemon-state.d.ts.map +1 -0
- package/dist/server/daemon-state.js +100 -0
- package/dist/server/daemon-state.js.map +1 -0
- package/dist/server/events-websocket.d.ts +32 -0
- package/dist/server/events-websocket.d.ts.map +1 -0
- package/dist/server/events-websocket.js +96 -0
- package/dist/server/events-websocket.js.map +1 -0
- package/dist/server/formatters.d.ts +94 -0
- package/dist/server/formatters.d.ts.map +1 -0
- package/dist/server/formatters.js +142 -0
- package/dist/server/formatters.js.map +1 -0
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +153 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/lsp-websocket.d.ts +33 -0
- package/dist/server/lsp-websocket.d.ts.map +1 -0
- package/dist/server/lsp-websocket.js +161 -0
- package/dist/server/lsp-websocket.js.map +1 -0
- package/dist/server/routes/agents.d.ts +9 -0
- package/dist/server/routes/agents.d.ts.map +1 -0
- package/dist/server/routes/agents.js +369 -0
- package/dist/server/routes/agents.js.map +1 -0
- package/dist/server/routes/daemon.d.ts +13 -0
- package/dist/server/routes/daemon.d.ts.map +1 -0
- package/dist/server/routes/daemon.js +187 -0
- package/dist/server/routes/daemon.js.map +1 -0
- package/dist/server/routes/events.d.ts +23 -0
- package/dist/server/routes/events.d.ts.map +1 -0
- package/dist/server/routes/events.js +282 -0
- package/dist/server/routes/events.js.map +1 -0
- package/dist/server/routes/extensions.d.ts +9 -0
- package/dist/server/routes/extensions.d.ts.map +1 -0
- package/dist/server/routes/extensions.js +202 -0
- package/dist/server/routes/extensions.js.map +1 -0
- package/dist/server/routes/health.d.ts +7 -0
- package/dist/server/routes/health.d.ts.map +1 -0
- package/dist/server/routes/health.js +33 -0
- package/dist/server/routes/health.js.map +1 -0
- package/dist/server/routes/index.d.ts +21 -0
- package/dist/server/routes/index.d.ts.map +1 -0
- package/dist/server/routes/index.js +21 -0
- package/dist/server/routes/index.js.map +1 -0
- package/dist/server/routes/lsp.d.ts +9 -0
- package/dist/server/routes/lsp.d.ts.map +1 -0
- package/dist/server/routes/lsp.js +50 -0
- package/dist/server/routes/lsp.js.map +1 -0
- package/dist/server/routes/plugins.d.ts +9 -0
- package/dist/server/routes/plugins.d.ts.map +1 -0
- package/dist/server/routes/plugins.js +109 -0
- package/dist/server/routes/plugins.js.map +1 -0
- package/dist/server/routes/pools.d.ts +9 -0
- package/dist/server/routes/pools.d.ts.map +1 -0
- package/dist/server/routes/pools.js +189 -0
- package/dist/server/routes/pools.js.map +1 -0
- package/dist/server/routes/scheduler.d.ts +9 -0
- package/dist/server/routes/scheduler.d.ts.map +1 -0
- package/dist/server/routes/scheduler.js +162 -0
- package/dist/server/routes/scheduler.js.map +1 -0
- package/dist/server/routes/sessions.d.ts +27 -0
- package/dist/server/routes/sessions.d.ts.map +1 -0
- package/dist/server/routes/sessions.js +773 -0
- package/dist/server/routes/sessions.js.map +1 -0
- package/dist/server/routes/tasks.d.ts +9 -0
- package/dist/server/routes/tasks.d.ts.map +1 -0
- package/dist/server/routes/tasks.js +954 -0
- package/dist/server/routes/tasks.js.map +1 -0
- package/dist/server/routes/upload.d.ts +8 -0
- package/dist/server/routes/upload.d.ts.map +1 -0
- package/dist/server/routes/upload.js +40 -0
- package/dist/server/routes/upload.js.map +1 -0
- package/dist/server/routes/workflows.d.ts +9 -0
- package/dist/server/routes/workflows.d.ts.map +1 -0
- package/dist/server/routes/workflows.js +532 -0
- package/dist/server/routes/workflows.js.map +1 -0
- package/dist/server/routes/workspace-files.d.ts +12 -0
- package/dist/server/routes/workspace-files.d.ts.map +1 -0
- package/dist/server/routes/workspace-files.js +520 -0
- package/dist/server/routes/workspace-files.js.map +1 -0
- package/dist/server/routes/worktrees.d.ts +9 -0
- package/dist/server/routes/worktrees.d.ts.map +1 -0
- package/dist/server/routes/worktrees.js +94 -0
- package/dist/server/routes/worktrees.js.map +1 -0
- package/dist/server/server.d.ts +14 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +258 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/services/lsp-manager.d.ts +93 -0
- package/dist/server/services/lsp-manager.d.ts.map +1 -0
- package/dist/server/services/lsp-manager.js +291 -0
- package/dist/server/services/lsp-manager.js.map +1 -0
- package/dist/server/services/session-messages.d.ts +61 -0
- package/dist/server/services/session-messages.d.ts.map +1 -0
- package/dist/server/services/session-messages.js +101 -0
- package/dist/server/services/session-messages.js.map +1 -0
- package/dist/server/services.d.ts +35 -0
- package/dist/server/services.d.ts.map +1 -0
- package/dist/server/services.js +159 -0
- package/dist/server/services.js.map +1 -0
- package/dist/server/static.d.ts +18 -0
- package/dist/server/static.d.ts.map +1 -0
- package/dist/server/static.js +71 -0
- package/dist/server/static.js.map +1 -0
- package/dist/server/types.d.ts +20 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +7 -0
- package/dist/server/types.js.map +1 -0
- package/dist/server/websocket.d.ts +16 -0
- package/dist/server/websocket.d.ts.map +1 -0
- package/dist/server/websocket.js +143 -0
- package/dist/server/websocket.js.map +1 -0
- package/dist/services/agent-pool-service.d.ts +181 -0
- package/dist/services/agent-pool-service.d.ts.map +1 -0
- package/dist/services/agent-pool-service.js +590 -0
- package/dist/services/agent-pool-service.js.map +1 -0
- package/dist/services/agent-registry.d.ts +185 -0
- package/dist/services/agent-registry.d.ts.map +1 -0
- package/dist/services/agent-registry.js +432 -0
- package/dist/services/agent-registry.js.map +1 -0
- package/dist/services/dispatch-daemon.d.ts +429 -0
- package/dist/services/dispatch-daemon.d.ts.map +1 -0
- package/dist/services/dispatch-daemon.js +1833 -0
- package/dist/services/dispatch-daemon.js.map +1 -0
- package/dist/services/dispatch-service.d.ts +148 -0
- package/dist/services/dispatch-service.d.ts.map +1 -0
- package/dist/services/dispatch-service.js +170 -0
- package/dist/services/dispatch-service.js.map +1 -0
- package/dist/services/docs-steward-service.d.ts +199 -0
- package/dist/services/docs-steward-service.d.ts.map +1 -0
- package/dist/services/docs-steward-service.js +599 -0
- package/dist/services/docs-steward-service.js.map +1 -0
- package/dist/services/health-steward-service.d.ts +446 -0
- package/dist/services/health-steward-service.d.ts.map +1 -0
- package/dist/services/health-steward-service.js +866 -0
- package/dist/services/health-steward-service.js.map +1 -0
- package/dist/services/index.d.ts +26 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +111 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/merge-request-provider.d.ts +59 -0
- package/dist/services/merge-request-provider.d.ts.map +1 -0
- package/dist/services/merge-request-provider.js +89 -0
- package/dist/services/merge-request-provider.js.map +1 -0
- package/dist/services/merge-steward-service.d.ts +268 -0
- package/dist/services/merge-steward-service.d.ts.map +1 -0
- package/dist/services/merge-steward-service.js +568 -0
- package/dist/services/merge-steward-service.js.map +1 -0
- package/dist/services/plugin-executor.d.ts +247 -0
- package/dist/services/plugin-executor.d.ts.map +1 -0
- package/dist/services/plugin-executor.js +451 -0
- package/dist/services/plugin-executor.js.map +1 -0
- package/dist/services/role-definition-service.d.ts +117 -0
- package/dist/services/role-definition-service.d.ts.map +1 -0
- package/dist/services/role-definition-service.js +289 -0
- package/dist/services/role-definition-service.js.map +1 -0
- package/dist/services/steward-scheduler.d.ts +336 -0
- package/dist/services/steward-scheduler.d.ts.map +1 -0
- package/dist/services/steward-scheduler.js +732 -0
- package/dist/services/steward-scheduler.js.map +1 -0
- package/dist/services/task-assignment-service.d.ts +291 -0
- package/dist/services/task-assignment-service.d.ts.map +1 -0
- package/dist/services/task-assignment-service.js +454 -0
- package/dist/services/task-assignment-service.js.map +1 -0
- package/dist/services/worker-task-service.d.ts +202 -0
- package/dist/services/worker-task-service.d.ts.map +1 -0
- package/dist/services/worker-task-service.js +228 -0
- package/dist/services/worker-task-service.js.map +1 -0
- package/dist/testing/index.d.ts +13 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +17 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/orchestration-tests.d.ts +62 -0
- package/dist/testing/orchestration-tests.d.ts.map +1 -0
- package/dist/testing/orchestration-tests.js +1115 -0
- package/dist/testing/orchestration-tests.js.map +1 -0
- package/dist/testing/test-context.d.ts +171 -0
- package/dist/testing/test-context.d.ts.map +1 -0
- package/dist/testing/test-context.js +665 -0
- package/dist/testing/test-context.js.map +1 -0
- package/dist/testing/test-prompts.d.ts +46 -0
- package/dist/testing/test-prompts.d.ts.map +1 -0
- package/dist/testing/test-prompts.js +140 -0
- package/dist/testing/test-prompts.js.map +1 -0
- package/dist/testing/test-utils.d.ts +200 -0
- package/dist/testing/test-utils.d.ts.map +1 -0
- package/dist/testing/test-utils.js +378 -0
- package/dist/testing/test-utils.js.map +1 -0
- package/dist/types/agent-pool.d.ts +215 -0
- package/dist/types/agent-pool.d.ts.map +1 -0
- package/dist/types/agent-pool.js +143 -0
- package/dist/types/agent-pool.js.map +1 -0
- package/dist/types/agent.d.ts +265 -0
- package/dist/types/agent.d.ts.map +1 -0
- package/dist/types/agent.js +127 -0
- package/dist/types/agent.js.map +1 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +40 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/message-types.d.ts +294 -0
- package/dist/types/message-types.d.ts.map +1 -0
- package/dist/types/message-types.js +354 -0
- package/dist/types/message-types.js.map +1 -0
- package/dist/types/role-definition.d.ts +272 -0
- package/dist/types/role-definition.d.ts.map +1 -0
- package/dist/types/role-definition.js +144 -0
- package/dist/types/role-definition.js.map +1 -0
- package/dist/types/task-meta.d.ts +248 -0
- package/dist/types/task-meta.d.ts.map +1 -0
- package/dist/types/task-meta.js +213 -0
- package/dist/types/task-meta.js.map +1 -0
- package/package.json +120 -0
- package/web/assets/abap-BrgZPUOV.js +6 -0
- package/web/assets/apex-DyP6w7ZV.js +6 -0
- package/web/assets/azcli-BaLxmfj-.js +6 -0
- package/web/assets/bat-CFOPXBzS.js +6 -0
- package/web/assets/bicep-BfEKNvv3.js +7 -0
- package/web/assets/cameligo-BFG1Mk7z.js +6 -0
- package/web/assets/clojure-DTECt2xU.js +6 -0
- package/web/assets/codicon-DCmgc-ay.ttf +0 -0
- package/web/assets/coffee-CDGzqUPQ.js +6 -0
- package/web/assets/cpp-CLLBncYj.js +6 -0
- package/web/assets/csharp-dUCx_-0o.js +6 -0
- package/web/assets/csp-5Rap-vPy.js +6 -0
- package/web/assets/css-D3h14YRZ.js +8 -0
- package/web/assets/cssMode-DMo-5YLA.js +9 -0
- package/web/assets/cypher-DrQuvNYM.js +6 -0
- package/web/assets/dart-CFKIUWau.js +6 -0
- package/web/assets/dockerfile-Zznr-cwX.js +6 -0
- package/web/assets/ecl-Ce3n6wWz.js +6 -0
- package/web/assets/elixir-deUWdS0T.js +6 -0
- package/web/assets/flow9-i9-g7ZhI.js +6 -0
- package/web/assets/freemarker2-D4qgkQzN.js +8 -0
- package/web/assets/fsharp-CzKuDChf.js +6 -0
- package/web/assets/go-Cphgjts3.js +6 -0
- package/web/assets/graphql-Cg7bfA9N.js +6 -0
- package/web/assets/handlebars-CXFvNjQC.js +6 -0
- package/web/assets/hcl-0cvrggvQ.js +6 -0
- package/web/assets/html-oyuB_D-B.js +6 -0
- package/web/assets/htmlMode-iWuZ24-r.js +9 -0
- package/web/assets/index-DqP-_E4F.css +32 -0
- package/web/assets/index-R1cylSgw.js +1665 -0
- package/web/assets/ini-Drc7WvVn.js +6 -0
- package/web/assets/java-B_fMsGYe.js +6 -0
- package/web/assets/javascript-CRIkN2Pg.js +6 -0
- package/web/assets/jsonMode-DVDkDgex.js +15 -0
- package/web/assets/julia-Bqgm2twL.js +6 -0
- package/web/assets/kotlin-BSkB5QuD.js +6 -0
- package/web/assets/less-BsTHnhdd.js +7 -0
- package/web/assets/lexon-YWi4-JPR.js +6 -0
- package/web/assets/liquid-CSfldbB5.js +6 -0
- package/web/assets/lua-nf6ki56Z.js +6 -0
- package/web/assets/m3-Cpb6xl2v.js +6 -0
- package/web/assets/markdown-DSZPf7rp.js +6 -0
- package/web/assets/mdx-Dd58iymR.js +6 -0
- package/web/assets/mips-B_c3zf-v.js +6 -0
- package/web/assets/monaco-editor-B4lwqA13.js +751 -0
- package/web/assets/monaco-editor-CQpyCxOA.css +1 -0
- package/web/assets/msdax-rUNN04Wq.js +6 -0
- package/web/assets/mysql-DDwshQtU.js +6 -0
- package/web/assets/objective-c-B5zXfXm9.js +6 -0
- package/web/assets/pascal-CXOwvkN_.js +6 -0
- package/web/assets/pascaligo-Bc-ZgV77.js +6 -0
- package/web/assets/perl-CwNk8-XU.js +6 -0
- package/web/assets/pgsql-tGk8EFnU.js +6 -0
- package/web/assets/php-CpIb_Oan.js +6 -0
- package/web/assets/pla-B03wrqEc.js +6 -0
- package/web/assets/postiats-BKlk5iyT.js +6 -0
- package/web/assets/powerquery-Bhzvs7bI.js +6 -0
- package/web/assets/powershell-Dd3NCNK9.js +6 -0
- package/web/assets/protobuf-COyEY5Pt.js +7 -0
- package/web/assets/pug-BaJupSGV.js +6 -0
- package/web/assets/python-XWrMqdhO.js +6 -0
- package/web/assets/qsharp-DXyYeYxl.js +6 -0
- package/web/assets/r-CdQndTaG.js +6 -0
- package/web/assets/razor-DPlhCpIs.js +6 -0
- package/web/assets/redis-CVwtpugi.js +6 -0
- package/web/assets/redshift-25W9uPmb.js +6 -0
- package/web/assets/restructuredtext-DfzH4Xui.js +6 -0
- package/web/assets/router-vendor-DHlGizSU.js +41 -0
- package/web/assets/ruby-Cp1zYvxS.js +6 -0
- package/web/assets/rust-D5C2fndG.js +6 -0
- package/web/assets/sb-CDntyWJ8.js +6 -0
- package/web/assets/scala-BoFRg7Ot.js +6 -0
- package/web/assets/scheme-Bio4gycK.js +6 -0
- package/web/assets/scss-4Ik7cdeQ.js +8 -0
- package/web/assets/shell-CX-rkNHf.js +6 -0
- package/web/assets/solidity-Tw7wswEv.js +6 -0
- package/web/assets/sophia-C5WLch3f.js +6 -0
- package/web/assets/sparql-DHaeiCBh.js +6 -0
- package/web/assets/sql-CCSDG5nI.js +6 -0
- package/web/assets/st-pnP8ivHi.js +6 -0
- package/web/assets/swift-DwJ7jVG9.js +8 -0
- package/web/assets/systemverilog-B9Xyijhd.js +6 -0
- package/web/assets/tcl-DnHyzjbg.js +6 -0
- package/web/assets/tsMode-BbA1Jbf3.js +16 -0
- package/web/assets/twig-CPajHgWi.js +6 -0
- package/web/assets/typescript-DcLHYzvH.js +6 -0
- package/web/assets/typespec-D-MeaMDU.js +6 -0
- package/web/assets/ui-vendor-BSco96uv.js +51 -0
- package/web/assets/utils-vendor-DaJ2Dubl.js +911 -0
- package/web/assets/vb-DgyLZaXg.js +6 -0
- package/web/assets/wgsl-DYQUnd45.js +303 -0
- package/web/assets/xml-xKYS3dO6.js +6 -0
- package/web/assets/yaml-CNmlXqzH.js +6 -0
- package/web/favicon.ico +0 -0
- package/web/index.html +22 -0
- package/web/logo.png +0 -0
|
@@ -0,0 +1,986 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager Service
|
|
3
|
+
*
|
|
4
|
+
* This service manages agent sessions with provider session ID support for
|
|
5
|
+
* resumable sessions and cross-restart persistence.
|
|
6
|
+
*
|
|
7
|
+
* Key features:
|
|
8
|
+
* - Track active sessions with process metadata
|
|
9
|
+
* - Persist session state to database via Agent entity metadata
|
|
10
|
+
* - Start, resume, stop, and suspend sessions
|
|
11
|
+
* - Message running sessions via agent channels
|
|
12
|
+
* - Query session history per agent
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import { EventEmitter } from 'node:events';
|
|
17
|
+
import { createTimestamp } from '@stoneforge/core';
|
|
18
|
+
import { getAgentMetadata } from '../api/orchestrator-api.js';
|
|
19
|
+
import { getProviderRegistry } from '../providers/registry.js';
|
|
20
|
+
import { trackListeners } from './event-utils.js';
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Session Manager Implementation
|
|
23
|
+
// ============================================================================
|
|
24
|
+
/**
|
|
25
|
+
* Implementation of the Session Manager.
|
|
26
|
+
*/
|
|
27
|
+
export class SessionManagerImpl {
|
|
28
|
+
spawner;
|
|
29
|
+
api;
|
|
30
|
+
registry;
|
|
31
|
+
sessions = new Map();
|
|
32
|
+
agentSessions = new Map(); // agentId -> active sessionId
|
|
33
|
+
sessionHistory = new Map();
|
|
34
|
+
sessionCleanupFns = new Map(); // sessionId -> cleanup function
|
|
35
|
+
constructor(spawner, api, // Used for message operations in messageSession
|
|
36
|
+
registry) {
|
|
37
|
+
this.spawner = spawner;
|
|
38
|
+
this.api = api;
|
|
39
|
+
this.registry = registry;
|
|
40
|
+
// Subscribe to spawner events to track session state
|
|
41
|
+
this.setupSpawnerEventHandlers();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Gets the API instance for direct operations
|
|
45
|
+
*/
|
|
46
|
+
getApi() {
|
|
47
|
+
return this.api;
|
|
48
|
+
}
|
|
49
|
+
// ----------------------------------------
|
|
50
|
+
// Session Lifecycle
|
|
51
|
+
// ----------------------------------------
|
|
52
|
+
async startSession(agentId, options) {
|
|
53
|
+
// Get agent to determine role and mode
|
|
54
|
+
const agent = await this.registry.getAgent(agentId);
|
|
55
|
+
if (!agent) {
|
|
56
|
+
throw new Error(`Agent not found: ${agentId}`);
|
|
57
|
+
}
|
|
58
|
+
const meta = getAgentMetadata(agent);
|
|
59
|
+
if (!meta) {
|
|
60
|
+
throw new Error(`Entity is not a valid agent: ${agentId}`);
|
|
61
|
+
}
|
|
62
|
+
// Check if agent already has an active session
|
|
63
|
+
const existingSessionId = this.agentSessions.get(agentId);
|
|
64
|
+
if (existingSessionId) {
|
|
65
|
+
const existingSession = this.sessions.get(existingSessionId);
|
|
66
|
+
if (existingSession && existingSession.status === 'running') {
|
|
67
|
+
throw new Error(`Agent ${agentId} already has an active session: ${existingSessionId}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Determine interactive mode based on agent role if not explicitly specified
|
|
71
|
+
// Directors and persistent workers use interactive mode (PTY)
|
|
72
|
+
// Ephemeral workers and stewards use headless mode (stream-json)
|
|
73
|
+
const isInteractiveByRole = meta.agentRole === 'director' ||
|
|
74
|
+
(meta.agentRole === 'worker' && meta.workerMode === 'persistent');
|
|
75
|
+
const useInteractive = options?.interactive ?? isInteractiveByRole;
|
|
76
|
+
// Resolve provider from agent metadata
|
|
77
|
+
const providerName = meta.provider;
|
|
78
|
+
let providerOverride;
|
|
79
|
+
if (providerName && providerName !== 'claude') {
|
|
80
|
+
const registry = getProviderRegistry();
|
|
81
|
+
providerOverride = await registry.getOrThrow(providerName);
|
|
82
|
+
}
|
|
83
|
+
// Resolve model: use options override, or fall back to agent metadata
|
|
84
|
+
const modelOverride = options?.model ?? meta.model;
|
|
85
|
+
// Build spawn options
|
|
86
|
+
const spawnOptions = {
|
|
87
|
+
workingDirectory: options?.workingDirectory,
|
|
88
|
+
initialPrompt: options?.initialPrompt,
|
|
89
|
+
environmentVariables: options?.environmentVariables,
|
|
90
|
+
mode: useInteractive ? 'interactive' : 'headless',
|
|
91
|
+
cols: options?.cols,
|
|
92
|
+
rows: options?.rows,
|
|
93
|
+
provider: providerOverride,
|
|
94
|
+
model: modelOverride,
|
|
95
|
+
};
|
|
96
|
+
console.log('[session-manager] Starting session for agent', agentId, 'mode:', spawnOptions.mode, 'provider:', providerName ?? 'claude', 'model:', modelOverride ?? 'default', 'prompt length:', options?.initialPrompt?.length ?? 0);
|
|
97
|
+
// Spawn the session
|
|
98
|
+
const result = await this.spawner.spawn(agentId, meta.agentRole, spawnOptions);
|
|
99
|
+
// Create internal session state
|
|
100
|
+
const sessionState = this.createSessionState(result, agentId, meta, options);
|
|
101
|
+
// Track the session
|
|
102
|
+
this.sessions.set(sessionState.id, sessionState);
|
|
103
|
+
this.agentSessions.set(agentId, sessionState.id);
|
|
104
|
+
// Forward events from spawner BEFORE any awaits to avoid missing
|
|
105
|
+
// early exit events (e.g. when provider uses `exec` and the process
|
|
106
|
+
// terminates before the awaits below complete).
|
|
107
|
+
this.setupSessionEventForwarding(sessionState, result.events);
|
|
108
|
+
// Update agent's session status in database
|
|
109
|
+
await this.registry.updateAgentSession(agentId, result.session.providerSessionId, 'running');
|
|
110
|
+
// Persist session state
|
|
111
|
+
await this.persistSession(sessionState.id);
|
|
112
|
+
return {
|
|
113
|
+
session: this.toPublicSession(sessionState),
|
|
114
|
+
events: sessionState.events,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async resumeSession(agentId, options) {
|
|
118
|
+
// Get agent to determine role and mode
|
|
119
|
+
const agent = await this.registry.getAgent(agentId);
|
|
120
|
+
if (!agent) {
|
|
121
|
+
throw new Error(`Agent not found: ${agentId}`);
|
|
122
|
+
}
|
|
123
|
+
const meta = getAgentMetadata(agent);
|
|
124
|
+
if (!meta) {
|
|
125
|
+
throw new Error(`Entity is not a valid agent: ${agentId}`);
|
|
126
|
+
}
|
|
127
|
+
// Check if agent already has an active session
|
|
128
|
+
const existingSessionId = this.agentSessions.get(agentId);
|
|
129
|
+
if (existingSessionId) {
|
|
130
|
+
const existingSession = this.sessions.get(existingSessionId);
|
|
131
|
+
if (existingSession && existingSession.status === 'running') {
|
|
132
|
+
throw new Error(`Agent ${agentId} already has an active session: ${existingSessionId}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Perform UWP check before resuming (if enabled)
|
|
136
|
+
// This implements the Universal Work Principle: check queue before continuing previous context
|
|
137
|
+
let uwpCheck;
|
|
138
|
+
const shouldCheckQueue = options.checkReadyQueue !== false; // Default to true
|
|
139
|
+
if (shouldCheckQueue && options.getReadyTasks) {
|
|
140
|
+
const readyTasks = await options.getReadyTasks(agentId, 1);
|
|
141
|
+
if (readyTasks.length > 0) {
|
|
142
|
+
const task = readyTasks[0];
|
|
143
|
+
uwpCheck = {
|
|
144
|
+
hasReadyTask: true,
|
|
145
|
+
taskId: task.id,
|
|
146
|
+
taskTitle: task.title,
|
|
147
|
+
taskPriority: task.priority,
|
|
148
|
+
shouldProcessFirst: true,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
uwpCheck = {
|
|
153
|
+
hasReadyTask: false,
|
|
154
|
+
shouldProcessFirst: false,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Build the resume prompt, prepending task instructions if UWP found a task
|
|
159
|
+
let effectivePrompt = options.resumePrompt;
|
|
160
|
+
if (uwpCheck?.hasReadyTask && uwpCheck.shouldProcessFirst) {
|
|
161
|
+
const taskInstructions = this.buildUWPTaskPrompt(uwpCheck);
|
|
162
|
+
effectivePrompt = effectivePrompt
|
|
163
|
+
? `${taskInstructions}\n\n${effectivePrompt}`
|
|
164
|
+
: taskInstructions;
|
|
165
|
+
}
|
|
166
|
+
// Look up the original session's working directory from history if not provided
|
|
167
|
+
let workingDirectory = options.workingDirectory;
|
|
168
|
+
if (!workingDirectory) {
|
|
169
|
+
const history = await this.getSessionHistory(agentId, 20);
|
|
170
|
+
const previousSession = history.find(h => h.providerSessionId === options.providerSessionId);
|
|
171
|
+
if (previousSession?.workingDirectory) {
|
|
172
|
+
workingDirectory = previousSession.workingDirectory;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Resolve provider from agent metadata
|
|
176
|
+
const providerName = meta.provider;
|
|
177
|
+
let providerOverride;
|
|
178
|
+
if (providerName && providerName !== 'claude') {
|
|
179
|
+
const registry = getProviderRegistry();
|
|
180
|
+
providerOverride = await registry.getOrThrow(providerName);
|
|
181
|
+
}
|
|
182
|
+
// Resolve model from agent metadata (resume doesn't allow model override)
|
|
183
|
+
const modelFromMeta = meta.model;
|
|
184
|
+
// Build spawn options with resume
|
|
185
|
+
const spawnOptions = {
|
|
186
|
+
workingDirectory,
|
|
187
|
+
resumeSessionId: options.providerSessionId,
|
|
188
|
+
initialPrompt: effectivePrompt,
|
|
189
|
+
provider: providerOverride,
|
|
190
|
+
model: modelFromMeta,
|
|
191
|
+
};
|
|
192
|
+
// Spawn the session with resume
|
|
193
|
+
const result = await this.spawner.spawn(agentId, meta.agentRole, spawnOptions);
|
|
194
|
+
// Create internal session state
|
|
195
|
+
const sessionState = this.createSessionState(result, agentId, meta, {
|
|
196
|
+
workingDirectory: options.workingDirectory,
|
|
197
|
+
worktree: options.worktree,
|
|
198
|
+
});
|
|
199
|
+
// Track the session
|
|
200
|
+
this.sessions.set(sessionState.id, sessionState);
|
|
201
|
+
this.agentSessions.set(agentId, sessionState.id);
|
|
202
|
+
// Forward events from spawner BEFORE any awaits to avoid missing
|
|
203
|
+
// early exit events (e.g. when provider uses `exec` and the process
|
|
204
|
+
// terminates before the awaits below complete).
|
|
205
|
+
this.setupSessionEventForwarding(sessionState, result.events);
|
|
206
|
+
// Update agent's session status in database
|
|
207
|
+
await this.registry.updateAgentSession(agentId, options.providerSessionId, 'running');
|
|
208
|
+
// Persist session state
|
|
209
|
+
await this.persistSession(sessionState.id);
|
|
210
|
+
return {
|
|
211
|
+
session: this.toPublicSession(sessionState),
|
|
212
|
+
events: sessionState.events,
|
|
213
|
+
uwpCheck,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
async stopSession(sessionId, options) {
|
|
217
|
+
const session = this.sessions.get(sessionId);
|
|
218
|
+
if (!session) {
|
|
219
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
220
|
+
}
|
|
221
|
+
// Clean up event listeners to prevent leaks
|
|
222
|
+
this.cleanupSessionEventListeners(sessionId);
|
|
223
|
+
// Update session state BEFORE terminating to prevent race with exit event handler
|
|
224
|
+
const updatedSession = {
|
|
225
|
+
...session,
|
|
226
|
+
status: 'terminated',
|
|
227
|
+
endedAt: createTimestamp(),
|
|
228
|
+
terminationReason: options?.reason,
|
|
229
|
+
persisted: false,
|
|
230
|
+
};
|
|
231
|
+
this.sessions.set(sessionId, updatedSession);
|
|
232
|
+
// Clear active session for agent
|
|
233
|
+
if (this.agentSessions.get(session.agentId) === sessionId) {
|
|
234
|
+
this.agentSessions.delete(session.agentId);
|
|
235
|
+
}
|
|
236
|
+
// Add to history (do this before terminate to avoid race with exit handler)
|
|
237
|
+
this.addToHistory(session.agentId, updatedSession);
|
|
238
|
+
// Now terminate via spawner (may trigger exit event, but status already terminated)
|
|
239
|
+
await this.spawner.terminate(sessionId, options?.graceful ?? true);
|
|
240
|
+
// Update agent's session status in database
|
|
241
|
+
await this.registry.updateAgentSession(session.agentId, undefined, 'idle');
|
|
242
|
+
// Persist session state
|
|
243
|
+
await this.persistSession(sessionId);
|
|
244
|
+
// Schedule cleanup of terminated session from memory (M-1)
|
|
245
|
+
this.scheduleTerminatedSessionCleanup(sessionId);
|
|
246
|
+
// Emit status change
|
|
247
|
+
session.events.emit('status', 'terminated');
|
|
248
|
+
}
|
|
249
|
+
async interruptSession(sessionId) {
|
|
250
|
+
const session = this.sessions.get(sessionId);
|
|
251
|
+
if (!session) {
|
|
252
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
253
|
+
}
|
|
254
|
+
if (session.status !== 'running') {
|
|
255
|
+
throw new Error(`Cannot interrupt session in status: ${session.status}`);
|
|
256
|
+
}
|
|
257
|
+
// Interrupt via spawner
|
|
258
|
+
await this.spawner.interrupt(sessionId);
|
|
259
|
+
// Emit interrupt event
|
|
260
|
+
session.events.emit('interrupt');
|
|
261
|
+
}
|
|
262
|
+
async suspendSession(sessionId, reason) {
|
|
263
|
+
const session = this.sessions.get(sessionId);
|
|
264
|
+
if (!session) {
|
|
265
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
266
|
+
}
|
|
267
|
+
// Clean up event listeners to prevent leaks
|
|
268
|
+
this.cleanupSessionEventListeners(sessionId);
|
|
269
|
+
// Update session state BEFORE suspending to prevent race with exit event handler
|
|
270
|
+
const updatedSession = {
|
|
271
|
+
...session,
|
|
272
|
+
status: 'suspended',
|
|
273
|
+
endedAt: createTimestamp(),
|
|
274
|
+
terminationReason: reason,
|
|
275
|
+
persisted: false,
|
|
276
|
+
};
|
|
277
|
+
this.sessions.set(sessionId, updatedSession);
|
|
278
|
+
// Clear active session for agent (but keep in sessions map for resume)
|
|
279
|
+
if (this.agentSessions.get(session.agentId) === sessionId) {
|
|
280
|
+
this.agentSessions.delete(session.agentId);
|
|
281
|
+
}
|
|
282
|
+
// Now suspend via spawner (may trigger exit event, but status already 'suspended')
|
|
283
|
+
try {
|
|
284
|
+
await this.spawner.suspend(sessionId);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
// Revert status on failure
|
|
288
|
+
this.sessions.set(sessionId, session);
|
|
289
|
+
if (this.agentSessions.get(session.agentId) !== sessionId) {
|
|
290
|
+
this.agentSessions.set(session.agentId, sessionId);
|
|
291
|
+
}
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
// Add to history
|
|
295
|
+
this.addToHistory(session.agentId, updatedSession);
|
|
296
|
+
// Update agent's session status in database
|
|
297
|
+
await this.registry.updateAgentSession(session.agentId, session.providerSessionId, 'suspended');
|
|
298
|
+
// Persist session state
|
|
299
|
+
await this.persistSession(sessionId);
|
|
300
|
+
// Emit status change
|
|
301
|
+
updatedSession.events.emit('status', 'suspended');
|
|
302
|
+
}
|
|
303
|
+
// ----------------------------------------
|
|
304
|
+
// Session Queries
|
|
305
|
+
// ----------------------------------------
|
|
306
|
+
getSession(sessionId) {
|
|
307
|
+
const session = this.sessions.get(sessionId);
|
|
308
|
+
return session ? this.toPublicSession(session) : undefined;
|
|
309
|
+
}
|
|
310
|
+
getActiveSession(agentId) {
|
|
311
|
+
const sessionId = this.agentSessions.get(agentId);
|
|
312
|
+
if (!sessionId) {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
const session = this.sessions.get(sessionId);
|
|
316
|
+
// Allow both 'starting' and 'running' status - 'starting' is needed for SSE
|
|
317
|
+
// connections that happen before the Claude CLI emits its init event
|
|
318
|
+
if (!session || (session.status !== 'running' && session.status !== 'starting')) {
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
// Validate that the process/session is actually alive
|
|
322
|
+
if (session.pid) {
|
|
323
|
+
// Interactive sessions: check OS process liveness
|
|
324
|
+
if (!this.isProcessAlive(session.pid)) {
|
|
325
|
+
this.cleanupDeadSession(session);
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
// Headless sessions (no PID): cross-reference with spawner
|
|
331
|
+
const spawnerSession = this.spawner.getSession(sessionId);
|
|
332
|
+
if (!spawnerSession || spawnerSession.status === 'terminated') {
|
|
333
|
+
this.cleanupDeadSession(session);
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return this.toPublicSession(session);
|
|
338
|
+
}
|
|
339
|
+
listSessions(filter) {
|
|
340
|
+
let sessions = Array.from(this.sessions.values());
|
|
341
|
+
// Validate liveness for sessions in active states.
|
|
342
|
+
// This catches sessions whose processes exited without the exit handler firing.
|
|
343
|
+
const activeStatuses = ['starting', 'running', 'terminating'];
|
|
344
|
+
let didCleanup = false;
|
|
345
|
+
for (const session of sessions) {
|
|
346
|
+
if (!activeStatuses.includes(session.status))
|
|
347
|
+
continue;
|
|
348
|
+
if (session.pid) {
|
|
349
|
+
// Interactive sessions: check if the OS process is still alive
|
|
350
|
+
if (!this.isProcessAlive(session.pid)) {
|
|
351
|
+
this.cleanupDeadSession(session);
|
|
352
|
+
didCleanup = true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
// Headless sessions (no PID): cross-reference with the spawner.
|
|
357
|
+
// If the spawner no longer tracks this session, the process has exited
|
|
358
|
+
// and the exit event was lost or already processed by the spawner.
|
|
359
|
+
const spawnerSession = this.spawner.getSession(session.id);
|
|
360
|
+
if (!spawnerSession || spawnerSession.status === 'terminated') {
|
|
361
|
+
this.cleanupDeadSession(session);
|
|
362
|
+
didCleanup = true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Re-read after potential cleanups
|
|
367
|
+
if (didCleanup) {
|
|
368
|
+
sessions = Array.from(this.sessions.values());
|
|
369
|
+
}
|
|
370
|
+
if (filter) {
|
|
371
|
+
if (filter.agentId !== undefined) {
|
|
372
|
+
sessions = sessions.filter((s) => s.agentId === filter.agentId);
|
|
373
|
+
}
|
|
374
|
+
if (filter.role !== undefined) {
|
|
375
|
+
sessions = sessions.filter((s) => s.agentRole === filter.role);
|
|
376
|
+
}
|
|
377
|
+
if (filter.status !== undefined) {
|
|
378
|
+
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
|
379
|
+
sessions = sessions.filter((s) => statuses.includes(s.status));
|
|
380
|
+
}
|
|
381
|
+
if (filter.startedAfter !== undefined) {
|
|
382
|
+
const afterTime = this.getTimestampMs(filter.startedAfter);
|
|
383
|
+
sessions = sessions.filter((s) => {
|
|
384
|
+
if (!s.startedAt)
|
|
385
|
+
return false;
|
|
386
|
+
return this.getTimestampMs(s.startedAt) >= afterTime;
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (filter.startedBefore !== undefined) {
|
|
390
|
+
const beforeTime = this.getTimestampMs(filter.startedBefore);
|
|
391
|
+
sessions = sessions.filter((s) => {
|
|
392
|
+
if (!s.startedAt)
|
|
393
|
+
return false;
|
|
394
|
+
return this.getTimestampMs(s.startedAt) <= beforeTime;
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (filter.resumable !== undefined) {
|
|
398
|
+
sessions = sessions.filter((s) => filter.resumable ? s.providerSessionId !== undefined : s.providerSessionId === undefined);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return sessions.map((s) => this.toPublicSession(s));
|
|
402
|
+
}
|
|
403
|
+
getMostRecentResumableSession(agentId) {
|
|
404
|
+
const agentSessions = Array.from(this.sessions.values())
|
|
405
|
+
.filter((s) => s.agentId === agentId && s.providerSessionId !== undefined)
|
|
406
|
+
.sort((a, b) => {
|
|
407
|
+
const aTime = this.getTimestampMs(a.createdAt);
|
|
408
|
+
const bTime = this.getTimestampMs(b.createdAt);
|
|
409
|
+
return bTime - aTime;
|
|
410
|
+
});
|
|
411
|
+
return agentSessions.length > 0 ? this.toPublicSession(agentSessions[0]) : undefined;
|
|
412
|
+
}
|
|
413
|
+
async getSessionHistory(agentId, limit = 10) {
|
|
414
|
+
// First, check in-memory history
|
|
415
|
+
const inMemoryHistory = this.sessionHistory.get(agentId) ?? [];
|
|
416
|
+
// Load from agent metadata if needed
|
|
417
|
+
const agent = await this.registry.getAgent(agentId);
|
|
418
|
+
if (!agent) {
|
|
419
|
+
return inMemoryHistory.slice(0, limit);
|
|
420
|
+
}
|
|
421
|
+
const meta = getAgentMetadata(agent);
|
|
422
|
+
if (!meta) {
|
|
423
|
+
return inMemoryHistory.slice(0, limit);
|
|
424
|
+
}
|
|
425
|
+
// Get persisted history from agent metadata
|
|
426
|
+
const persistedHistory = this.getPersistedHistory(agent);
|
|
427
|
+
// Merge and dedupe
|
|
428
|
+
const allHistory = this.mergeHistory(inMemoryHistory, persistedHistory);
|
|
429
|
+
return allHistory.slice(0, limit);
|
|
430
|
+
}
|
|
431
|
+
async getSessionHistoryByRole(role, limit = 10) {
|
|
432
|
+
// Get all agents with the specified role
|
|
433
|
+
const agents = await this.registry.getAgentsByRole(role);
|
|
434
|
+
// Collect all session history entries from all agents with this role
|
|
435
|
+
const allRoleHistory = [];
|
|
436
|
+
for (const agent of agents) {
|
|
437
|
+
// Agent.id is ElementId but we need EntityId for session history
|
|
438
|
+
const agentId = agent.id;
|
|
439
|
+
const agentHistory = await this.getSessionHistory(agentId, limit);
|
|
440
|
+
const agentMeta = getAgentMetadata(agent);
|
|
441
|
+
// Convert to role-based entries
|
|
442
|
+
for (const entry of agentHistory) {
|
|
443
|
+
allRoleHistory.push({
|
|
444
|
+
...entry,
|
|
445
|
+
role: agentMeta?.agentRole ?? role,
|
|
446
|
+
agentId: agentId,
|
|
447
|
+
agentName: agent.name,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// Sort by ended or started time, most recent first
|
|
452
|
+
allRoleHistory.sort((a, b) => {
|
|
453
|
+
const aTime = a.endedAt
|
|
454
|
+
? this.getTimestampMs(a.endedAt)
|
|
455
|
+
: a.startedAt
|
|
456
|
+
? this.getTimestampMs(a.startedAt)
|
|
457
|
+
: 0;
|
|
458
|
+
const bTime = b.endedAt
|
|
459
|
+
? this.getTimestampMs(b.endedAt)
|
|
460
|
+
: b.startedAt
|
|
461
|
+
? this.getTimestampMs(b.startedAt)
|
|
462
|
+
: 0;
|
|
463
|
+
return bTime - aTime;
|
|
464
|
+
});
|
|
465
|
+
return allRoleHistory.slice(0, limit);
|
|
466
|
+
}
|
|
467
|
+
async getPreviousSession(role) {
|
|
468
|
+
// Get session history for the role
|
|
469
|
+
const roleHistory = await this.getSessionHistoryByRole(role, 100);
|
|
470
|
+
// Find the most recent session that has ended (suspended or terminated)
|
|
471
|
+
const previousSession = roleHistory.find((entry) => entry.status === 'suspended' || entry.status === 'terminated');
|
|
472
|
+
return previousSession;
|
|
473
|
+
}
|
|
474
|
+
// ----------------------------------------
|
|
475
|
+
// Session Communication
|
|
476
|
+
// ----------------------------------------
|
|
477
|
+
async messageSession(sessionId, options) {
|
|
478
|
+
const session = this.sessions.get(sessionId);
|
|
479
|
+
if (!session) {
|
|
480
|
+
return { success: false, error: `Session not found: ${sessionId}` };
|
|
481
|
+
}
|
|
482
|
+
// Validate options
|
|
483
|
+
if (!options.contentRef && !options.content) {
|
|
484
|
+
return { success: false, error: 'Either contentRef or content must be provided' };
|
|
485
|
+
}
|
|
486
|
+
// Get agent's channel
|
|
487
|
+
const agent = await this.registry.getAgent(session.agentId);
|
|
488
|
+
if (!agent) {
|
|
489
|
+
return { success: false, error: `Agent not found: ${session.agentId}` };
|
|
490
|
+
}
|
|
491
|
+
const meta = getAgentMetadata(agent);
|
|
492
|
+
const channelId = meta?.channelId;
|
|
493
|
+
if (!channelId) {
|
|
494
|
+
return { success: false, error: `Agent has no channel: ${session.agentId}` };
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
// Resolve message content
|
|
498
|
+
let messageContent = options.content;
|
|
499
|
+
if (!messageContent && options.contentRef) {
|
|
500
|
+
const doc = await this.api.get(options.contentRef);
|
|
501
|
+
if (doc && 'content' in doc) {
|
|
502
|
+
messageContent = doc.content;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (!messageContent) {
|
|
506
|
+
return { success: false, error: 'Could not resolve message content' };
|
|
507
|
+
}
|
|
508
|
+
// Format the message with sender context
|
|
509
|
+
const senderId = options.senderId ?? 'system';
|
|
510
|
+
const formattedMessage = `[Message from ${senderId}]: ${messageContent}`;
|
|
511
|
+
// Forward to the running process via spawner
|
|
512
|
+
if (session.mode === 'interactive') {
|
|
513
|
+
// Write the message content first
|
|
514
|
+
await this.spawner.writeToPty(sessionId, formattedMessage);
|
|
515
|
+
// Wait for the terminal to process the pasted content before sending Enter.
|
|
516
|
+
// The carriage return must be sent as a separate message to ensure proper submission.
|
|
517
|
+
// A longer delay (1500ms) is needed to ensure large messages are fully received
|
|
518
|
+
// by the PTY before the Enter key is sent.
|
|
519
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
520
|
+
await this.spawner.writeToPty(sessionId, '\r');
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
await this.spawner.sendInput(sessionId, formattedMessage);
|
|
524
|
+
}
|
|
525
|
+
// Update session activity
|
|
526
|
+
const updatedSession = {
|
|
527
|
+
...session,
|
|
528
|
+
lastActivityAt: createTimestamp(),
|
|
529
|
+
persisted: false,
|
|
530
|
+
};
|
|
531
|
+
this.sessions.set(sessionId, updatedSession);
|
|
532
|
+
return { success: true };
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
536
|
+
return { success: false, error: errorMessage };
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
getEventEmitter(sessionId) {
|
|
540
|
+
const session = this.sessions.get(sessionId);
|
|
541
|
+
return session?.events;
|
|
542
|
+
}
|
|
543
|
+
// ----------------------------------------
|
|
544
|
+
// User Idle Tracking
|
|
545
|
+
// ----------------------------------------
|
|
546
|
+
recordUserInput(sessionId) {
|
|
547
|
+
const session = this.sessions.get(sessionId);
|
|
548
|
+
if (session) {
|
|
549
|
+
session.lastUserInputAt = Date.now();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
getSessionUserIdleMs(agentId) {
|
|
553
|
+
const sessionId = this.agentSessions.get(agentId);
|
|
554
|
+
if (!sessionId)
|
|
555
|
+
return undefined;
|
|
556
|
+
const session = this.sessions.get(sessionId);
|
|
557
|
+
if (!session || !session.lastUserInputAt)
|
|
558
|
+
return undefined;
|
|
559
|
+
return Date.now() - session.lastUserInputAt;
|
|
560
|
+
}
|
|
561
|
+
// ----------------------------------------
|
|
562
|
+
// Persistence
|
|
563
|
+
// ----------------------------------------
|
|
564
|
+
async persistSession(sessionId) {
|
|
565
|
+
const session = this.sessions.get(sessionId);
|
|
566
|
+
if (!session) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
// Get current agent state
|
|
570
|
+
const agent = await this.registry.getAgent(session.agentId);
|
|
571
|
+
if (!agent) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
// Get the current in-memory session history for this agent
|
|
575
|
+
const inMemoryHistory = this.sessionHistory.get(session.agentId) ?? [];
|
|
576
|
+
// Update agent metadata with session info and history
|
|
577
|
+
// This persists session history to database for cross-restart recovery
|
|
578
|
+
await this.registry.updateAgentMetadata(session.agentId, {
|
|
579
|
+
sessionId: session.providerSessionId,
|
|
580
|
+
sessionStatus: session.status === 'running' ? 'running' : session.status === 'suspended' ? 'suspended' : 'idle',
|
|
581
|
+
lastActivityAt: session.lastActivityAt,
|
|
582
|
+
// Persist session history (limited to 20 entries to avoid bloat)
|
|
583
|
+
sessionHistory: inMemoryHistory.slice(0, 20),
|
|
584
|
+
});
|
|
585
|
+
// Mark as persisted
|
|
586
|
+
const updatedSession = {
|
|
587
|
+
...session,
|
|
588
|
+
persisted: true,
|
|
589
|
+
};
|
|
590
|
+
this.sessions.set(sessionId, updatedSession);
|
|
591
|
+
}
|
|
592
|
+
async loadSessionState(agentId) {
|
|
593
|
+
const agent = await this.registry.getAgent(agentId);
|
|
594
|
+
if (!agent) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const meta = getAgentMetadata(agent);
|
|
598
|
+
if (!meta) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
// Load history from agent metadata
|
|
602
|
+
const persistedHistory = this.getPersistedHistory(agent);
|
|
603
|
+
if (persistedHistory.length > 0) {
|
|
604
|
+
this.sessionHistory.set(agentId, persistedHistory);
|
|
605
|
+
}
|
|
606
|
+
// Check if there's a suspended session that can be resumed
|
|
607
|
+
if (meta.sessionId && meta.sessionStatus === 'suspended') {
|
|
608
|
+
// Create a placeholder session record for the suspended session
|
|
609
|
+
const suspendedSession = persistedHistory.find((h) => h.providerSessionId === meta.sessionId && h.status === 'suspended');
|
|
610
|
+
if (suspendedSession) {
|
|
611
|
+
// Determine mode based on agent role - directors and persistent workers use interactive
|
|
612
|
+
const isInteractive = meta.agentRole === 'director' ||
|
|
613
|
+
(meta.agentRole === 'worker' && meta.workerMode === 'persistent');
|
|
614
|
+
const sessionState = {
|
|
615
|
+
id: suspendedSession.id,
|
|
616
|
+
providerSessionId: suspendedSession.providerSessionId,
|
|
617
|
+
agentId,
|
|
618
|
+
agentRole: meta.agentRole,
|
|
619
|
+
workerMode: meta.agentRole === 'worker' ? meta.workerMode : undefined,
|
|
620
|
+
mode: isInteractive ? 'interactive' : 'headless',
|
|
621
|
+
status: 'suspended',
|
|
622
|
+
workingDirectory: suspendedSession.workingDirectory,
|
|
623
|
+
worktree: suspendedSession.worktree,
|
|
624
|
+
createdAt: suspendedSession.startedAt ?? createTimestamp(),
|
|
625
|
+
startedAt: suspendedSession.startedAt,
|
|
626
|
+
lastActivityAt: suspendedSession.endedAt ?? createTimestamp(),
|
|
627
|
+
endedAt: suspendedSession.endedAt,
|
|
628
|
+
terminationReason: suspendedSession.terminationReason,
|
|
629
|
+
events: new EventEmitter(),
|
|
630
|
+
persisted: true,
|
|
631
|
+
};
|
|
632
|
+
this.sessions.set(sessionState.id, sessionState);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async reconcileOnStartup() {
|
|
637
|
+
let reconciled = 0;
|
|
638
|
+
const errors = [];
|
|
639
|
+
try {
|
|
640
|
+
// Find all agents that are marked as 'running' in the database
|
|
641
|
+
const agents = await this.registry.listAgents({ sessionStatus: 'running' });
|
|
642
|
+
for (const agent of agents) {
|
|
643
|
+
const agentId = agent.id;
|
|
644
|
+
const meta = getAgentMetadata(agent);
|
|
645
|
+
if (!meta)
|
|
646
|
+
continue;
|
|
647
|
+
// Check if there's a live in-memory session for this agent
|
|
648
|
+
const activeSessionId = this.agentSessions.get(agentId);
|
|
649
|
+
if (activeSessionId) {
|
|
650
|
+
const activeSession = this.sessions.get(activeSessionId);
|
|
651
|
+
if (activeSession && activeSession.status === 'running') {
|
|
652
|
+
// Session exists in memory and is running — check PID
|
|
653
|
+
if (activeSession.pid && this.isProcessAlive(activeSession.pid)) {
|
|
654
|
+
continue; // Process is alive, nothing to reconcile
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// No live session — this agent is stale, reset to idle
|
|
659
|
+
try {
|
|
660
|
+
await this.registry.updateAgentSession(agentId, undefined, 'idle');
|
|
661
|
+
reconciled++;
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
const msg = `Failed to reset agent ${agent.name} (${agentId}): ${error instanceof Error ? error.message : String(error)}`;
|
|
665
|
+
errors.push(msg);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
errors.push(`Failed to list agents: ${error instanceof Error ? error.message : String(error)}`);
|
|
671
|
+
}
|
|
672
|
+
return { reconciled, errors };
|
|
673
|
+
}
|
|
674
|
+
// ----------------------------------------
|
|
675
|
+
// Private Helpers
|
|
676
|
+
// ----------------------------------------
|
|
677
|
+
scheduleTerminatedSessionCleanup(sessionId) {
|
|
678
|
+
setTimeout(() => {
|
|
679
|
+
const session = this.sessions.get(sessionId);
|
|
680
|
+
if (session && session.status === 'terminated') {
|
|
681
|
+
this.sessions.delete(sessionId);
|
|
682
|
+
}
|
|
683
|
+
}, 5000);
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Checks whether a process with the given PID is still alive.
|
|
687
|
+
* Uses signal 0 which doesn't kill — just checks existence.
|
|
688
|
+
*/
|
|
689
|
+
isProcessAlive(pid) {
|
|
690
|
+
try {
|
|
691
|
+
process.kill(pid, 0);
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Cleans up a session whose process is no longer alive.
|
|
700
|
+
* Transitions it to 'terminated' and schedules memory cleanup.
|
|
701
|
+
*/
|
|
702
|
+
cleanupDeadSession(session) {
|
|
703
|
+
const updated = {
|
|
704
|
+
...session,
|
|
705
|
+
status: 'terminated',
|
|
706
|
+
endedAt: createTimestamp(),
|
|
707
|
+
terminationReason: 'Process no longer alive (PID check)',
|
|
708
|
+
persisted: false,
|
|
709
|
+
};
|
|
710
|
+
this.sessions.set(session.id, updated);
|
|
711
|
+
if (this.agentSessions.get(session.agentId) === session.id) {
|
|
712
|
+
this.agentSessions.delete(session.agentId);
|
|
713
|
+
}
|
|
714
|
+
this.addToHistory(session.agentId, updated);
|
|
715
|
+
this.registry.updateAgentSession(session.agentId, undefined, 'idle').catch(() => { });
|
|
716
|
+
this.persistSession(session.id).then(() => {
|
|
717
|
+
this.scheduleTerminatedSessionCleanup(session.id);
|
|
718
|
+
}).catch(() => { });
|
|
719
|
+
}
|
|
720
|
+
setupSpawnerEventHandlers() {
|
|
721
|
+
// Note: The spawner emits events per-session via the session's event emitter
|
|
722
|
+
// We don't need global event handlers here
|
|
723
|
+
}
|
|
724
|
+
setupSessionEventForwarding(session, spawnerEvents) {
|
|
725
|
+
// Create named handler functions so we can remove them later
|
|
726
|
+
const onEvent = (event) => {
|
|
727
|
+
const current = this.sessions.get(session.id);
|
|
728
|
+
if (!current)
|
|
729
|
+
return;
|
|
730
|
+
current.events.emit('event', event);
|
|
731
|
+
// Update last activity
|
|
732
|
+
this.sessions.set(session.id, {
|
|
733
|
+
...current,
|
|
734
|
+
lastActivityAt: createTimestamp(),
|
|
735
|
+
persisted: false,
|
|
736
|
+
});
|
|
737
|
+
};
|
|
738
|
+
const onPtyData = (data) => {
|
|
739
|
+
const current = this.sessions.get(session.id);
|
|
740
|
+
if (!current)
|
|
741
|
+
return;
|
|
742
|
+
current.events.emit('pty-data', data);
|
|
743
|
+
// Update last activity
|
|
744
|
+
this.sessions.set(session.id, {
|
|
745
|
+
...current,
|
|
746
|
+
lastActivityAt: createTimestamp(),
|
|
747
|
+
persisted: false,
|
|
748
|
+
});
|
|
749
|
+
};
|
|
750
|
+
const onError = (error) => {
|
|
751
|
+
const current = this.sessions.get(session.id);
|
|
752
|
+
if (!current)
|
|
753
|
+
return;
|
|
754
|
+
current.events.emit('error', error);
|
|
755
|
+
};
|
|
756
|
+
const onStderr = (data) => {
|
|
757
|
+
const current = this.sessions.get(session.id);
|
|
758
|
+
if (!current)
|
|
759
|
+
return;
|
|
760
|
+
current.events.emit('stderr', data);
|
|
761
|
+
};
|
|
762
|
+
const onRaw = (data) => {
|
|
763
|
+
const current = this.sessions.get(session.id);
|
|
764
|
+
if (!current)
|
|
765
|
+
return;
|
|
766
|
+
current.events.emit('raw', data);
|
|
767
|
+
};
|
|
768
|
+
const onExit = async (code, signal) => {
|
|
769
|
+
console.log(`[session-manager] Exit event received for session ${session.id}, agent ${session.agentId}, code=${code}, signal=${signal}`);
|
|
770
|
+
// Clean up all spawner event listeners immediately to prevent leaks
|
|
771
|
+
this.cleanupSessionEventListeners(session.id, spawnerEvents);
|
|
772
|
+
const exitingSession = this.sessions.get(session.id);
|
|
773
|
+
if (exitingSession) {
|
|
774
|
+
exitingSession.events.emit('exit', code, signal);
|
|
775
|
+
}
|
|
776
|
+
// Update session status if not already updated
|
|
777
|
+
const currentSession = this.sessions.get(session.id);
|
|
778
|
+
if (currentSession && currentSession.status !== 'terminated' && currentSession.status !== 'suspended') {
|
|
779
|
+
console.log(`[session-manager] Cleaning up ${currentSession.status} session ${session.id} for agent ${session.agentId}`);
|
|
780
|
+
const updatedSession = {
|
|
781
|
+
...currentSession,
|
|
782
|
+
status: 'terminated',
|
|
783
|
+
endedAt: createTimestamp(),
|
|
784
|
+
persisted: false,
|
|
785
|
+
};
|
|
786
|
+
this.sessions.set(session.id, updatedSession);
|
|
787
|
+
// Clear active session mapping
|
|
788
|
+
if (this.agentSessions.get(session.agentId) === session.id) {
|
|
789
|
+
this.agentSessions.delete(session.agentId);
|
|
790
|
+
console.log(`[session-manager] Cleared active session mapping for agent ${session.agentId}`);
|
|
791
|
+
}
|
|
792
|
+
// Add to history
|
|
793
|
+
this.addToHistory(session.agentId, updatedSession);
|
|
794
|
+
// Update agent status in registry to 'idle'
|
|
795
|
+
try {
|
|
796
|
+
await this.registry.updateAgentSession(session.agentId, undefined, 'idle');
|
|
797
|
+
console.log(`[session-manager] Updated agent ${session.agentId} status to idle in registry`);
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
console.error(`[session-manager] Failed to update agent ${session.agentId} status:`, error);
|
|
801
|
+
}
|
|
802
|
+
// Persist session state
|
|
803
|
+
try {
|
|
804
|
+
await this.persistSession(session.id);
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
console.error(`[session-manager] Failed to persist session ${session.id}:`, error);
|
|
808
|
+
}
|
|
809
|
+
// Schedule cleanup of terminated session from memory (M-1)
|
|
810
|
+
this.scheduleTerminatedSessionCleanup(session.id);
|
|
811
|
+
updatedSession.events.emit('status', 'terminated');
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
console.log(`[session-manager] Session ${session.id} already in status: ${currentSession?.status ?? 'not found'}`);
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
// Update providerSessionId when discovered by the spawner
|
|
818
|
+
const onProviderSessionId = (providerSessionId) => {
|
|
819
|
+
const current = this.sessions.get(session.id);
|
|
820
|
+
if (!current || current.providerSessionId)
|
|
821
|
+
return;
|
|
822
|
+
this.sessions.set(session.id, {
|
|
823
|
+
...current,
|
|
824
|
+
providerSessionId,
|
|
825
|
+
persisted: false,
|
|
826
|
+
});
|
|
827
|
+
// Persist session so providerSessionId survives a server restart
|
|
828
|
+
this.registry.updateAgentSession(session.agentId, providerSessionId, 'running').catch((err) => {
|
|
829
|
+
console.error(`[session-manager] Failed to persist providerSessionId for ${session.id}:`, err);
|
|
830
|
+
});
|
|
831
|
+
this.persistSession(session.id).catch((err) => {
|
|
832
|
+
console.error(`[session-manager] Failed to persist session after providerSessionId for ${session.id}:`, err);
|
|
833
|
+
});
|
|
834
|
+
};
|
|
835
|
+
// Attach all event listeners with tracked maxListeners
|
|
836
|
+
const cleanup = trackListeners(spawnerEvents, {
|
|
837
|
+
'event': onEvent,
|
|
838
|
+
'pty-data': onPtyData,
|
|
839
|
+
'error': onError,
|
|
840
|
+
'stderr': onStderr,
|
|
841
|
+
'raw': onRaw,
|
|
842
|
+
'exit': onExit,
|
|
843
|
+
'provider-session-id': onProviderSessionId,
|
|
844
|
+
});
|
|
845
|
+
this.sessionCleanupFns.set(session.id, cleanup);
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Cleans up event listeners for a session.
|
|
849
|
+
* Called when a session exits to prevent listener leaks.
|
|
850
|
+
*/
|
|
851
|
+
cleanupSessionEventListeners(sessionId, _spawnerEvents) {
|
|
852
|
+
const cleanup = this.sessionCleanupFns.get(sessionId);
|
|
853
|
+
if (cleanup) {
|
|
854
|
+
cleanup();
|
|
855
|
+
this.sessionCleanupFns.delete(sessionId);
|
|
856
|
+
console.log(`[session-manager] Cleaned up event listeners for session ${sessionId}`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
createSessionState(result, agentId, meta, options) {
|
|
860
|
+
return {
|
|
861
|
+
id: result.session.id,
|
|
862
|
+
providerSessionId: result.session.providerSessionId,
|
|
863
|
+
agentId,
|
|
864
|
+
agentRole: meta.agentRole,
|
|
865
|
+
workerMode: meta.agentRole === 'worker' ? meta.workerMode : undefined,
|
|
866
|
+
mode: result.session.mode,
|
|
867
|
+
pid: result.session.pid,
|
|
868
|
+
status: result.session.status,
|
|
869
|
+
workingDirectory: result.session.workingDirectory,
|
|
870
|
+
worktree: options?.worktree,
|
|
871
|
+
createdAt: result.session.createdAt,
|
|
872
|
+
startedAt: result.session.startedAt,
|
|
873
|
+
lastActivityAt: result.session.lastActivityAt,
|
|
874
|
+
events: new EventEmitter(),
|
|
875
|
+
persisted: false,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
toPublicSession(session) {
|
|
879
|
+
return {
|
|
880
|
+
id: session.id,
|
|
881
|
+
providerSessionId: session.providerSessionId,
|
|
882
|
+
agentId: session.agentId,
|
|
883
|
+
agentRole: session.agentRole,
|
|
884
|
+
workerMode: session.workerMode,
|
|
885
|
+
mode: session.mode,
|
|
886
|
+
pid: session.pid,
|
|
887
|
+
status: session.status,
|
|
888
|
+
workingDirectory: session.workingDirectory,
|
|
889
|
+
worktree: session.worktree,
|
|
890
|
+
createdAt: session.createdAt,
|
|
891
|
+
startedAt: session.startedAt,
|
|
892
|
+
lastActivityAt: session.lastActivityAt,
|
|
893
|
+
endedAt: session.endedAt,
|
|
894
|
+
terminationReason: session.terminationReason,
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
addToHistory(agentId, session) {
|
|
898
|
+
const historyEntry = {
|
|
899
|
+
id: session.id,
|
|
900
|
+
providerSessionId: session.providerSessionId,
|
|
901
|
+
status: session.status,
|
|
902
|
+
workingDirectory: session.workingDirectory,
|
|
903
|
+
worktree: session.worktree,
|
|
904
|
+
startedAt: session.startedAt,
|
|
905
|
+
endedAt: session.endedAt,
|
|
906
|
+
terminationReason: session.terminationReason,
|
|
907
|
+
};
|
|
908
|
+
const history = this.sessionHistory.get(agentId) ?? [];
|
|
909
|
+
this.sessionHistory.set(agentId, [historyEntry, ...history.filter((h) => h.id !== session.id)].slice(0, 20));
|
|
910
|
+
}
|
|
911
|
+
getPersistedHistory(agent) {
|
|
912
|
+
// Session history is stored under metadata.agent.sessionHistory
|
|
913
|
+
const agentMeta = agent.metadata?.agent;
|
|
914
|
+
const sessionHistory = agentMeta?.sessionHistory;
|
|
915
|
+
if (!Array.isArray(sessionHistory)) {
|
|
916
|
+
return [];
|
|
917
|
+
}
|
|
918
|
+
// Validate and type-cast
|
|
919
|
+
return sessionHistory.filter((entry) => {
|
|
920
|
+
if (typeof entry !== 'object' || entry === null)
|
|
921
|
+
return false;
|
|
922
|
+
const e = entry;
|
|
923
|
+
return (typeof e.id === 'string' &&
|
|
924
|
+
typeof e.status === 'string' &&
|
|
925
|
+
typeof e.workingDirectory === 'string');
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
mergeHistory(inMemory, persisted) {
|
|
929
|
+
const seen = new Set();
|
|
930
|
+
const merged = [];
|
|
931
|
+
// In-memory history takes precedence (more recent)
|
|
932
|
+
for (const entry of inMemory) {
|
|
933
|
+
if (!seen.has(entry.id)) {
|
|
934
|
+
seen.add(entry.id);
|
|
935
|
+
merged.push(entry);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
// Add persisted entries not in memory
|
|
939
|
+
for (const entry of persisted) {
|
|
940
|
+
if (!seen.has(entry.id)) {
|
|
941
|
+
seen.add(entry.id);
|
|
942
|
+
merged.push(entry);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
// Sort by start time descending
|
|
946
|
+
return merged.sort((a, b) => {
|
|
947
|
+
const aTime = a.startedAt ? this.getTimestampMs(a.startedAt) : 0;
|
|
948
|
+
const bTime = b.startedAt ? this.getTimestampMs(b.startedAt) : 0;
|
|
949
|
+
return bTime - aTime;
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
getTimestampMs(timestamp) {
|
|
953
|
+
return typeof timestamp === 'number' ? timestamp : new Date(timestamp).getTime();
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Builds the UWP task prompt to instruct the agent to process the assigned task first.
|
|
957
|
+
* This ensures compliance with the Universal Work Principle: "If there is work on your anchor, YOU MUST RUN IT"
|
|
958
|
+
*/
|
|
959
|
+
buildUWPTaskPrompt(uwpCheck) {
|
|
960
|
+
const parts = [
|
|
961
|
+
'**IMPORTANT: Task Assigned During Suspension**',
|
|
962
|
+
'',
|
|
963
|
+
'Before continuing with any previous context, you must first process the following assigned task:',
|
|
964
|
+
'',
|
|
965
|
+
`- Task ID: ${uwpCheck.taskId}`,
|
|
966
|
+
];
|
|
967
|
+
if (uwpCheck.taskTitle) {
|
|
968
|
+
parts.push(`- Title: ${uwpCheck.taskTitle}`);
|
|
969
|
+
}
|
|
970
|
+
if (uwpCheck.taskPriority !== undefined) {
|
|
971
|
+
parts.push(`- Priority: ${uwpCheck.taskPriority}`);
|
|
972
|
+
}
|
|
973
|
+
parts.push('', 'Please check the task details and begin working on it immediately.', 'Use `sf task get ' + uwpCheck.taskId + '` to retrieve full task details.');
|
|
974
|
+
return parts.join('\n');
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
// ============================================================================
|
|
978
|
+
// Factory Function
|
|
979
|
+
// ============================================================================
|
|
980
|
+
/**
|
|
981
|
+
* Creates a SessionManager instance
|
|
982
|
+
*/
|
|
983
|
+
export function createSessionManager(spawner, api, registry) {
|
|
984
|
+
return new SessionManagerImpl(spawner, api, registry);
|
|
985
|
+
}
|
|
986
|
+
//# sourceMappingURL=session-manager.js.map
|