im-hub-pro 0.2.29
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 +21 -0
- package/README.md +497 -0
- package/README.zh-CN.md +496 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +921 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/acp-server.d.ts +8 -0
- package/dist/core/acp-server.d.ts.map +1 -0
- package/dist/core/acp-server.js +266 -0
- package/dist/core/acp-server.js.map +1 -0
- package/dist/core/agent-base.d.ts +94 -0
- package/dist/core/agent-base.d.ts.map +1 -0
- package/dist/core/agent-base.js +374 -0
- package/dist/core/agent-base.js.map +1 -0
- package/dist/core/agent-cwd.d.ts +45 -0
- package/dist/core/agent-cwd.d.ts.map +1 -0
- package/dist/core/agent-cwd.js +178 -0
- package/dist/core/agent-cwd.js.map +1 -0
- package/dist/core/agent-cwd.test.d.ts +2 -0
- package/dist/core/agent-cwd.test.d.ts.map +1 -0
- package/dist/core/agent-cwd.test.js +149 -0
- package/dist/core/agent-cwd.test.js.map +1 -0
- package/dist/core/approval-bus.d.ts +232 -0
- package/dist/core/approval-bus.d.ts.map +1 -0
- package/dist/core/approval-bus.js +703 -0
- package/dist/core/approval-bus.js.map +1 -0
- package/dist/core/approval-bus.synthetic.test.d.ts +2 -0
- package/dist/core/approval-bus.synthetic.test.d.ts.map +1 -0
- package/dist/core/approval-bus.synthetic.test.js +182 -0
- package/dist/core/approval-bus.synthetic.test.js.map +1 -0
- package/dist/core/approval-bus.test.d.ts +2 -0
- package/dist/core/approval-bus.test.d.ts.map +1 -0
- package/dist/core/approval-bus.test.js +537 -0
- package/dist/core/approval-bus.test.js.map +1 -0
- package/dist/core/approval-router.d.ts +95 -0
- package/dist/core/approval-router.d.ts.map +1 -0
- package/dist/core/approval-router.js +450 -0
- package/dist/core/approval-router.js.map +1 -0
- package/dist/core/approval-router.test.d.ts +2 -0
- package/dist/core/approval-router.test.d.ts.map +1 -0
- package/dist/core/approval-router.test.js +413 -0
- package/dist/core/approval-router.test.js.map +1 -0
- package/dist/core/audit-log.d.ts +55 -0
- package/dist/core/audit-log.d.ts.map +1 -0
- package/dist/core/audit-log.js +203 -0
- package/dist/core/audit-log.js.map +1 -0
- package/dist/core/bgjob-reader.d.ts +65 -0
- package/dist/core/bgjob-reader.d.ts.map +1 -0
- package/dist/core/bgjob-reader.js +212 -0
- package/dist/core/bgjob-reader.js.map +1 -0
- package/dist/core/bgjob-reader.test.d.ts +2 -0
- package/dist/core/bgjob-reader.test.d.ts.map +1 -0
- package/dist/core/bgjob-reader.test.js +178 -0
- package/dist/core/bgjob-reader.test.js.map +1 -0
- package/dist/core/circuit-breaker.d.ts +37 -0
- package/dist/core/circuit-breaker.d.ts.map +1 -0
- package/dist/core/circuit-breaker.js +115 -0
- package/dist/core/circuit-breaker.js.map +1 -0
- package/dist/core/commands/agent.d.ts +4 -0
- package/dist/core/commands/agent.d.ts.map +1 -0
- package/dist/core/commands/agent.js +21 -0
- package/dist/core/commands/agent.js.map +1 -0
- package/dist/core/commands/approval.d.ts +3 -0
- package/dist/core/commands/approval.d.ts.map +1 -0
- package/dist/core/commands/approval.js +44 -0
- package/dist/core/commands/approval.js.map +1 -0
- package/dist/core/commands/approval.test.d.ts +2 -0
- package/dist/core/commands/approval.test.d.ts.map +1 -0
- package/dist/core/commands/approval.test.js +85 -0
- package/dist/core/commands/approval.test.js.map +1 -0
- package/dist/core/commands/audit.d.ts +3 -0
- package/dist/core/commands/audit.d.ts.map +1 -0
- package/dist/core/commands/audit.js +84 -0
- package/dist/core/commands/audit.js.map +1 -0
- package/dist/core/commands/builtin.d.ts +3 -0
- package/dist/core/commands/builtin.d.ts.map +1 -0
- package/dist/core/commands/builtin.js +26 -0
- package/dist/core/commands/builtin.js.map +1 -0
- package/dist/core/commands/job.d.ts +3 -0
- package/dist/core/commands/job.d.ts.map +1 -0
- package/dist/core/commands/job.js +195 -0
- package/dist/core/commands/job.js.map +1 -0
- package/dist/core/commands/model.d.ts +9 -0
- package/dist/core/commands/model.d.ts.map +1 -0
- package/dist/core/commands/model.js +183 -0
- package/dist/core/commands/model.js.map +1 -0
- package/dist/core/commands/plan.d.ts +3 -0
- package/dist/core/commands/plan.d.ts.map +1 -0
- package/dist/core/commands/plan.js +75 -0
- package/dist/core/commands/plan.js.map +1 -0
- package/dist/core/commands/plan.test.d.ts +2 -0
- package/dist/core/commands/plan.test.d.ts.map +1 -0
- package/dist/core/commands/plan.test.js +122 -0
- package/dist/core/commands/plan.test.js.map +1 -0
- package/dist/core/commands/router.d.ts +3 -0
- package/dist/core/commands/router.d.ts.map +1 -0
- package/dist/core/commands/router.js +71 -0
- package/dist/core/commands/router.js.map +1 -0
- package/dist/core/commands/schedule.d.ts +3 -0
- package/dist/core/commands/schedule.d.ts.map +1 -0
- package/dist/core/commands/schedule.js +123 -0
- package/dist/core/commands/schedule.js.map +1 -0
- package/dist/core/commands/sessions.d.ts +3 -0
- package/dist/core/commands/sessions.d.ts.map +1 -0
- package/dist/core/commands/sessions.js +88 -0
- package/dist/core/commands/sessions.js.map +1 -0
- package/dist/core/commands/stats.d.ts +3 -0
- package/dist/core/commands/stats.d.ts.map +1 -0
- package/dist/core/commands/stats.js +73 -0
- package/dist/core/commands/stats.js.map +1 -0
- package/dist/core/commands/think.d.ts +3 -0
- package/dist/core/commands/think.d.ts.map +1 -0
- package/dist/core/commands/think.js +28 -0
- package/dist/core/commands/think.js.map +1 -0
- package/dist/core/commands/workspaces.d.ts +3 -0
- package/dist/core/commands/workspaces.d.ts.map +1 -0
- package/dist/core/commands/workspaces.js +47 -0
- package/dist/core/commands/workspaces.js.map +1 -0
- package/dist/core/config-schema.d.ts +58 -0
- package/dist/core/config-schema.d.ts.map +1 -0
- package/dist/core/config-schema.js +63 -0
- package/dist/core/config-schema.js.map +1 -0
- package/dist/core/cron.d.ts +29 -0
- package/dist/core/cron.d.ts.map +1 -0
- package/dist/core/cron.js +184 -0
- package/dist/core/cron.js.map +1 -0
- package/dist/core/event-bus.d.ts +80 -0
- package/dist/core/event-bus.d.ts.map +1 -0
- package/dist/core/event-bus.js +62 -0
- package/dist/core/event-bus.js.map +1 -0
- package/dist/core/intent-llm.d.ts +27 -0
- package/dist/core/intent-llm.d.ts.map +1 -0
- package/dist/core/intent-llm.js +170 -0
- package/dist/core/intent-llm.js.map +1 -0
- package/dist/core/intent.d.ts +12 -0
- package/dist/core/intent.d.ts.map +1 -0
- package/dist/core/intent.js +187 -0
- package/dist/core/intent.js.map +1 -0
- package/dist/core/job-board.d.ts +84 -0
- package/dist/core/job-board.d.ts.map +1 -0
- package/dist/core/job-board.js +379 -0
- package/dist/core/job-board.js.map +1 -0
- package/dist/core/logger.d.ts +6 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +54 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/metrics.d.ts +55 -0
- package/dist/core/metrics.d.ts.map +1 -0
- package/dist/core/metrics.js +291 -0
- package/dist/core/metrics.js.map +1 -0
- package/dist/core/onboarding.d.ts +94 -0
- package/dist/core/onboarding.d.ts.map +1 -0
- package/dist/core/onboarding.js +426 -0
- package/dist/core/onboarding.js.map +1 -0
- package/dist/core/onboarding.test.d.ts +2 -0
- package/dist/core/onboarding.test.d.ts.map +1 -0
- package/dist/core/onboarding.test.js +112 -0
- package/dist/core/onboarding.test.js.map +1 -0
- package/dist/core/rate-limiter.d.ts +44 -0
- package/dist/core/rate-limiter.d.ts.map +1 -0
- package/dist/core/rate-limiter.js +115 -0
- package/dist/core/rate-limiter.js.map +1 -0
- package/dist/core/registry.d.ts +32 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +122 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/router.d.ts +41 -0
- package/dist/core/router.d.ts.map +1 -0
- package/dist/core/router.js +431 -0
- package/dist/core/router.js.map +1 -0
- package/dist/core/schedule.d.ts +65 -0
- package/dist/core/schedule.d.ts.map +1 -0
- package/dist/core/schedule.js +316 -0
- package/dist/core/schedule.js.map +1 -0
- package/dist/core/session-subtasks.test.d.ts +2 -0
- package/dist/core/session-subtasks.test.d.ts.map +1 -0
- package/dist/core/session-subtasks.test.js +88 -0
- package/dist/core/session-subtasks.test.js.map +1 -0
- package/dist/core/session.d.ts +182 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +774 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/sqlite-helper.d.ts +37 -0
- package/dist/core/sqlite-helper.d.ts.map +1 -0
- package/dist/core/sqlite-helper.js +79 -0
- package/dist/core/sqlite-helper.js.map +1 -0
- package/dist/core/transcribe.d.ts +25 -0
- package/dist/core/transcribe.d.ts.map +1 -0
- package/dist/core/transcribe.js +217 -0
- package/dist/core/transcribe.js.map +1 -0
- package/dist/core/transcribe.test.d.ts +2 -0
- package/dist/core/transcribe.test.d.ts.map +1 -0
- package/dist/core/transcribe.test.js +163 -0
- package/dist/core/transcribe.test.js.map +1 -0
- package/dist/core/types.d.ts +352 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/workspace.d.ts +67 -0
- package/dist/core/workspace.d.ts.map +1 -0
- package/dist/core/workspace.js +113 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/agents/acp/acp-adapter.d.ts +16 -0
- package/dist/plugins/agents/acp/acp-adapter.d.ts.map +1 -0
- package/dist/plugins/agents/acp/acp-adapter.js +49 -0
- package/dist/plugins/agents/acp/acp-adapter.js.map +1 -0
- package/dist/plugins/agents/acp/acp-client.d.ts +32 -0
- package/dist/plugins/agents/acp/acp-client.d.ts.map +1 -0
- package/dist/plugins/agents/acp/acp-client.js +175 -0
- package/dist/plugins/agents/acp/acp-client.js.map +1 -0
- package/dist/plugins/agents/acp/discovery.d.ts +19 -0
- package/dist/plugins/agents/acp/discovery.d.ts.map +1 -0
- package/dist/plugins/agents/acp/discovery.js +109 -0
- package/dist/plugins/agents/acp/discovery.js.map +1 -0
- package/dist/plugins/agents/acp/index.d.ts +4 -0
- package/dist/plugins/agents/acp/index.d.ts.map +1 -0
- package/dist/plugins/agents/acp/index.js +4 -0
- package/dist/plugins/agents/acp/index.js.map +1 -0
- package/dist/plugins/agents/acp/types.d.ts +62 -0
- package/dist/plugins/agents/acp/types.d.ts.map +1 -0
- package/dist/plugins/agents/acp/types.js +5 -0
- package/dist/plugins/agents/acp/types.js.map +1 -0
- package/dist/plugins/agents/claude-code/adapter.test.d.ts +2 -0
- package/dist/plugins/agents/claude-code/adapter.test.d.ts.map +1 -0
- package/dist/plugins/agents/claude-code/adapter.test.js +195 -0
- package/dist/plugins/agents/claude-code/adapter.test.js.map +1 -0
- package/dist/plugins/agents/claude-code/index.d.ts +25 -0
- package/dist/plugins/agents/claude-code/index.d.ts.map +1 -0
- package/dist/plugins/agents/claude-code/index.js +184 -0
- package/dist/plugins/agents/claude-code/index.js.map +1 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts +42 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts.map +1 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.js +235 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.test.d.ts +2 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.test.d.ts.map +1 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.test.js +188 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.test.js.map +1 -0
- package/dist/plugins/agents/codex/adapter.test.d.ts +2 -0
- package/dist/plugins/agents/codex/adapter.test.d.ts.map +1 -0
- package/dist/plugins/agents/codex/adapter.test.js +192 -0
- package/dist/plugins/agents/codex/adapter.test.js.map +1 -0
- package/dist/plugins/agents/codex/index.d.ts +37 -0
- package/dist/plugins/agents/codex/index.d.ts.map +1 -0
- package/dist/plugins/agents/codex/index.js +254 -0
- package/dist/plugins/agents/codex/index.js.map +1 -0
- package/dist/plugins/agents/copilot/index.d.ts +35 -0
- package/dist/plugins/agents/copilot/index.d.ts.map +1 -0
- package/dist/plugins/agents/copilot/index.js +182 -0
- package/dist/plugins/agents/copilot/index.js.map +1 -0
- package/dist/plugins/agents/opencode/adapter.test.d.ts +2 -0
- package/dist/plugins/agents/opencode/adapter.test.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/adapter.test.js +139 -0
- package/dist/plugins/agents/opencode/adapter.test.js.map +1 -0
- package/dist/plugins/agents/opencode/http-adapter.test.d.ts +2 -0
- package/dist/plugins/agents/opencode/http-adapter.test.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/http-adapter.test.js +492 -0
- package/dist/plugins/agents/opencode/http-adapter.test.js.map +1 -0
- package/dist/plugins/agents/opencode/index.d.ts +5 -0
- package/dist/plugins/agents/opencode/index.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/index.js +30 -0
- package/dist/plugins/agents/opencode/index.js.map +1 -0
- package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts +138 -0
- package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/opencode-http-adapter.js +549 -0
- package/dist/plugins/agents/opencode/opencode-http-adapter.js.map +1 -0
- package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts +24 -0
- package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/opencode-stdio-adapter.js +103 -0
- package/dist/plugins/agents/opencode/opencode-stdio-adapter.js.map +1 -0
- package/dist/plugins/agents/opencode/serve-manager.d.ts +27 -0
- package/dist/plugins/agents/opencode/serve-manager.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/serve-manager.js +190 -0
- package/dist/plugins/agents/opencode/serve-manager.js.map +1 -0
- package/dist/plugins/messengers/discord/discord-adapter.d.ts +22 -0
- package/dist/plugins/messengers/discord/discord-adapter.d.ts.map +1 -0
- package/dist/plugins/messengers/discord/discord-adapter.js +241 -0
- package/dist/plugins/messengers/discord/discord-adapter.js.map +1 -0
- package/dist/plugins/messengers/discord/discord-adapter.test.d.ts +2 -0
- package/dist/plugins/messengers/discord/discord-adapter.test.d.ts.map +1 -0
- package/dist/plugins/messengers/discord/discord-adapter.test.js +332 -0
- package/dist/plugins/messengers/discord/discord-adapter.test.js.map +1 -0
- package/dist/plugins/messengers/discord/index.d.ts +4 -0
- package/dist/plugins/messengers/discord/index.d.ts.map +1 -0
- package/dist/plugins/messengers/discord/index.js +4 -0
- package/dist/plugins/messengers/discord/index.js.map +1 -0
- package/dist/plugins/messengers/discord/markdown-to-discord.d.ts +11 -0
- package/dist/plugins/messengers/discord/markdown-to-discord.d.ts.map +1 -0
- package/dist/plugins/messengers/discord/markdown-to-discord.js +59 -0
- package/dist/plugins/messengers/discord/markdown-to-discord.js.map +1 -0
- package/dist/plugins/messengers/discord/types.d.ts +9 -0
- package/dist/plugins/messengers/discord/types.d.ts.map +1 -0
- package/dist/plugins/messengers/discord/types.js +3 -0
- package/dist/plugins/messengers/discord/types.js.map +1 -0
- package/dist/plugins/messengers/feishu/card-builder.d.ts +23 -0
- package/dist/plugins/messengers/feishu/card-builder.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/card-builder.js +89 -0
- package/dist/plugins/messengers/feishu/card-builder.js.map +1 -0
- package/dist/plugins/messengers/feishu/feishu-adapter.d.ts +33 -0
- package/dist/plugins/messengers/feishu/feishu-adapter.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/feishu-adapter.js +195 -0
- package/dist/plugins/messengers/feishu/feishu-adapter.js.map +1 -0
- package/dist/plugins/messengers/feishu/feishu-client.d.ts +44 -0
- package/dist/plugins/messengers/feishu/feishu-client.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/feishu-client.js +120 -0
- package/dist/plugins/messengers/feishu/feishu-client.js.map +1 -0
- package/dist/plugins/messengers/feishu/feishu-dedup.test.d.ts +2 -0
- package/dist/plugins/messengers/feishu/feishu-dedup.test.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/feishu-dedup.test.js +70 -0
- package/dist/plugins/messengers/feishu/feishu-dedup.test.js.map +1 -0
- package/dist/plugins/messengers/feishu/index.d.ts +4 -0
- package/dist/plugins/messengers/feishu/index.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/index.js +4 -0
- package/dist/plugins/messengers/feishu/index.js.map +1 -0
- package/dist/plugins/messengers/feishu/types.d.ts +113 -0
- package/dist/plugins/messengers/feishu/types.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/types.js +4 -0
- package/dist/plugins/messengers/feishu/types.js.map +1 -0
- package/dist/plugins/messengers/telegram/index.d.ts +4 -0
- package/dist/plugins/messengers/telegram/index.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/index.js +4 -0
- package/dist/plugins/messengers/telegram/index.js.map +1 -0
- package/dist/plugins/messengers/telegram/markdown-to-html.d.ts +5 -0
- package/dist/plugins/messengers/telegram/markdown-to-html.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/markdown-to-html.js +186 -0
- package/dist/plugins/messengers/telegram/markdown-to-html.js.map +1 -0
- package/dist/plugins/messengers/telegram/media-download.d.ts +51 -0
- package/dist/plugins/messengers/telegram/media-download.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/media-download.js +224 -0
- package/dist/plugins/messengers/telegram/media-download.js.map +1 -0
- package/dist/plugins/messengers/telegram/media-download.test.d.ts +2 -0
- package/dist/plugins/messengers/telegram/media-download.test.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/media-download.test.js +125 -0
- package/dist/plugins/messengers/telegram/media-download.test.js.map +1 -0
- package/dist/plugins/messengers/telegram/telegram-adapter.d.ts +62 -0
- package/dist/plugins/messengers/telegram/telegram-adapter.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/telegram-adapter.js +653 -0
- package/dist/plugins/messengers/telegram/telegram-adapter.js.map +1 -0
- package/dist/plugins/messengers/telegram/types.d.ts +47 -0
- package/dist/plugins/messengers/telegram/types.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/types.js +3 -0
- package/dist/plugins/messengers/telegram/types.js.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-adapter.d.ts +68 -0
- package/dist/plugins/messengers/wechat/ilink-adapter.d.ts.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-adapter.js +483 -0
- package/dist/plugins/messengers/wechat/ilink-adapter.js.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-client.d.ts +66 -0
- package/dist/plugins/messengers/wechat/ilink-client.d.ts.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-client.js +288 -0
- package/dist/plugins/messengers/wechat/ilink-client.js.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-types.d.ts +173 -0
- package/dist/plugins/messengers/wechat/ilink-types.d.ts.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-types.js +12 -0
- package/dist/plugins/messengers/wechat/ilink-types.js.map +1 -0
- package/dist/utils/backoff.d.ts +35 -0
- package/dist/utils/backoff.d.ts.map +1 -0
- package/dist/utils/backoff.js +59 -0
- package/dist/utils/backoff.js.map +1 -0
- package/dist/utils/cross-platform.d.ts +26 -0
- package/dist/utils/cross-platform.d.ts.map +1 -0
- package/dist/utils/cross-platform.js +58 -0
- package/dist/utils/cross-platform.js.map +1 -0
- package/dist/utils/message-split.d.ts +14 -0
- package/dist/utils/message-split.d.ts.map +1 -0
- package/dist/utils/message-split.js +65 -0
- package/dist/utils/message-split.js.map +1 -0
- package/dist/utils/safe-equal.d.ts +2 -0
- package/dist/utils/safe-equal.d.ts.map +1 -0
- package/dist/utils/safe-equal.js +11 -0
- package/dist/utils/safe-equal.js.map +1 -0
- package/dist/web/public/_app.js +196 -0
- package/dist/web/public/index.html +935 -0
- package/dist/web/public/settings.html +1181 -0
- package/dist/web/public/tasks.html +1827 -0
- package/dist/web/server.d.ts +11 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +1820 -0
- package/dist/web/server.js.map +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
// Session manager — per-conversation state
|
|
2
|
+
//
|
|
3
|
+
// On-disk layout (one directory tree per home):
|
|
4
|
+
// ~/.im-hub/sessions/<safe-key>.json — metadata (no messages)
|
|
5
|
+
// ~/.im-hub/sessions/<safe-key>.log — append-only JSONL of messages
|
|
6
|
+
//
|
|
7
|
+
// Splitting the message log out of the JSON metadata avoids rewriting the
|
|
8
|
+
// entire history on every chat turn (the old behavior was an O(N) write
|
|
9
|
+
// per message). All metadata writes are atomic via writeFile→rename.
|
|
10
|
+
import { createHash, randomBytes } from 'crypto';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { mkdir, readFile, writeFile, rename, unlink, appendFile, readdir } from 'fs/promises';
|
|
14
|
+
import { approvalBus } from './approval-bus.js';
|
|
15
|
+
import { logger as rootLogger } from './logger.js';
|
|
16
|
+
const log = rootLogger.child({ component: 'session' });
|
|
17
|
+
const SESSIONS_DIR = join(homedir(), '.im-hub', 'sessions');
|
|
18
|
+
function sanitizeKey(raw) {
|
|
19
|
+
return raw.replace(/[^A-Za-z0-9_-]/g, (c) => {
|
|
20
|
+
return createHash('sha256').update(c).digest('hex').slice(0, 8);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function sessionFilePath(key) {
|
|
24
|
+
const safe = sanitizeKey(key);
|
|
25
|
+
return join(SESSIONS_DIR, `${safe}.json`);
|
|
26
|
+
}
|
|
27
|
+
function sessionLogPath(key) {
|
|
28
|
+
const safe = sanitizeKey(key);
|
|
29
|
+
return join(SESSIONS_DIR, `${safe}.log`);
|
|
30
|
+
}
|
|
31
|
+
// Two-tier TTL (split out to fix the "agent drift after long pause" issue):
|
|
32
|
+
//
|
|
33
|
+
// MESSAGES_TTL — how long the in-memory chat history sticks around before
|
|
34
|
+
// we drop it from RAM and delete the .log file. Short by
|
|
35
|
+
// default (30 min) because a long pause usually means the
|
|
36
|
+
// user has switched topics; replaying stale messages back to
|
|
37
|
+
// the agent just bloats the prompt.
|
|
38
|
+
//
|
|
39
|
+
// META_TTL — how long the *session metadata* (agent, model, variant,
|
|
40
|
+
// claudeSessionId, claudeSessionPrimed, usage stats) lives
|
|
41
|
+
// on disk. Long by default (7 days) so the thread's "sticky
|
|
42
|
+
// agent" and resumable claude-code session id survive
|
|
43
|
+
// overnight / weekend gaps. Without this, a 30-minute
|
|
44
|
+
// silence followed by a coding-keyword message would
|
|
45
|
+
// re-classify and switch agents (e.g. claude-code → opencode).
|
|
46
|
+
//
|
|
47
|
+
// Both are env-overridable for ops tuning.
|
|
48
|
+
function envInt(name, fallback) {
|
|
49
|
+
const raw = process.env[name];
|
|
50
|
+
if (!raw)
|
|
51
|
+
return fallback;
|
|
52
|
+
const n = parseInt(raw, 10);
|
|
53
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
54
|
+
}
|
|
55
|
+
const MESSAGES_TTL = envInt('IMHUB_SESSION_MESSAGES_TTL_MS', 30 * 60 * 1000);
|
|
56
|
+
const META_TTL = envInt('IMHUB_SESSION_META_TTL_MS', 7 * 24 * 60 * 60 * 1000);
|
|
57
|
+
// Back-compat: external callers (tests, schedule.ts) used to import DEFAULT_TTL
|
|
58
|
+
// to mean "the one ttl". Keep the symbol pointing at META_TTL so anywhere it
|
|
59
|
+
// still appears in logs/metrics gets the long-lived value.
|
|
60
|
+
const DEFAULT_TTL = META_TTL;
|
|
61
|
+
const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
|
62
|
+
function metaStale(s, now = Date.now()) {
|
|
63
|
+
return now - s.lastActivity.getTime() > META_TTL;
|
|
64
|
+
}
|
|
65
|
+
function messagesStale(s, now = Date.now()) {
|
|
66
|
+
return now - s.lastActivity.getTime() > MESSAGES_TTL;
|
|
67
|
+
}
|
|
68
|
+
class SessionManager {
|
|
69
|
+
sessions = new Map();
|
|
70
|
+
cleanupTimer;
|
|
71
|
+
/**
|
|
72
|
+
* Per-key promise chain used to serialize writes that perform a
|
|
73
|
+
* read-modify-write on the in-memory session + persistent JSONL log.
|
|
74
|
+
*
|
|
75
|
+
* Without this, two concurrent {@link addMessage} calls on the same key
|
|
76
|
+
* could interleave: A reads cached session, B reads cached session, both
|
|
77
|
+
* push a different message into the same array, then both write — the
|
|
78
|
+
* later write wins and the earlier message is lost from disk (the
|
|
79
|
+
* in-memory copy is fine because both pushes happened on the same object
|
|
80
|
+
* reference, but the JSONL log only reflects the most recent appendFile +
|
|
81
|
+
* meta save).
|
|
82
|
+
*
|
|
83
|
+
* The lock is keyed by `${platform}:${channelId}:${threadId}`; different
|
|
84
|
+
* threads still proceed in parallel.
|
|
85
|
+
*/
|
|
86
|
+
writeQueues = new Map();
|
|
87
|
+
async start() {
|
|
88
|
+
// Ensure sessions directory exists
|
|
89
|
+
await mkdir(SESSIONS_DIR, { recursive: true });
|
|
90
|
+
// Start cleanup timer
|
|
91
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL);
|
|
92
|
+
log.info({ dir: SESSIONS_DIR }, 'Session manager started');
|
|
93
|
+
}
|
|
94
|
+
stop() {
|
|
95
|
+
if (this.cleanupTimer) {
|
|
96
|
+
clearInterval(this.cleanupTimer);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Run `fn` while holding a per-key serial lock. Subsequent calls with the
|
|
101
|
+
* same key wait until prior ones settle (success OR failure). Returns the
|
|
102
|
+
* value of `fn`. Internal use only — keeps {@link addMessage} race-free
|
|
103
|
+
* without introducing an external dep like p-queue.
|
|
104
|
+
*/
|
|
105
|
+
async withLock(key, fn) {
|
|
106
|
+
const prev = this.writeQueues.get(key) ?? Promise.resolve();
|
|
107
|
+
// Chain even on failure so a thrown error in one segment doesn't poison
|
|
108
|
+
// subsequent waiters with that same rejection.
|
|
109
|
+
const next = prev.then(fn, fn);
|
|
110
|
+
this.writeQueues.set(key, next);
|
|
111
|
+
try {
|
|
112
|
+
return await next;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
// Only clear if we're still the tail; another caller may have queued
|
|
116
|
+
// behind us in the meantime and we shouldn't drop their reference.
|
|
117
|
+
if (this.writeQueues.get(key) === next) {
|
|
118
|
+
this.writeQueues.delete(key);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Get or create a session for a conversation
|
|
124
|
+
* Session key: `${platform}:${channelId}:${threadId}`
|
|
125
|
+
*/
|
|
126
|
+
async getOrCreateSession(platform, channelId, threadId, agent) {
|
|
127
|
+
const key = `${platform}:${channelId}:${threadId}`;
|
|
128
|
+
const now = new Date();
|
|
129
|
+
// Check memory cache
|
|
130
|
+
let session = this.sessions.get(key);
|
|
131
|
+
if (session) {
|
|
132
|
+
if (metaStale(session, now.getTime())) {
|
|
133
|
+
session = undefined; // fully expired → create new below
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
if (messagesStale(session, now.getTime()) && session.messages.length > 0) {
|
|
137
|
+
session.messages = [];
|
|
138
|
+
try {
|
|
139
|
+
await unlink(sessionLogPath(key));
|
|
140
|
+
}
|
|
141
|
+
catch { /* no log to drop */ }
|
|
142
|
+
}
|
|
143
|
+
session.lastActivity = now;
|
|
144
|
+
await this.saveSessionMeta(key, session);
|
|
145
|
+
return session;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Try loading from disk
|
|
149
|
+
session = await this.loadSession(key);
|
|
150
|
+
if (session && !metaStale(session, now.getTime())) {
|
|
151
|
+
if (messagesStale(session, now.getTime()) && session.messages.length > 0) {
|
|
152
|
+
session.messages = [];
|
|
153
|
+
try {
|
|
154
|
+
await unlink(sessionLogPath(key));
|
|
155
|
+
}
|
|
156
|
+
catch { /* no log to drop */ }
|
|
157
|
+
}
|
|
158
|
+
session.lastActivity = now;
|
|
159
|
+
this.sessions.set(key, session);
|
|
160
|
+
await this.saveSessionMeta(key, session);
|
|
161
|
+
return session;
|
|
162
|
+
}
|
|
163
|
+
// Create new session
|
|
164
|
+
session = {
|
|
165
|
+
id: `${platform}-${channelId}-${threadId}-${Date.now()}-${randomBytes(4).toString('hex')}`,
|
|
166
|
+
channelId,
|
|
167
|
+
threadId,
|
|
168
|
+
platform,
|
|
169
|
+
agent,
|
|
170
|
+
createdAt: now,
|
|
171
|
+
lastActivity: now,
|
|
172
|
+
ttl: DEFAULT_TTL,
|
|
173
|
+
messages: [],
|
|
174
|
+
};
|
|
175
|
+
this.sessions.set(key, session);
|
|
176
|
+
await this.saveSession(key, session);
|
|
177
|
+
return session;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get existing session without creating a new one
|
|
181
|
+
* Returns undefined if no session exists or it's expired
|
|
182
|
+
*/
|
|
183
|
+
async getExistingSession(platform, channelId, threadId) {
|
|
184
|
+
const key = `${platform}:${channelId}:${threadId}`;
|
|
185
|
+
const now = new Date();
|
|
186
|
+
// Check memory cache
|
|
187
|
+
let session = this.sessions.get(key);
|
|
188
|
+
if (session) {
|
|
189
|
+
if (metaStale(session, now.getTime())) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
if (messagesStale(session, now.getTime()) && session.messages.length > 0) {
|
|
193
|
+
// Drop stale chat history but preserve metadata (sticky agent,
|
|
194
|
+
// claudeSessionId etc.) — that's the whole point of META_TTL.
|
|
195
|
+
session.messages = [];
|
|
196
|
+
try {
|
|
197
|
+
await unlink(sessionLogPath(key));
|
|
198
|
+
}
|
|
199
|
+
catch { /* ignore */ }
|
|
200
|
+
}
|
|
201
|
+
return session;
|
|
202
|
+
}
|
|
203
|
+
// Try loading from disk
|
|
204
|
+
session = await this.loadSession(key);
|
|
205
|
+
if (session && !metaStale(session, now.getTime())) {
|
|
206
|
+
if (messagesStale(session, now.getTime()) && session.messages.length > 0) {
|
|
207
|
+
session.messages = [];
|
|
208
|
+
try {
|
|
209
|
+
await unlink(sessionLogPath(key));
|
|
210
|
+
}
|
|
211
|
+
catch { /* ignore */ }
|
|
212
|
+
}
|
|
213
|
+
this.sessions.set(key, session);
|
|
214
|
+
return session;
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Switch the agent for a session.
|
|
220
|
+
*
|
|
221
|
+
* Generates a new session id but preserves thread identity AND every
|
|
222
|
+
* thread-level field that isn't agent-specific:
|
|
223
|
+
* - usage (per-thread /stats roll-up)
|
|
224
|
+
* - subtasks/active (subtask state lives at thread level)
|
|
225
|
+
* - claudeSessionId (Claude UUID survives /oc → /cc round-trips so the
|
|
226
|
+
* underlying ~/.claude/projects jsonl keeps continuing
|
|
227
|
+
* when the user comes back to claude)
|
|
228
|
+
*
|
|
229
|
+
* `model` and `variant` are reset because they live in different namespaces
|
|
230
|
+
* across CLIs (`opencode` model ≠ `claude` model); carrying them across
|
|
231
|
+
* would just feed the new agent an unrecognized argument.
|
|
232
|
+
*/
|
|
233
|
+
async switchAgent(platform, channelId, threadId, newAgent) {
|
|
234
|
+
const key = `${platform}:${channelId}:${threadId}`;
|
|
235
|
+
// Get existing session or create new
|
|
236
|
+
const existing = this.sessions.get(key) || await this.loadSession(key);
|
|
237
|
+
const now = new Date();
|
|
238
|
+
const session = {
|
|
239
|
+
id: `${platform}-${channelId}-${threadId}-${Date.now()}-${randomBytes(4).toString('hex')}`,
|
|
240
|
+
channelId,
|
|
241
|
+
threadId,
|
|
242
|
+
platform,
|
|
243
|
+
agent: newAgent,
|
|
244
|
+
createdAt: existing?.createdAt || now,
|
|
245
|
+
lastActivity: now,
|
|
246
|
+
ttl: DEFAULT_TTL,
|
|
247
|
+
messages: existing?.messages || [],
|
|
248
|
+
usage: existing?.usage,
|
|
249
|
+
activeSubtaskId: existing?.activeSubtaskId,
|
|
250
|
+
subtasks: existing?.subtasks,
|
|
251
|
+
subtaskCounter: existing?.subtaskCounter,
|
|
252
|
+
claudeSessionId: existing?.claudeSessionId,
|
|
253
|
+
claudeSessionPrimed: existing?.claudeSessionPrimed,
|
|
254
|
+
opencodeSessionId: existing?.opencodeSessionId,
|
|
255
|
+
codexSessionId: existing?.codexSessionId,
|
|
256
|
+
planMode: existing?.planMode,
|
|
257
|
+
};
|
|
258
|
+
this.sessions.set(key, session);
|
|
259
|
+
await this.saveSession(key, session);
|
|
260
|
+
return session;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Append a message to the session history.
|
|
264
|
+
*
|
|
265
|
+
* Performance: instead of re-serializing the entire session JSON every
|
|
266
|
+
* turn, the message body is appended to a JSONL log file alongside the
|
|
267
|
+
* metadata (which gets a tiny atomic update for `lastActivity`).
|
|
268
|
+
*
|
|
269
|
+
* Concurrency: the entire read-modify-write is serialized per session key
|
|
270
|
+
* via {@link withLock} so two concurrent calls on the same thread (e.g.
|
|
271
|
+
* the IM message landing event AND a tool-result event arriving within
|
|
272
|
+
* the same ms) cannot lose either message to a lost-update race.
|
|
273
|
+
*/
|
|
274
|
+
async addMessage(platform, channelId, threadId, message) {
|
|
275
|
+
const key = `${platform}:${channelId}:${threadId}`;
|
|
276
|
+
return this.withLock(key, async () => {
|
|
277
|
+
const session = this.sessions.get(key) || await this.loadSession(key);
|
|
278
|
+
if (!session)
|
|
279
|
+
return;
|
|
280
|
+
session.messages.push(message);
|
|
281
|
+
session.lastActivity = new Date();
|
|
282
|
+
this.sessions.set(key, session);
|
|
283
|
+
// Append-only log avoids rewriting the entire history per turn.
|
|
284
|
+
try {
|
|
285
|
+
await appendFile(sessionLogPath(key), JSON.stringify(message) + '\n');
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// Disk error → fall back to full save which will catch it again
|
|
289
|
+
}
|
|
290
|
+
// Persist metadata only (now small & cheap).
|
|
291
|
+
await this.saveSessionMeta(key, session);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Persist `model` / `variant` / arbitrary patchable fields. Used by
|
|
296
|
+
* `/model`, `/think` etc so the change survives a restart between turns.
|
|
297
|
+
* Mutates the in-memory session in place AND writes metadata atomically.
|
|
298
|
+
*/
|
|
299
|
+
async patchSession(platform, channelId, threadId, patch) {
|
|
300
|
+
const key = `${platform}:${channelId}:${threadId}`;
|
|
301
|
+
const session = this.sessions.get(key) || await this.loadSession(key);
|
|
302
|
+
if (!session)
|
|
303
|
+
return undefined;
|
|
304
|
+
if (patch.model !== undefined)
|
|
305
|
+
session.model = patch.model || undefined;
|
|
306
|
+
if (patch.variant !== undefined)
|
|
307
|
+
session.variant = patch.variant || undefined;
|
|
308
|
+
if (patch.agent !== undefined)
|
|
309
|
+
session.agent = patch.agent;
|
|
310
|
+
if (patch.planMode !== undefined) {
|
|
311
|
+
// Normalize to canonical shape: true keeps the flag, false drops it.
|
|
312
|
+
// Storing only the truthy state keeps the on-disk JSON small and lets
|
|
313
|
+
// a missing field unambiguously mean "off".
|
|
314
|
+
if (patch.planMode)
|
|
315
|
+
session.planMode = true;
|
|
316
|
+
else
|
|
317
|
+
delete session.planMode;
|
|
318
|
+
}
|
|
319
|
+
session.lastActivity = new Date();
|
|
320
|
+
this.sessions.set(key, session);
|
|
321
|
+
await this.saveSessionMeta(key, session);
|
|
322
|
+
return session;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Persist claude-code resumable session bookkeeping (UUID + primed flag).
|
|
326
|
+
* Returns the updated session, or undefined if no session exists yet for
|
|
327
|
+
* this thread. Caller is expected to ensure the session exists first.
|
|
328
|
+
*/
|
|
329
|
+
async setClaudeSessionId(platform, channelId, threadId, claudeSessionId) {
|
|
330
|
+
const key = `${platform}:${channelId}:${threadId}`;
|
|
331
|
+
const session = this.sessions.get(key) || await this.loadSession(key);
|
|
332
|
+
if (!session)
|
|
333
|
+
return undefined;
|
|
334
|
+
session.claudeSessionId = claudeSessionId;
|
|
335
|
+
session.lastActivity = new Date();
|
|
336
|
+
this.sessions.set(key, session);
|
|
337
|
+
await this.saveSessionMeta(key, session);
|
|
338
|
+
return session;
|
|
339
|
+
}
|
|
340
|
+
async markClaudeSessionPrimed(platform, channelId, threadId) {
|
|
341
|
+
const key = `${platform}:${channelId}:${threadId}`;
|
|
342
|
+
const session = this.sessions.get(key) || await this.loadSession(key);
|
|
343
|
+
if (!session || session.claudeSessionPrimed)
|
|
344
|
+
return;
|
|
345
|
+
session.claudeSessionPrimed = true;
|
|
346
|
+
session.lastActivity = new Date();
|
|
347
|
+
this.sessions.set(key, session);
|
|
348
|
+
await this.saveSessionMeta(key, session);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Persist opencode's native session id (`ses_…`) once we've seen it in the
|
|
352
|
+
* adapter's stream. Idempotent — calling with the same id is a no-op so
|
|
353
|
+
* the per-event callback can fire as many times as opencode sends events.
|
|
354
|
+
*/
|
|
355
|
+
async setOpencodeSessionId(platform, channelId, threadId, opencodeSessionId) {
|
|
356
|
+
const key = `${platform}:${channelId}:${threadId}`;
|
|
357
|
+
const session = this.sessions.get(key) || await this.loadSession(key);
|
|
358
|
+
if (!session)
|
|
359
|
+
return undefined;
|
|
360
|
+
if (session.opencodeSessionId === opencodeSessionId)
|
|
361
|
+
return session;
|
|
362
|
+
session.opencodeSessionId = opencodeSessionId;
|
|
363
|
+
session.lastActivity = new Date();
|
|
364
|
+
this.sessions.set(key, session);
|
|
365
|
+
await this.saveSessionMeta(key, session);
|
|
366
|
+
return session;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Persist codex's native thread id (UUID) once we've seen it in the
|
|
370
|
+
* adapter's `thread.started` event. Idempotent — same id may fire multiple
|
|
371
|
+
* times per spawn. Mirrors setOpencodeSessionId.
|
|
372
|
+
*/
|
|
373
|
+
async setCodexSessionId(platform, channelId, threadId, codexSessionId) {
|
|
374
|
+
const key = `${platform}:${channelId}:${threadId}`;
|
|
375
|
+
const session = this.sessions.get(key) || await this.loadSession(key);
|
|
376
|
+
if (!session)
|
|
377
|
+
return undefined;
|
|
378
|
+
if (session.codexSessionId === codexSessionId)
|
|
379
|
+
return session;
|
|
380
|
+
session.codexSessionId = codexSessionId;
|
|
381
|
+
session.lastActivity = new Date();
|
|
382
|
+
this.sessions.set(key, session);
|
|
383
|
+
await this.saveSessionMeta(key, session);
|
|
384
|
+
return session;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Increment the per-session usage roll-up after a successful agent
|
|
388
|
+
* invocation. Used by router.callAgentWithHistory to power /stats.
|
|
389
|
+
*/
|
|
390
|
+
async recordUsage(platform, channelId, threadId, delta) {
|
|
391
|
+
const key = `${platform}:${channelId}:${threadId}`;
|
|
392
|
+
const session = this.sessions.get(key) || await this.loadSession(key);
|
|
393
|
+
if (!session)
|
|
394
|
+
return;
|
|
395
|
+
if (!session.usage) {
|
|
396
|
+
session.usage = {
|
|
397
|
+
turns: 0,
|
|
398
|
+
costUsd: 0,
|
|
399
|
+
promptChars: 0,
|
|
400
|
+
responseChars: 0,
|
|
401
|
+
durationMsTotal: 0,
|
|
402
|
+
startedAt: new Date().toISOString(),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
session.usage.turns += 1;
|
|
406
|
+
session.usage.costUsd += Number.isFinite(delta.costUsd) ? delta.costUsd : 0;
|
|
407
|
+
session.usage.promptChars += Number.isFinite(delta.promptChars) ? delta.promptChars : 0;
|
|
408
|
+
session.usage.responseChars += Number.isFinite(delta.responseChars) ? delta.responseChars : 0;
|
|
409
|
+
session.usage.durationMsTotal += Number.isFinite(delta.durationMs) ? delta.durationMs : 0;
|
|
410
|
+
session.lastActivity = new Date();
|
|
411
|
+
this.sessions.set(key, session);
|
|
412
|
+
await this.saveSessionMeta(key, session);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Reset conversation history (keep session but clear messages)
|
|
416
|
+
*/
|
|
417
|
+
async resetConversation(platform, channelId, threadId) {
|
|
418
|
+
const key = `${platform}:${channelId}:${threadId}`;
|
|
419
|
+
const session = this.sessions.get(key) || await this.loadSession(key);
|
|
420
|
+
if (session) {
|
|
421
|
+
session.messages = [];
|
|
422
|
+
session.lastActivity = new Date();
|
|
423
|
+
session.id = `${platform}-${channelId}-${threadId}-${Date.now()}-${randomBytes(4).toString('hex')}`; // New session ID
|
|
424
|
+
// Forget the old per-agent CLI sessions — /new should give a clean slate
|
|
425
|
+
// for both Claude (`--resume`) and opencode (`--session`).
|
|
426
|
+
delete session.claudeSessionId;
|
|
427
|
+
delete session.claudeSessionPrimed;
|
|
428
|
+
delete session.opencodeSessionId;
|
|
429
|
+
delete session.codexSessionId;
|
|
430
|
+
// Plan mode is per-conversation intent ("先规划再动手") — a fresh
|
|
431
|
+
// conversation always starts at "off" so users don't get a surprising
|
|
432
|
+
// read-only run after /new.
|
|
433
|
+
delete session.planMode;
|
|
434
|
+
// Drop any per-thread auto-allow approval rules so the new conversation
|
|
435
|
+
// starts back at "ask every time".
|
|
436
|
+
try {
|
|
437
|
+
approvalBus.clearAutoAllowForThread(threadId);
|
|
438
|
+
}
|
|
439
|
+
catch { /* ignore */ }
|
|
440
|
+
this.sessions.set(key, session);
|
|
441
|
+
await this.saveSession(key, session);
|
|
442
|
+
return session;
|
|
443
|
+
}
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Get session with messages (convenience method)
|
|
448
|
+
*/
|
|
449
|
+
async getSessionWithHistory(platform, channelId, threadId) {
|
|
450
|
+
const session = await this.getExistingSession(platform, channelId, threadId);
|
|
451
|
+
if (session) {
|
|
452
|
+
return { session, messages: session.messages };
|
|
453
|
+
}
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Create or get a subtask session (independent from parent).
|
|
458
|
+
*/
|
|
459
|
+
async getOrCreateSubSession(platform, channelId, threadId, subtaskId, agent) {
|
|
460
|
+
const key = `${platform}:${channelId}:${threadId}:sub:${subtaskId}`;
|
|
461
|
+
const now = new Date();
|
|
462
|
+
let session = this.sessions.get(key) || await this.loadSession(key);
|
|
463
|
+
if (session) {
|
|
464
|
+
session.lastActivity = now;
|
|
465
|
+
return session;
|
|
466
|
+
}
|
|
467
|
+
session = {
|
|
468
|
+
id: `sub-${platform}-${channelId}-${threadId}-${subtaskId}`,
|
|
469
|
+
channelId, threadId, platform, agent,
|
|
470
|
+
createdAt: now, lastActivity: now, ttl: DEFAULT_TTL, messages: [],
|
|
471
|
+
};
|
|
472
|
+
this.sessions.set(key, session);
|
|
473
|
+
await this.saveSession(key, session);
|
|
474
|
+
return session;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Set active subtask id on parent session — subsequent messages route to the subtask.
|
|
478
|
+
*/
|
|
479
|
+
async setActiveSubtask(platform, channelId, threadId, taskId) {
|
|
480
|
+
const key = `${platform}:${channelId}:${threadId}`;
|
|
481
|
+
const session = this.sessions.get(key) || await this.loadSession(key);
|
|
482
|
+
if (!session)
|
|
483
|
+
return;
|
|
484
|
+
session.activeSubtaskId = taskId;
|
|
485
|
+
this.sessions.set(key, session);
|
|
486
|
+
await this.saveSession(key, session);
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Get subtask metadata list from parent session.
|
|
490
|
+
*/
|
|
491
|
+
async getSubtasks(platform, channelId, threadId) {
|
|
492
|
+
const key = `${platform}:${channelId}:${threadId}`;
|
|
493
|
+
const session = this.sessions.get(key) || await this.loadSession(key);
|
|
494
|
+
return session?.subtasks || [];
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Scan all session files on disk and return every subtask, flattened, with
|
|
498
|
+
* its parent platform/channelId/threadId/agent attached so the dashboard
|
|
499
|
+
* can render subtasks across all conversations.
|
|
500
|
+
*
|
|
501
|
+
* Session files live as `<sanitized-key>.json` under SESSIONS_DIR. The
|
|
502
|
+
* sanitized key is one-way (sha256-prefix per non-alnum char), so we
|
|
503
|
+
* cannot reverse it — but each session file preserves the original
|
|
504
|
+
* platform/channelId/threadId fields, which is what we need.
|
|
505
|
+
*/
|
|
506
|
+
async listAllSubtasks(opts = {}) {
|
|
507
|
+
let names;
|
|
508
|
+
try {
|
|
509
|
+
names = await readdir(SESSIONS_DIR);
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
return [];
|
|
513
|
+
}
|
|
514
|
+
const out = [];
|
|
515
|
+
for (const name of names) {
|
|
516
|
+
if (!name.endsWith('.json'))
|
|
517
|
+
continue;
|
|
518
|
+
try {
|
|
519
|
+
const raw = await readFile(join(SESSIONS_DIR, name), 'utf-8');
|
|
520
|
+
const parsed = JSON.parse(raw);
|
|
521
|
+
if (!parsed.subtasks?.length)
|
|
522
|
+
continue;
|
|
523
|
+
// Filter by parent-agent up-front so we don't allocate items we'll
|
|
524
|
+
// discard. Subtasks inherit the parent session's agent — there is
|
|
525
|
+
// no per-subtask agent override today.
|
|
526
|
+
if (opts.agent && (parsed.agent || '') !== opts.agent)
|
|
527
|
+
continue;
|
|
528
|
+
for (const st of parsed.subtasks) {
|
|
529
|
+
out.push({
|
|
530
|
+
...st,
|
|
531
|
+
createdAt: st.createdAt ? new Date(st.createdAt) : new Date(0),
|
|
532
|
+
completedAt: st.completedAt ? new Date(st.completedAt) : undefined,
|
|
533
|
+
platform: parsed.platform || '',
|
|
534
|
+
channelId: parsed.channelId || '',
|
|
535
|
+
threadId: parsed.threadId || '',
|
|
536
|
+
parentAgent: parsed.agent || '',
|
|
537
|
+
parentSessionId: parsed.id || '',
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
// skip corrupt session file
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// newest first
|
|
546
|
+
out.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
547
|
+
return out;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Update subtask metadata in parent session.
|
|
551
|
+
*/
|
|
552
|
+
async updateSubtask(platform, channelId, threadId, taskId, patch) {
|
|
553
|
+
const key = `${platform}:${channelId}:${threadId}`;
|
|
554
|
+
const session = this.sessions.get(key) || await this.loadSession(key);
|
|
555
|
+
if (!session)
|
|
556
|
+
return;
|
|
557
|
+
if (!session.subtasks) {
|
|
558
|
+
session.subtasks = [];
|
|
559
|
+
}
|
|
560
|
+
const idx = session.subtasks.findIndex(s => s.id === taskId);
|
|
561
|
+
if (idx >= 0) {
|
|
562
|
+
session.subtasks[idx] = { ...session.subtasks[idx], ...patch };
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
session.subtasks.push({ id: taskId, ...patch });
|
|
566
|
+
}
|
|
567
|
+
this.sessions.set(key, session);
|
|
568
|
+
await this.saveSession(key, session);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Get next subtask id and persist the increment.
|
|
572
|
+
*
|
|
573
|
+
* Previously returned 1 when the parent session didn't exist yet, but
|
|
574
|
+
* never created one — second call returned 1 again, leading to subtask
|
|
575
|
+
* id collisions. Now we lazy-create the parent session so the counter
|
|
576
|
+
* increments durably from the first call.
|
|
577
|
+
*/
|
|
578
|
+
async nextSubtaskId(platform, channelId, threadId, agent = '') {
|
|
579
|
+
const key = `${platform}:${channelId}:${threadId}`;
|
|
580
|
+
let session = this.sessions.get(key) || await this.loadSession(key);
|
|
581
|
+
if (!session) {
|
|
582
|
+
const now = new Date();
|
|
583
|
+
session = {
|
|
584
|
+
id: `${platform}-${channelId}-${threadId}-${Date.now()}-${randomBytes(4).toString('hex')}`,
|
|
585
|
+
channelId, threadId, platform, agent,
|
|
586
|
+
createdAt: now, lastActivity: now, ttl: DEFAULT_TTL, messages: [],
|
|
587
|
+
subtaskCounter: 0,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
session.subtaskCounter = (session.subtaskCounter || 0) + 1;
|
|
591
|
+
this.sessions.set(key, session);
|
|
592
|
+
await this.saveSession(key, session);
|
|
593
|
+
return session.subtaskCounter;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Persist the full session (metadata + messages). Used for the legacy
|
|
597
|
+
* one-file format on resetConversation() and switchAgent() — anywhere
|
|
598
|
+
* the messages array itself was rewritten. Atomic via tmp+rename.
|
|
599
|
+
*/
|
|
600
|
+
async saveSession(key, session) {
|
|
601
|
+
await this.saveSessionMeta(key, session);
|
|
602
|
+
// Rewrite the JSONL log to match the in-memory messages array. This is
|
|
603
|
+
// only called from paths that actually mutate `messages` wholesale
|
|
604
|
+
// (resetConversation, switchAgent). addMessage uses appendFile which
|
|
605
|
+
// is far cheaper for the hot path.
|
|
606
|
+
const logPath = sessionLogPath(key);
|
|
607
|
+
try {
|
|
608
|
+
const lines = session.messages.map((m) => JSON.stringify(m)).join('\n');
|
|
609
|
+
await this.atomicWrite(logPath, lines + (lines ? '\n' : ''));
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
// disk failure — in-memory state is still authoritative
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/** Persist metadata only (no messages payload), atomically. */
|
|
616
|
+
async saveSessionMeta(key, session) {
|
|
617
|
+
const filePath = sessionFilePath(key);
|
|
618
|
+
try {
|
|
619
|
+
const meta = {
|
|
620
|
+
id: session.id,
|
|
621
|
+
channelId: session.channelId,
|
|
622
|
+
threadId: session.threadId,
|
|
623
|
+
platform: session.platform,
|
|
624
|
+
agent: session.agent,
|
|
625
|
+
model: session.model,
|
|
626
|
+
variant: session.variant,
|
|
627
|
+
usage: session.usage,
|
|
628
|
+
createdAt: session.createdAt,
|
|
629
|
+
lastActivity: session.lastActivity,
|
|
630
|
+
ttl: session.ttl,
|
|
631
|
+
activeSubtaskId: session.activeSubtaskId,
|
|
632
|
+
subtasks: session.subtasks,
|
|
633
|
+
subtaskCounter: session.subtaskCounter,
|
|
634
|
+
claudeSessionId: session.claudeSessionId,
|
|
635
|
+
claudeSessionPrimed: session.claudeSessionPrimed,
|
|
636
|
+
opencodeSessionId: session.opencodeSessionId,
|
|
637
|
+
codexSessionId: session.codexSessionId,
|
|
638
|
+
planMode: session.planMode,
|
|
639
|
+
messageCount: session.messages.length,
|
|
640
|
+
};
|
|
641
|
+
await this.atomicWrite(filePath, JSON.stringify(meta, null, 2));
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
// ignore
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/** Crash-safe write: tmp file + atomic rename.
|
|
648
|
+
*
|
|
649
|
+
* Recovers from ENOENT (parent dir missing) by mkdir-recursive + retry
|
|
650
|
+
* exactly once. This keeps the manager robust to environments where
|
|
651
|
+
* start() wasn't called yet — tests in particular tend to import
|
|
652
|
+
* sessionManager without going through the start() lifecycle, and
|
|
653
|
+
* saveSessionMeta's outer try/catch would otherwise swallow the ENOENT
|
|
654
|
+
* and silently produce a no-op write (the bug that caused
|
|
655
|
+
* session-subtasks.test.ts to fail on a fresh CI runner). */
|
|
656
|
+
async atomicWrite(filePath, contents) {
|
|
657
|
+
const tmp = `${filePath}.${randomBytes(4).toString('hex')}.tmp`;
|
|
658
|
+
try {
|
|
659
|
+
await writeFile(tmp, contents);
|
|
660
|
+
}
|
|
661
|
+
catch (err) {
|
|
662
|
+
if (err.code === 'ENOENT') {
|
|
663
|
+
await mkdir(SESSIONS_DIR, { recursive: true });
|
|
664
|
+
await writeFile(tmp, contents);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
throw err;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
try {
|
|
671
|
+
await rename(tmp, filePath);
|
|
672
|
+
}
|
|
673
|
+
catch (err) {
|
|
674
|
+
try {
|
|
675
|
+
await unlink(tmp);
|
|
676
|
+
}
|
|
677
|
+
catch { /* ignore */ }
|
|
678
|
+
throw err;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
async loadSession(key) {
|
|
682
|
+
const filePath = sessionFilePath(key);
|
|
683
|
+
try {
|
|
684
|
+
const data = await readFile(filePath, 'utf-8');
|
|
685
|
+
const parsed = JSON.parse(data);
|
|
686
|
+
const session = {
|
|
687
|
+
id: parsed.id,
|
|
688
|
+
channelId: parsed.channelId,
|
|
689
|
+
threadId: parsed.threadId,
|
|
690
|
+
platform: parsed.platform,
|
|
691
|
+
agent: parsed.agent,
|
|
692
|
+
model: parsed.model,
|
|
693
|
+
variant: parsed.variant,
|
|
694
|
+
usage: parsed.usage,
|
|
695
|
+
createdAt: new Date(parsed.createdAt),
|
|
696
|
+
lastActivity: new Date(parsed.lastActivity),
|
|
697
|
+
ttl: parsed.ttl,
|
|
698
|
+
messages: parsed.messages || [], // legacy one-file format
|
|
699
|
+
activeSubtaskId: parsed.activeSubtaskId,
|
|
700
|
+
subtasks: parsed.subtasks,
|
|
701
|
+
subtaskCounter: parsed.subtaskCounter,
|
|
702
|
+
claudeSessionId: parsed.claudeSessionId,
|
|
703
|
+
claudeSessionPrimed: parsed.claudeSessionPrimed,
|
|
704
|
+
opencodeSessionId: parsed.opencodeSessionId,
|
|
705
|
+
codexSessionId: parsed.codexSessionId,
|
|
706
|
+
planMode: parsed.planMode,
|
|
707
|
+
};
|
|
708
|
+
// Convert message timestamps from legacy format if present
|
|
709
|
+
session.messages = session.messages.map((msg) => ({
|
|
710
|
+
...msg,
|
|
711
|
+
timestamp: new Date(msg.timestamp),
|
|
712
|
+
}));
|
|
713
|
+
// Then merge in JSONL log entries (new format).
|
|
714
|
+
try {
|
|
715
|
+
const log = await readFile(sessionLogPath(key), 'utf-8');
|
|
716
|
+
const logged = [];
|
|
717
|
+
for (const line of log.split('\n')) {
|
|
718
|
+
if (!line.trim())
|
|
719
|
+
continue;
|
|
720
|
+
try {
|
|
721
|
+
const m = JSON.parse(line);
|
|
722
|
+
logged.push({ ...m, timestamp: new Date(m.timestamp) });
|
|
723
|
+
}
|
|
724
|
+
catch { /* skip corrupt line */ }
|
|
725
|
+
}
|
|
726
|
+
// The log is authoritative for new-format sessions. If both exist
|
|
727
|
+
// (rare, after a save followed by addMessage), the log wins.
|
|
728
|
+
if (logged.length > 0) {
|
|
729
|
+
session.messages = logged;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
// No log file — legacy format only
|
|
734
|
+
}
|
|
735
|
+
return session;
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
return undefined;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async cleanup() {
|
|
742
|
+
const now = Date.now();
|
|
743
|
+
for (const [key, session] of this.sessions.entries()) {
|
|
744
|
+
const idle = now - session.lastActivity.getTime();
|
|
745
|
+
if (idle > META_TTL) {
|
|
746
|
+
// Full eviction: thread truly cold. Drop both files + cache entry.
|
|
747
|
+
this.sessions.delete(key);
|
|
748
|
+
const filePath = sessionFilePath(key);
|
|
749
|
+
const logPath = sessionLogPath(key);
|
|
750
|
+
try {
|
|
751
|
+
await unlink(filePath);
|
|
752
|
+
}
|
|
753
|
+
catch { /* ignore */ }
|
|
754
|
+
try {
|
|
755
|
+
await unlink(logPath);
|
|
756
|
+
}
|
|
757
|
+
catch { /* ignore */ }
|
|
758
|
+
}
|
|
759
|
+
else if (idle > MESSAGES_TTL && session.messages.length > 0) {
|
|
760
|
+
// Messages-only eviction: keep sticky agent / claudeSessionId on disk
|
|
761
|
+
// (meta file untouched), drop chat log + in-memory messages so the
|
|
762
|
+
// next turn starts with a fresh history but the same routing.
|
|
763
|
+
session.messages = [];
|
|
764
|
+
const logPath = sessionLogPath(key);
|
|
765
|
+
try {
|
|
766
|
+
await unlink(logPath);
|
|
767
|
+
}
|
|
768
|
+
catch { /* ignore */ }
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
export const sessionManager = new SessionManager();
|
|
774
|
+
//# sourceMappingURL=session.js.map
|