@vibesdotdev/session 0.0.0-stage → 1.0.0-rc.2
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 +18 -0
- package/README.md +69 -2
- package/dist/admission/admit-caller.context.descriptor.d.ts +33 -0
- package/dist/admission/admit-caller.context.descriptor.d.ts.map +1 -0
- package/dist/admission/admit-caller.context.descriptor.js +10 -0
- package/dist/admission/admit-caller.context.descriptor.js.map +1 -0
- package/dist/admission/admit-caller.context.impl.d.ts +8 -0
- package/dist/admission/admit-caller.context.impl.d.ts.map +1 -0
- package/dist/admission/admit-caller.context.impl.js +15 -0
- package/dist/admission/admit-caller.context.impl.js.map +1 -0
- package/dist/admission/admit-caller.d.ts +57 -0
- package/dist/admission/admit-caller.d.ts.map +1 -0
- package/dist/admission/admit-caller.js +163 -0
- package/dist/admission/admit-caller.js.map +1 -0
- package/dist/backup/session.sessions.target.descriptor.d.ts +47 -0
- package/dist/backup/session.sessions.target.descriptor.d.ts.map +1 -0
- package/dist/backup/session.sessions.target.descriptor.js +41 -0
- package/dist/backup/session.sessions.target.descriptor.js.map +1 -0
- package/dist/cli/chat/session.chat.descriptor.d.ts +4 -0
- package/dist/cli/chat/session.chat.descriptor.d.ts.map +1 -0
- package/dist/cli/chat/session.chat.descriptor.js +19 -0
- package/dist/cli/chat/session.chat.descriptor.js.map +1 -0
- package/dist/cli/chat/session.chat.impl.d.ts +6 -0
- package/dist/cli/chat/session.chat.impl.d.ts.map +1 -0
- package/dist/cli/chat/session.chat.impl.js +37 -0
- package/dist/cli/chat/session.chat.impl.js.map +1 -0
- package/dist/cli/chat/session.jobs.descriptor.d.ts +4 -0
- package/dist/cli/chat/session.jobs.descriptor.d.ts.map +1 -0
- package/dist/cli/chat/session.jobs.descriptor.js +17 -0
- package/dist/cli/chat/session.jobs.descriptor.js.map +1 -0
- package/dist/cli/chat/session.jobs.impl.d.ts +6 -0
- package/dist/cli/chat/session.jobs.impl.d.ts.map +1 -0
- package/dist/cli/chat/session.jobs.impl.js +29 -0
- package/dist/cli/chat/session.jobs.impl.js.map +1 -0
- package/dist/cli/chat/session.participants.descriptor.d.ts +4 -0
- package/dist/cli/chat/session.participants.descriptor.d.ts.map +1 -0
- package/dist/cli/chat/session.participants.descriptor.js +18 -0
- package/dist/cli/chat/session.participants.descriptor.js.map +1 -0
- package/dist/cli/chat/session.participants.impl.d.ts +12 -0
- package/dist/cli/chat/session.participants.impl.d.ts.map +1 -0
- package/dist/cli/chat/session.participants.impl.js +37 -0
- package/dist/cli/chat/session.participants.impl.js.map +1 -0
- package/dist/cli/command.d.ts +44 -0
- package/dist/cli/command.d.ts.map +1 -0
- package/dist/cli/command.js +57 -0
- package/dist/cli/command.js.map +1 -0
- package/dist/cli/display.d.ts +10 -0
- package/dist/cli/display.d.ts.map +1 -0
- package/dist/cli/display.js +16 -0
- package/dist/cli/display.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/lifecycle/session.archive.descriptor.d.ts +4 -0
- package/dist/cli/lifecycle/session.archive.descriptor.d.ts.map +1 -0
- package/dist/cli/lifecycle/session.archive.descriptor.js +16 -0
- package/dist/cli/lifecycle/session.archive.descriptor.js.map +1 -0
- package/dist/cli/lifecycle/session.archive.impl.d.ts +6 -0
- package/dist/cli/lifecycle/session.archive.impl.d.ts.map +1 -0
- package/dist/cli/lifecycle/session.archive.impl.js +18 -0
- package/dist/cli/lifecycle/session.archive.impl.js.map +1 -0
- package/dist/cli/lifecycle/session.init.descriptor.d.ts +4 -0
- package/dist/cli/lifecycle/session.init.descriptor.d.ts.map +1 -0
- package/dist/cli/lifecycle/session.init.descriptor.js +31 -0
- package/dist/cli/lifecycle/session.init.descriptor.js.map +1 -0
- package/dist/cli/lifecycle/session.init.impl.d.ts +6 -0
- package/dist/cli/lifecycle/session.init.impl.d.ts.map +1 -0
- package/dist/cli/lifecycle/session.init.impl.js +38 -0
- package/dist/cli/lifecycle/session.init.impl.js.map +1 -0
- package/dist/cli/lifecycle/session.list.descriptor.d.ts +4 -0
- package/dist/cli/lifecycle/session.list.descriptor.d.ts.map +1 -0
- package/dist/cli/lifecycle/session.list.descriptor.js +27 -0
- package/dist/cli/lifecycle/session.list.descriptor.js.map +1 -0
- package/dist/cli/lifecycle/session.list.impl.d.ts +12 -0
- package/dist/cli/lifecycle/session.list.impl.d.ts.map +1 -0
- package/dist/cli/lifecycle/session.list.impl.js +46 -0
- package/dist/cli/lifecycle/session.list.impl.js.map +1 -0
- package/dist/cli/lifecycle/session.pause.descriptor.d.ts +4 -0
- package/dist/cli/lifecycle/session.pause.descriptor.d.ts.map +1 -0
- package/dist/cli/lifecycle/session.pause.descriptor.js +13 -0
- package/dist/cli/lifecycle/session.pause.descriptor.js.map +1 -0
- package/dist/cli/lifecycle/session.pause.impl.d.ts +6 -0
- package/dist/cli/lifecycle/session.pause.impl.d.ts.map +1 -0
- package/dist/cli/lifecycle/session.pause.impl.js +18 -0
- package/dist/cli/lifecycle/session.pause.impl.js.map +1 -0
- package/dist/cli/lifecycle/session.resume.descriptor.d.ts +4 -0
- package/dist/cli/lifecycle/session.resume.descriptor.d.ts.map +1 -0
- package/dist/cli/lifecycle/session.resume.descriptor.js +21 -0
- package/dist/cli/lifecycle/session.resume.descriptor.js.map +1 -0
- package/dist/cli/lifecycle/session.resume.impl.d.ts +6 -0
- package/dist/cli/lifecycle/session.resume.impl.d.ts.map +1 -0
- package/dist/cli/lifecycle/session.resume.impl.js +18 -0
- package/dist/cli/lifecycle/session.resume.impl.js.map +1 -0
- package/dist/cli/lifecycle/session.search.descriptor.d.ts +4 -0
- package/dist/cli/lifecycle/session.search.descriptor.d.ts.map +1 -0
- package/dist/cli/lifecycle/session.search.descriptor.js +17 -0
- package/dist/cli/lifecycle/session.search.descriptor.js.map +1 -0
- package/dist/cli/lifecycle/session.search.impl.d.ts +12 -0
- package/dist/cli/lifecycle/session.search.impl.d.ts.map +1 -0
- package/dist/cli/lifecycle/session.search.impl.js +43 -0
- package/dist/cli/lifecycle/session.search.impl.js.map +1 -0
- package/dist/cli/lifecycle/session.status.descriptor.d.ts +4 -0
- package/dist/cli/lifecycle/session.status.descriptor.d.ts.map +1 -0
- package/dist/cli/lifecycle/session.status.descriptor.js +13 -0
- package/dist/cli/lifecycle/session.status.descriptor.js.map +1 -0
- package/dist/cli/lifecycle/session.status.impl.d.ts +6 -0
- package/dist/cli/lifecycle/session.status.impl.d.ts.map +1 -0
- package/dist/cli/lifecycle/session.status.impl.js +38 -0
- package/dist/cli/lifecycle/session.status.impl.js.map +1 -0
- package/dist/cli/output.d.ts +14 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +19 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/schemas/commands.d.ts +709 -0
- package/dist/cli/schemas/commands.d.ts.map +1 -0
- package/dist/cli/schemas/commands.js +359 -0
- package/dist/cli/schemas/commands.js.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.cli-group.descriptor.d.ts +4 -0
- package/dist/cli/scratchpad/session.scratchpad.cli-group.descriptor.d.ts.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.cli-group.descriptor.js +13 -0
- package/dist/cli/scratchpad/session.scratchpad.cli-group.descriptor.js.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.list.descriptor.d.ts +4 -0
- package/dist/cli/scratchpad/session.scratchpad.list.descriptor.d.ts.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.list.descriptor.js +16 -0
- package/dist/cli/scratchpad/session.scratchpad.list.descriptor.js.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.list.impl.d.ts +6 -0
- package/dist/cli/scratchpad/session.scratchpad.list.impl.d.ts.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.list.impl.js +33 -0
- package/dist/cli/scratchpad/session.scratchpad.list.impl.js.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.read.descriptor.d.ts +4 -0
- package/dist/cli/scratchpad/session.scratchpad.read.descriptor.d.ts.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.read.descriptor.js +13 -0
- package/dist/cli/scratchpad/session.scratchpad.read.descriptor.js.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.read.impl.d.ts +6 -0
- package/dist/cli/scratchpad/session.scratchpad.read.impl.d.ts.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.read.impl.js +18 -0
- package/dist/cli/scratchpad/session.scratchpad.read.impl.js.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.search.descriptor.d.ts +4 -0
- package/dist/cli/scratchpad/session.scratchpad.search.descriptor.d.ts.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.search.descriptor.js +18 -0
- package/dist/cli/scratchpad/session.scratchpad.search.descriptor.js.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.search.impl.d.ts +6 -0
- package/dist/cli/scratchpad/session.scratchpad.search.impl.d.ts.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.search.impl.js +29 -0
- package/dist/cli/scratchpad/session.scratchpad.search.impl.js.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.write.descriptor.d.ts +4 -0
- package/dist/cli/scratchpad/session.scratchpad.write.descriptor.d.ts.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.write.descriptor.js +23 -0
- package/dist/cli/scratchpad/session.scratchpad.write.descriptor.js.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.write.impl.d.ts +6 -0
- package/dist/cli/scratchpad/session.scratchpad.write.impl.d.ts.map +1 -0
- package/dist/cli/scratchpad/session.scratchpad.write.impl.js +18 -0
- package/dist/cli/scratchpad/session.scratchpad.write.impl.js.map +1 -0
- package/dist/cli/services/session/jobs.d.ts +61 -0
- package/dist/cli/services/session/jobs.d.ts.map +1 -0
- package/dist/cli/services/session/jobs.js +36 -0
- package/dist/cli/services/session/jobs.js.map +1 -0
- package/dist/cli/services/session/participant.d.ts +9 -0
- package/dist/cli/services/session/participant.d.ts.map +1 -0
- package/dist/cli/services/session/participant.js +93 -0
- package/dist/cli/services/session/participant.js.map +1 -0
- package/dist/cli/services/session/participants.d.ts +84 -0
- package/dist/cli/services/session/participants.d.ts.map +1 -0
- package/dist/cli/services/session/participants.js +33 -0
- package/dist/cli/services/session/participants.js.map +1 -0
- package/dist/cli/services/session/refusals.d.ts +15 -0
- package/dist/cli/services/session/refusals.d.ts.map +1 -0
- package/dist/cli/services/session/refusals.js +28 -0
- package/dist/cli/services/session/refusals.js.map +1 -0
- package/dist/cli/services/session/scratchpad.d.ts +89 -0
- package/dist/cli/services/session/scratchpad.d.ts.map +1 -0
- package/dist/cli/services/session/scratchpad.js +63 -0
- package/dist/cli/services/session/scratchpad.js.map +1 -0
- package/dist/cli/services/session/sessions.d.ts +277 -0
- package/dist/cli/services/session/sessions.d.ts.map +1 -0
- package/dist/cli/services/session/sessions.js +149 -0
- package/dist/cli/services/session/sessions.js.map +1 -0
- package/dist/cli/services/session/todos.d.ts +132 -0
- package/dist/cli/services/session/todos.d.ts.map +1 -0
- package/dist/cli/services/session/todos.js +85 -0
- package/dist/cli/services/session/todos.js.map +1 -0
- package/dist/cli/services/session-cli-context.d.ts +29 -0
- package/dist/cli/services/session-cli-context.d.ts.map +1 -0
- package/dist/cli/services/session-cli-context.js +21 -0
- package/dist/cli/services/session-cli-context.js.map +1 -0
- package/dist/cli/services/session.cli.consumer.d.ts +58 -0
- package/dist/cli/services/session.cli.consumer.d.ts.map +1 -0
- package/dist/cli/services/session.cli.consumer.js +13 -0
- package/dist/cli/services/session.cli.consumer.js.map +1 -0
- package/dist/cli/session-cli.plugin.d.ts +3 -0
- package/dist/cli/session-cli.plugin.d.ts.map +1 -0
- package/dist/cli/session-cli.plugin.js +150 -0
- package/dist/cli/session-cli.plugin.js.map +1 -0
- package/dist/cli/session.cli-group.descriptor.d.ts +4 -0
- package/dist/cli/session.cli-group.descriptor.d.ts.map +1 -0
- package/dist/cli/session.cli-group.descriptor.js +12 -0
- package/dist/cli/session.cli-group.descriptor.js.map +1 -0
- package/dist/cli/session.cli.descriptor.d.ts +4 -0
- package/dist/cli/session.cli.descriptor.d.ts.map +1 -0
- package/dist/cli/session.cli.descriptor.js +13 -0
- package/dist/cli/session.cli.descriptor.js.map +1 -0
- package/dist/cli/todo/session.todo.add.descriptor.d.ts +4 -0
- package/dist/cli/todo/session.todo.add.descriptor.d.ts.map +1 -0
- package/dist/cli/todo/session.todo.add.descriptor.js +23 -0
- package/dist/cli/todo/session.todo.add.descriptor.js.map +1 -0
- package/dist/cli/todo/session.todo.add.impl.d.ts +6 -0
- package/dist/cli/todo/session.todo.add.impl.d.ts.map +1 -0
- package/dist/cli/todo/session.todo.add.impl.js +22 -0
- package/dist/cli/todo/session.todo.add.impl.js.map +1 -0
- package/dist/cli/todo/session.todo.cli-group.descriptor.d.ts +4 -0
- package/dist/cli/todo/session.todo.cli-group.descriptor.d.ts.map +1 -0
- package/dist/cli/todo/session.todo.cli-group.descriptor.js +12 -0
- package/dist/cli/todo/session.todo.cli-group.descriptor.js.map +1 -0
- package/dist/cli/todo/session.todo.complete.descriptor.d.ts +4 -0
- package/dist/cli/todo/session.todo.complete.descriptor.d.ts.map +1 -0
- package/dist/cli/todo/session.todo.complete.descriptor.js +16 -0
- package/dist/cli/todo/session.todo.complete.descriptor.js.map +1 -0
- package/dist/cli/todo/session.todo.complete.impl.d.ts +6 -0
- package/dist/cli/todo/session.todo.complete.impl.d.ts.map +1 -0
- package/dist/cli/todo/session.todo.complete.impl.js +18 -0
- package/dist/cli/todo/session.todo.complete.impl.js.map +1 -0
- package/dist/cli/todo/session.todo.list.descriptor.d.ts +4 -0
- package/dist/cli/todo/session.todo.list.descriptor.d.ts.map +1 -0
- package/dist/cli/todo/session.todo.list.descriptor.js +19 -0
- package/dist/cli/todo/session.todo.list.descriptor.js.map +1 -0
- package/dist/cli/todo/session.todo.list.impl.d.ts +6 -0
- package/dist/cli/todo/session.todo.list.impl.d.ts.map +1 -0
- package/dist/cli/todo/session.todo.list.impl.js +34 -0
- package/dist/cli/todo/session.todo.list.impl.js.map +1 -0
- package/dist/cli/todo/session.todo.tree.descriptor.d.ts +4 -0
- package/dist/cli/todo/session.todo.tree.descriptor.d.ts.map +1 -0
- package/dist/cli/todo/session.todo.tree.descriptor.js +21 -0
- package/dist/cli/todo/session.todo.tree.descriptor.js.map +1 -0
- package/dist/cli/todo/session.todo.tree.impl.d.ts +6 -0
- package/dist/cli/todo/session.todo.tree.impl.d.ts.map +1 -0
- package/dist/cli/todo/session.todo.tree.impl.js +26 -0
- package/dist/cli/todo/session.todo.tree.impl.js.map +1 -0
- package/dist/cli/todo/session.todo.update.descriptor.d.ts +4 -0
- package/dist/cli/todo/session.todo.update.descriptor.d.ts.map +1 -0
- package/dist/cli/todo/session.todo.update.descriptor.js +21 -0
- package/dist/cli/todo/session.todo.update.descriptor.js.map +1 -0
- package/dist/cli/todo/session.todo.update.impl.d.ts +6 -0
- package/dist/cli/todo/session.todo.update.impl.d.ts.map +1 -0
- package/dist/cli/todo/session.todo.update.impl.js +18 -0
- package/dist/cli/todo/session.todo.update.impl.js.map +1 -0
- package/dist/client.d.ts +40 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +114 -0
- package/dist/client.js.map +1 -0
- package/dist/cockpit/build-state.d.ts +104 -0
- package/dist/cockpit/build-state.d.ts.map +1 -0
- package/dist/cockpit/build-state.js +273 -0
- package/dist/cockpit/build-state.js.map +1 -0
- package/dist/cockpit/resolve-participant.d.ts +55 -0
- package/dist/cockpit/resolve-participant.d.ts.map +1 -0
- package/dist/cockpit/resolve-participant.js +116 -0
- package/dist/cockpit/resolve-participant.js.map +1 -0
- package/dist/docs/session.docs.descriptor.d.ts +24 -0
- package/dist/docs/session.docs.descriptor.d.ts.map +1 -0
- package/dist/docs/session.docs.descriptor.js +589 -0
- package/dist/docs/session.docs.descriptor.js.map +1 -0
- package/dist/events/session.bound.event.descriptor.d.ts +100 -0
- package/dist/events/session.bound.event.descriptor.d.ts.map +1 -0
- package/dist/events/session.bound.event.descriptor.js +121 -0
- package/dist/events/session.bound.event.descriptor.js.map +1 -0
- package/dist/events/session.unbound.event.descriptor.d.ts +58 -0
- package/dist/events/session.unbound.event.descriptor.d.ts.map +1 -0
- package/dist/events/session.unbound.event.descriptor.js +45 -0
- package/dist/events/session.unbound.event.descriptor.js.map +1 -0
- package/dist/helper.d.ts +15 -0
- package/dist/helper.d.ts.map +1 -0
- package/dist/helper.js +23 -0
- package/dist/helper.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge-source/vibes.descriptor.d.ts +10 -0
- package/dist/knowledge-source/vibes.descriptor.d.ts.map +1 -0
- package/dist/knowledge-source/vibes.descriptor.js +10 -0
- package/dist/knowledge-source/vibes.descriptor.js.map +1 -0
- package/dist/knowledge-source/vibes.impl.consumer.d.ts +47 -0
- package/dist/knowledge-source/vibes.impl.consumer.d.ts.map +1 -0
- package/dist/knowledge-source/vibes.impl.consumer.js +457 -0
- package/dist/knowledge-source/vibes.impl.consumer.js.map +1 -0
- package/dist/manager/cli-body-connection.d.ts +4 -0
- package/dist/manager/cli-body-connection.d.ts.map +1 -0
- package/dist/manager/cli-body-connection.js +7 -0
- package/dist/manager/cli-body-connection.js.map +1 -0
- package/dist/manager/index.d.ts +3 -0
- package/dist/manager/index.d.ts.map +1 -0
- package/dist/manager/index.js +2 -0
- package/dist/manager/index.js.map +1 -0
- package/dist/manager/resolve-bound-session.d.ts +95 -0
- package/dist/manager/resolve-bound-session.d.ts.map +1 -0
- package/dist/manager/resolve-bound-session.js +163 -0
- package/dist/manager/resolve-bound-session.js.map +1 -0
- package/dist/manager.d.ts +715 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +3029 -0
- package/dist/manager.js.map +1 -0
- package/dist/mcp/index.d.ts +14 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +11 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/schemas/coordination.d.ts +20 -0
- package/dist/mcp/schemas/coordination.d.ts.map +1 -0
- package/dist/mcp/schemas/coordination.js +5 -0
- package/dist/mcp/schemas/coordination.js.map +1 -0
- package/dist/mcp/schemas/tools.d.ts +351 -0
- package/dist/mcp/schemas/tools.d.ts.map +1 -0
- package/dist/mcp/schemas/tools.js +279 -0
- package/dist/mcp/schemas/tools.js.map +1 -0
- package/dist/mcp/types.d.ts +3 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/mcp/utils/coordination.d.ts +17 -0
- package/dist/mcp/utils/coordination.d.ts.map +1 -0
- package/dist/mcp/utils/coordination.js +56 -0
- package/dist/mcp/utils/coordination.js.map +1 -0
- package/dist/mcp/utils/knowledge.d.ts +90 -0
- package/dist/mcp/utils/knowledge.d.ts.map +1 -0
- package/dist/mcp/utils/knowledge.js +455 -0
- package/dist/mcp/utils/knowledge.js.map +1 -0
- package/dist/mcp/utils/mcp-session-resolver.d.ts +19 -0
- package/dist/mcp/utils/mcp-session-resolver.d.ts.map +1 -0
- package/dist/mcp/utils/mcp-session-resolver.js +28 -0
- package/dist/mcp/utils/mcp-session-resolver.js.map +1 -0
- package/dist/mcp/utils/participant.d.ts +426 -0
- package/dist/mcp/utils/participant.d.ts.map +1 -0
- package/dist/mcp/utils/participant.js +1420 -0
- package/dist/mcp/utils/participant.js.map +1 -0
- package/dist/mcp/utils/session-registry.d.ts +194 -0
- package/dist/mcp/utils/session-registry.d.ts.map +1 -0
- package/dist/mcp/utils/session-registry.js +479 -0
- package/dist/mcp/utils/session-registry.js.map +1 -0
- package/dist/mcp/utils/surface-sessions.d.ts +54 -0
- package/dist/mcp/utils/surface-sessions.d.ts.map +1 -0
- package/dist/mcp/utils/surface-sessions.js +118 -0
- package/dist/mcp/utils/surface-sessions.js.map +1 -0
- package/dist/mcp/utils/unbound-guidance.d.ts +97 -0
- package/dist/mcp/utils/unbound-guidance.d.ts.map +1 -0
- package/dist/mcp/utils/unbound-guidance.js +236 -0
- package/dist/mcp/utils/unbound-guidance.js.map +1 -0
- package/dist/schemas/chatter.d.ts +228 -0
- package/dist/schemas/chatter.d.ts.map +1 -0
- package/dist/schemas/chatter.js +130 -0
- package/dist/schemas/chatter.js.map +1 -0
- package/dist/schemas/cockpit.d.ts +332 -0
- package/dist/schemas/cockpit.d.ts.map +1 -0
- package/dist/schemas/cockpit.js +139 -0
- package/dist/schemas/cockpit.js.map +1 -0
- package/dist/schemas/harness-seat.d.ts +41 -0
- package/dist/schemas/harness-seat.d.ts.map +1 -0
- package/dist/schemas/harness-seat.js +61 -0
- package/dist/schemas/harness-seat.js.map +1 -0
- package/dist/schemas/index.d.ts +29 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +29 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/job.d.ts +261 -0
- package/dist/schemas/job.d.ts.map +1 -0
- package/dist/schemas/job.js +144 -0
- package/dist/schemas/job.js.map +1 -0
- package/dist/schemas/json-text.d.ts +9 -0
- package/dist/schemas/json-text.d.ts.map +1 -0
- package/dist/schemas/json-text.js +129 -0
- package/dist/schemas/json-text.js.map +1 -0
- package/dist/schemas/knowledge.d.ts +109 -0
- package/dist/schemas/knowledge.d.ts.map +1 -0
- package/dist/schemas/knowledge.js +81 -0
- package/dist/schemas/knowledge.js.map +1 -0
- package/dist/schemas/mcp.d.ts +179 -0
- package/dist/schemas/mcp.d.ts.map +1 -0
- package/dist/schemas/mcp.js +46 -0
- package/dist/schemas/mcp.js.map +1 -0
- package/dist/schemas/participant.d.ts +407 -0
- package/dist/schemas/participant.d.ts.map +1 -0
- package/dist/schemas/participant.js +225 -0
- package/dist/schemas/participant.js.map +1 -0
- package/dist/schemas/presence-core.d.ts +93 -0
- package/dist/schemas/presence-core.d.ts.map +1 -0
- package/dist/schemas/presence-core.js +53 -0
- package/dist/schemas/presence-core.js.map +1 -0
- package/dist/schemas/presence.d.ts +69 -0
- package/dist/schemas/presence.d.ts.map +1 -0
- package/dist/schemas/presence.js +44 -0
- package/dist/schemas/presence.js.map +1 -0
- package/dist/schemas/registry-state.d.ts +64 -0
- package/dist/schemas/registry-state.d.ts.map +1 -0
- package/dist/schemas/registry-state.js +30 -0
- package/dist/schemas/registry-state.js.map +1 -0
- package/dist/schemas/scratchpad.d.ts +163 -0
- package/dist/schemas/scratchpad.d.ts.map +1 -0
- package/dist/schemas/scratchpad.js +119 -0
- package/dist/schemas/scratchpad.js.map +1 -0
- package/dist/schemas/session.d.ts +266 -0
- package/dist/schemas/session.d.ts.map +1 -0
- package/dist/schemas/session.js +179 -0
- package/dist/schemas/session.js.map +1 -0
- package/dist/schemas/todo-status-category.d.ts +21 -0
- package/dist/schemas/todo-status-category.d.ts.map +1 -0
- package/dist/schemas/todo-status-category.js +27 -0
- package/dist/schemas/todo-status-category.js.map +1 -0
- package/dist/schemas/todo.d.ts +293 -0
- package/dist/schemas/todo.d.ts.map +1 -0
- package/dist/schemas/todo.js +130 -0
- package/dist/schemas/todo.js.map +1 -0
- package/dist/schemas/types.d.ts +15 -0
- package/dist/schemas/types.d.ts.map +1 -0
- package/dist/schemas/types.js +9 -0
- package/dist/schemas/types.js.map +1 -0
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +2 -0
- package/dist/schemas.js.map +1 -0
- package/dist/search/providers/scratchpad.d.ts +9 -0
- package/dist/search/providers/scratchpad.d.ts.map +1 -0
- package/dist/search/providers/scratchpad.js +64 -0
- package/dist/search/providers/scratchpad.js.map +1 -0
- package/dist/search/providers/session.d.ts +13 -0
- package/dist/search/providers/session.d.ts.map +1 -0
- package/dist/search/providers/session.js +48 -0
- package/dist/search/providers/session.js.map +1 -0
- package/dist/session.client.descriptor.d.ts +27 -0
- package/dist/session.client.descriptor.d.ts.map +1 -0
- package/dist/session.client.descriptor.js +8 -0
- package/dist/session.client.descriptor.js.map +1 -0
- package/dist/session.client.impl.d.ts +7 -0
- package/dist/session.client.impl.d.ts.map +1 -0
- package/dist/session.client.impl.js +9 -0
- package/dist/session.client.impl.js.map +1 -0
- package/dist/session.cloud.plugin.d.ts +3 -0
- package/dist/session.cloud.plugin.d.ts.map +1 -0
- package/dist/session.cloud.plugin.js +16 -0
- package/dist/session.cloud.plugin.js.map +1 -0
- package/dist/session.consumer.plugin.d.ts +3 -0
- package/dist/session.consumer.plugin.d.ts.map +1 -0
- package/dist/session.consumer.plugin.js +28 -0
- package/dist/session.consumer.plugin.js.map +1 -0
- package/dist/session.context.descriptor.d.ts +33 -0
- package/dist/session.context.descriptor.d.ts.map +1 -0
- package/dist/session.context.descriptor.js +9 -0
- package/dist/session.context.descriptor.js.map +1 -0
- package/dist/session.context.impl.consumer.d.ts +3 -0
- package/dist/session.context.impl.consumer.d.ts.map +1 -0
- package/dist/session.context.impl.consumer.js +3 -0
- package/dist/session.context.impl.consumer.js.map +1 -0
- package/dist/session.context.impl.d.ts +8 -0
- package/dist/session.context.impl.d.ts.map +1 -0
- package/dist/session.context.impl.js +13 -0
- package/dist/session.context.impl.js.map +1 -0
- package/dist/session.plugin.d.ts +3 -0
- package/dist/session.plugin.d.ts.map +1 -0
- package/dist/session.plugin.js +132 -0
- package/dist/session.plugin.js.map +1 -0
- package/dist/storage/record-draft.d.ts +9 -0
- package/dist/storage/record-draft.d.ts.map +1 -0
- package/dist/storage/record-draft.js +2 -0
- package/dist/storage/record-draft.js.map +1 -0
- package/dist/storage/session-connections.record.descriptor.d.ts +46 -0
- package/dist/storage/session-connections.record.descriptor.d.ts.map +1 -0
- package/dist/storage/session-connections.record.descriptor.js +126 -0
- package/dist/storage/session-connections.record.descriptor.js.map +1 -0
- package/dist/storage/session-jobs.record.descriptor.d.ts +86 -0
- package/dist/storage/session-jobs.record.descriptor.d.ts.map +1 -0
- package/dist/storage/session-jobs.record.descriptor.js +135 -0
- package/dist/storage/session-jobs.record.descriptor.js.map +1 -0
- package/dist/storage/session-knowledge-refs.record.descriptor.d.ts +48 -0
- package/dist/storage/session-knowledge-refs.record.descriptor.d.ts.map +1 -0
- package/dist/storage/session-knowledge-refs.record.descriptor.js +103 -0
- package/dist/storage/session-knowledge-refs.record.descriptor.js.map +1 -0
- package/dist/storage/session-membership-leases.record.descriptor.d.ts +39 -0
- package/dist/storage/session-membership-leases.record.descriptor.d.ts.map +1 -0
- package/dist/storage/session-membership-leases.record.descriptor.js +185 -0
- package/dist/storage/session-membership-leases.record.descriptor.js.map +1 -0
- package/dist/storage/session-messages.record.descriptor.d.ts +79 -0
- package/dist/storage/session-messages.record.descriptor.d.ts.map +1 -0
- package/dist/storage/session-messages.record.descriptor.js +142 -0
- package/dist/storage/session-messages.record.descriptor.js.map +1 -0
- package/dist/storage/session-participants.record.descriptor.d.ts +78 -0
- package/dist/storage/session-participants.record.descriptor.d.ts.map +1 -0
- package/dist/storage/session-participants.record.descriptor.js +119 -0
- package/dist/storage/session-participants.record.descriptor.js.map +1 -0
- package/dist/storage/session-presence.record.descriptor.d.ts +43 -0
- package/dist/storage/session-presence.record.descriptor.d.ts.map +1 -0
- package/dist/storage/session-presence.record.descriptor.js +141 -0
- package/dist/storage/session-presence.record.descriptor.js.map +1 -0
- package/dist/storage/session-scratchpad.record.descriptor.d.ts +54 -0
- package/dist/storage/session-scratchpad.record.descriptor.d.ts.map +1 -0
- package/dist/storage/session-scratchpad.record.descriptor.js +134 -0
- package/dist/storage/session-scratchpad.record.descriptor.js.map +1 -0
- package/dist/storage/session-sessions.record.descriptor.d.ts +68 -0
- package/dist/storage/session-sessions.record.descriptor.d.ts.map +1 -0
- package/dist/storage/session-sessions.record.descriptor.js +260 -0
- package/dist/storage/session-sessions.record.descriptor.js.map +1 -0
- package/dist/storage/session-state.record.descriptor.d.ts +37 -0
- package/dist/storage/session-state.record.descriptor.d.ts.map +1 -0
- package/dist/storage/session-state.record.descriptor.js +84 -0
- package/dist/storage/session-state.record.descriptor.js.map +1 -0
- package/dist/storage/session-todo.record.descriptor.d.ts +84 -0
- package/dist/storage/session-todo.record.descriptor.d.ts.map +1 -0
- package/dist/storage/session-todo.record.descriptor.js +245 -0
- package/dist/storage/session-todo.record.descriptor.js.map +1 -0
- package/dist/subscriptions/session.bound.scope.descriptor.d.ts +21 -0
- package/dist/subscriptions/session.bound.scope.descriptor.d.ts.map +1 -0
- package/dist/subscriptions/session.bound.scope.descriptor.js +21 -0
- package/dist/subscriptions/session.bound.scope.descriptor.js.map +1 -0
- package/dist/subscriptions/session.bound.scope.impl.d.ts +29 -0
- package/dist/subscriptions/session.bound.scope.impl.d.ts.map +1 -0
- package/dist/subscriptions/session.bound.scope.impl.js +40 -0
- package/dist/subscriptions/session.bound.scope.impl.js.map +1 -0
- package/dist/svelte/components/SessionCockpit.svelte +360 -0
- package/dist/svelte/components/SessionCockpit.svelte.d.ts +17 -0
- package/dist/svelte/components/SessionCockpit.svelte.d.ts.map +1 -0
- package/dist/svelte/index.d.ts +18 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +16 -0
- package/dist/svelte/session-svelte.plugin.d.ts +10 -0
- package/dist/svelte/session-svelte.plugin.d.ts.map +1 -0
- package/dist/svelte/session-svelte.plugin.js +54 -0
- package/dist/svelte/styles.css +6 -0
- package/dist/svelte/tool-widgets/SessionCollaborateToolCard.svelte +53 -0
- package/dist/svelte/tool-widgets/SessionCollaborateToolCard.svelte.d.ts +5 -0
- package/dist/svelte/tool-widgets/SessionCollaborateToolCard.svelte.d.ts.map +1 -0
- package/dist/svelte/tool-widgets/SessionScratchpadToolCard.svelte +47 -0
- package/dist/svelte/tool-widgets/SessionScratchpadToolCard.svelte.d.ts +5 -0
- package/dist/svelte/tool-widgets/SessionScratchpadToolCard.svelte.d.ts.map +1 -0
- package/dist/svelte/tool-widgets/SessionTodoToolCard.svelte +43 -0
- package/dist/svelte/tool-widgets/SessionTodoToolCard.svelte.d.ts +5 -0
- package/dist/svelte/tool-widgets/SessionTodoToolCard.svelte.d.ts.map +1 -0
- package/dist/svelte/tool-widgets/SessionToolCard.svelte +47 -0
- package/dist/svelte/tool-widgets/SessionToolCard.svelte.d.ts +5 -0
- package/dist/svelte/tool-widgets/SessionToolCard.svelte.d.ts.map +1 -0
- package/dist/svelte/tool-widgets/SessionWorkspaceToolCard.svelte +44 -0
- package/dist/svelte/tool-widgets/SessionWorkspaceToolCard.svelte.d.ts +5 -0
- package/dist/svelte/tool-widgets/SessionWorkspaceToolCard.svelte.d.ts.map +1 -0
- package/dist/svelte/tool-widgets/card-fields.d.ts +68 -0
- package/dist/svelte/tool-widgets/card-fields.d.ts.map +1 -0
- package/dist/svelte/tool-widgets/card-fields.js +274 -0
- package/dist/testing/harness/transport-mocks.d.ts +380 -0
- package/dist/testing/harness/transport-mocks.d.ts.map +1 -0
- package/dist/testing/harness/transport-mocks.js +809 -0
- package/dist/testing/harness/transport-mocks.js.map +1 -0
- package/dist/tools/best-effort.d.ts +27 -0
- package/dist/tools/best-effort.d.ts.map +1 -0
- package/dist/tools/best-effort.js +31 -0
- package/dist/tools/best-effort.js.map +1 -0
- package/dist/tools/lifecycle/archive/handler.d.ts +19 -0
- package/dist/tools/lifecycle/archive/handler.d.ts.map +1 -0
- package/dist/tools/lifecycle/archive/handler.js +31 -0
- package/dist/tools/lifecycle/archive/handler.js.map +1 -0
- package/dist/tools/lifecycle/archive/schemas.d.ts +24 -0
- package/dist/tools/lifecycle/archive/schemas.d.ts.map +1 -0
- package/dist/tools/lifecycle/archive/schemas.js +22 -0
- package/dist/tools/lifecycle/archive/schemas.js.map +1 -0
- package/dist/tools/lifecycle/context/handler.d.ts +115 -0
- package/dist/tools/lifecycle/context/handler.d.ts.map +1 -0
- package/dist/tools/lifecycle/context/handler.js +82 -0
- package/dist/tools/lifecycle/context/handler.js.map +1 -0
- package/dist/tools/lifecycle/context/schemas.d.ts +44 -0
- package/dist/tools/lifecycle/context/schemas.d.ts.map +1 -0
- package/dist/tools/lifecycle/context/schemas.js +42 -0
- package/dist/tools/lifecycle/context/schemas.js.map +1 -0
- package/dist/tools/lifecycle/delete/handler.d.ts +7 -0
- package/dist/tools/lifecycle/delete/handler.d.ts.map +1 -0
- package/dist/tools/lifecycle/delete/handler.js +29 -0
- package/dist/tools/lifecycle/delete/handler.js.map +1 -0
- package/dist/tools/lifecycle/delete/schemas.d.ts +16 -0
- package/dist/tools/lifecycle/delete/schemas.d.ts.map +1 -0
- package/dist/tools/lifecycle/delete/schemas.js +14 -0
- package/dist/tools/lifecycle/delete/schemas.js.map +1 -0
- package/dist/tools/lifecycle/get/handler.d.ts +18 -0
- package/dist/tools/lifecycle/get/handler.d.ts.map +1 -0
- package/dist/tools/lifecycle/get/handler.js +28 -0
- package/dist/tools/lifecycle/get/handler.js.map +1 -0
- package/dist/tools/lifecycle/get/schemas.d.ts +26 -0
- package/dist/tools/lifecycle/get/schemas.d.ts.map +1 -0
- package/dist/tools/lifecycle/get/schemas.js +24 -0
- package/dist/tools/lifecycle/get/schemas.js.map +1 -0
- package/dist/tools/lifecycle/init/atlas.consumer.d.ts +17 -0
- package/dist/tools/lifecycle/init/atlas.consumer.d.ts.map +1 -0
- package/dist/tools/lifecycle/init/atlas.consumer.js +180 -0
- package/dist/tools/lifecycle/init/atlas.consumer.js.map +1 -0
- package/dist/tools/lifecycle/init/handler.d.ts +4 -0
- package/dist/tools/lifecycle/init/handler.d.ts.map +1 -0
- package/dist/tools/lifecycle/init/handler.js +369 -0
- package/dist/tools/lifecycle/init/handler.js.map +1 -0
- package/dist/tools/lifecycle/init/knowledge.d.ts +18 -0
- package/dist/tools/lifecycle/init/knowledge.d.ts.map +1 -0
- package/dist/tools/lifecycle/init/knowledge.js +86 -0
- package/dist/tools/lifecycle/init/knowledge.js.map +1 -0
- package/dist/tools/lifecycle/init/language.d.ts +10 -0
- package/dist/tools/lifecycle/init/language.d.ts.map +1 -0
- package/dist/tools/lifecycle/init/language.js +26 -0
- package/dist/tools/lifecycle/init/language.js.map +1 -0
- package/dist/tools/lifecycle/init/participant.d.ts +58 -0
- package/dist/tools/lifecycle/init/participant.d.ts.map +1 -0
- package/dist/tools/lifecycle/init/participant.js +219 -0
- package/dist/tools/lifecycle/init/participant.js.map +1 -0
- package/dist/tools/lifecycle/init/schemas.d.ts +106 -0
- package/dist/tools/lifecycle/init/schemas.d.ts.map +1 -0
- package/dist/tools/lifecycle/init/schemas.js +94 -0
- package/dist/tools/lifecycle/init/schemas.js.map +1 -0
- package/dist/tools/lifecycle/init/types.d.ts +143 -0
- package/dist/tools/lifecycle/init/types.d.ts.map +1 -0
- package/dist/tools/lifecycle/init/types.js +3 -0
- package/dist/tools/lifecycle/init/types.js.map +1 -0
- package/dist/tools/lifecycle/init/workspace.consumer.d.ts +13 -0
- package/dist/tools/lifecycle/init/workspace.consumer.d.ts.map +1 -0
- package/dist/tools/lifecycle/init/workspace.consumer.js +58 -0
- package/dist/tools/lifecycle/init/workspace.consumer.js.map +1 -0
- package/dist/tools/lifecycle/list/handler.d.ts +47 -0
- package/dist/tools/lifecycle/list/handler.d.ts.map +1 -0
- package/dist/tools/lifecycle/list/handler.js +48 -0
- package/dist/tools/lifecycle/list/handler.js.map +1 -0
- package/dist/tools/lifecycle/list/schemas.d.ts +37 -0
- package/dist/tools/lifecycle/list/schemas.d.ts.map +1 -0
- package/dist/tools/lifecycle/list/schemas.js +30 -0
- package/dist/tools/lifecycle/list/schemas.js.map +1 -0
- package/dist/tools/lifecycle/packet/handler.d.ts +490 -0
- package/dist/tools/lifecycle/packet/handler.d.ts.map +1 -0
- package/dist/tools/lifecycle/packet/handler.js +428 -0
- package/dist/tools/lifecycle/packet/handler.js.map +1 -0
- package/dist/tools/lifecycle/packet/schemas.d.ts +100 -0
- package/dist/tools/lifecycle/packet/schemas.d.ts.map +1 -0
- package/dist/tools/lifecycle/packet/schemas.js +36 -0
- package/dist/tools/lifecycle/packet/schemas.js.map +1 -0
- package/dist/tools/lifecycle/packet/todos.d.ts +30 -0
- package/dist/tools/lifecycle/packet/todos.d.ts.map +1 -0
- package/dist/tools/lifecycle/packet/todos.js +47 -0
- package/dist/tools/lifecycle/packet/todos.js.map +1 -0
- package/dist/tools/lifecycle/search/handler.d.ts +36 -0
- package/dist/tools/lifecycle/search/handler.d.ts.map +1 -0
- package/dist/tools/lifecycle/search/handler.js +44 -0
- package/dist/tools/lifecycle/search/handler.js.map +1 -0
- package/dist/tools/lifecycle/search/schemas.d.ts +37 -0
- package/dist/tools/lifecycle/search/schemas.d.ts.map +1 -0
- package/dist/tools/lifecycle/search/schemas.js +30 -0
- package/dist/tools/lifecycle/search/schemas.js.map +1 -0
- package/dist/tools/lifecycle/snapshot/handler.d.ts +144 -0
- package/dist/tools/lifecycle/snapshot/handler.d.ts.map +1 -0
- package/dist/tools/lifecycle/snapshot/handler.js +31 -0
- package/dist/tools/lifecycle/snapshot/handler.js.map +1 -0
- package/dist/tools/lifecycle/snapshot/schemas.d.ts +27 -0
- package/dist/tools/lifecycle/snapshot/schemas.d.ts.map +1 -0
- package/dist/tools/lifecycle/snapshot/schemas.js +25 -0
- package/dist/tools/lifecycle/snapshot/schemas.js.map +1 -0
- package/dist/tools/lifecycle/state/pause/handler.d.ts +13 -0
- package/dist/tools/lifecycle/state/pause/handler.d.ts.map +1 -0
- package/dist/tools/lifecycle/state/pause/handler.js +33 -0
- package/dist/tools/lifecycle/state/pause/handler.js.map +1 -0
- package/dist/tools/lifecycle/state/pause/schemas.d.ts +21 -0
- package/dist/tools/lifecycle/state/pause/schemas.d.ts.map +1 -0
- package/dist/tools/lifecycle/state/pause/schemas.js +19 -0
- package/dist/tools/lifecycle/state/pause/schemas.js.map +1 -0
- package/dist/tools/lifecycle/state/resume/handler.d.ts +13 -0
- package/dist/tools/lifecycle/state/resume/handler.d.ts.map +1 -0
- package/dist/tools/lifecycle/state/resume/handler.js +31 -0
- package/dist/tools/lifecycle/state/resume/handler.js.map +1 -0
- package/dist/tools/lifecycle/state/resume/schemas.d.ts +21 -0
- package/dist/tools/lifecycle/state/resume/schemas.d.ts.map +1 -0
- package/dist/tools/lifecycle/state/resume/schemas.js +19 -0
- package/dist/tools/lifecycle/state/resume/schemas.js.map +1 -0
- package/dist/tools/lifecycle/state/status/handler.d.ts +133 -0
- package/dist/tools/lifecycle/state/status/handler.d.ts.map +1 -0
- package/dist/tools/lifecycle/state/status/handler.js +232 -0
- package/dist/tools/lifecycle/state/status/handler.js.map +1 -0
- package/dist/tools/lifecycle/state/status/schemas.d.ts +85 -0
- package/dist/tools/lifecycle/state/status/schemas.d.ts.map +1 -0
- package/dist/tools/lifecycle/state/status/schemas.js +84 -0
- package/dist/tools/lifecycle/state/status/schemas.js.map +1 -0
- package/dist/tools/lifecycle/tool/schemas/index.d.ts +133 -0
- package/dist/tools/lifecycle/tool/schemas/index.d.ts.map +1 -0
- package/dist/tools/lifecycle/tool/schemas/index.js +66 -0
- package/dist/tools/lifecycle/tool/schemas/index.js.map +1 -0
- package/dist/tools/lifecycle/tool/session.tool.descriptor.d.ts +8 -0
- package/dist/tools/lifecycle/tool/session.tool.descriptor.d.ts.map +1 -0
- package/dist/tools/lifecycle/tool/session.tool.descriptor.js +44 -0
- package/dist/tools/lifecycle/tool/session.tool.descriptor.js.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.archive.impl.d.ts +7 -0
- package/dist/tools/lifecycle/tool/session.variant.archive.impl.d.ts.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.archive.impl.js +4 -0
- package/dist/tools/lifecycle/tool/session.variant.archive.impl.js.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.context.impl.d.ts +6 -0
- package/dist/tools/lifecycle/tool/session.variant.context.impl.d.ts.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.context.impl.js +4 -0
- package/dist/tools/lifecycle/tool/session.variant.context.impl.js.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.delete.impl.d.ts +7 -0
- package/dist/tools/lifecycle/tool/session.variant.delete.impl.d.ts.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.delete.impl.js +4 -0
- package/dist/tools/lifecycle/tool/session.variant.delete.impl.js.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.get.impl.d.ts +6 -0
- package/dist/tools/lifecycle/tool/session.variant.get.impl.d.ts.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.get.impl.js +4 -0
- package/dist/tools/lifecycle/tool/session.variant.get.impl.js.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.init.impl.cloud.d.ts +34 -0
- package/dist/tools/lifecycle/tool/session.variant.init.impl.cloud.d.ts.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.init.impl.cloud.js +6 -0
- package/dist/tools/lifecycle/tool/session.variant.init.impl.cloud.js.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.init.impl.consumer.d.ts +34 -0
- package/dist/tools/lifecycle/tool/session.variant.init.impl.consumer.d.ts.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.init.impl.consumer.js +4 -0
- package/dist/tools/lifecycle/tool/session.variant.init.impl.consumer.js.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.list.impl.d.ts +8 -0
- package/dist/tools/lifecycle/tool/session.variant.list.impl.d.ts.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.list.impl.js +4 -0
- package/dist/tools/lifecycle/tool/session.variant.list.impl.js.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.packet.impl.d.ts +9 -0
- package/dist/tools/lifecycle/tool/session.variant.packet.impl.d.ts.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.packet.impl.js +4 -0
- package/dist/tools/lifecycle/tool/session.variant.packet.impl.js.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.pause.impl.d.ts +6 -0
- package/dist/tools/lifecycle/tool/session.variant.pause.impl.d.ts.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.pause.impl.js +4 -0
- package/dist/tools/lifecycle/tool/session.variant.pause.impl.js.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.resume.impl.d.ts +6 -0
- package/dist/tools/lifecycle/tool/session.variant.resume.impl.d.ts.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.resume.impl.js +4 -0
- package/dist/tools/lifecycle/tool/session.variant.resume.impl.js.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.search.impl.d.ts +8 -0
- package/dist/tools/lifecycle/tool/session.variant.search.impl.d.ts.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.search.impl.js +4 -0
- package/dist/tools/lifecycle/tool/session.variant.search.impl.js.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.snapshot.impl.d.ts +6 -0
- package/dist/tools/lifecycle/tool/session.variant.snapshot.impl.d.ts.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.snapshot.impl.js +4 -0
- package/dist/tools/lifecycle/tool/session.variant.snapshot.impl.js.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.status.impl.d.ts +8 -0
- package/dist/tools/lifecycle/tool/session.variant.status.impl.d.ts.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.status.impl.js +4 -0
- package/dist/tools/lifecycle/tool/session.variant.status.impl.js.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.update.impl.d.ts +10 -0
- package/dist/tools/lifecycle/tool/session.variant.update.impl.d.ts.map +1 -0
- package/dist/tools/lifecycle/tool/session.variant.update.impl.js +4 -0
- package/dist/tools/lifecycle/tool/session.variant.update.impl.js.map +1 -0
- package/dist/tools/lifecycle/update/handler.d.ts +47 -0
- package/dist/tools/lifecycle/update/handler.d.ts.map +1 -0
- package/dist/tools/lifecycle/update/handler.js +30 -0
- package/dist/tools/lifecycle/update/handler.js.map +1 -0
- package/dist/tools/lifecycle/update/schemas.d.ts +25 -0
- package/dist/tools/lifecycle/update/schemas.d.ts.map +1 -0
- package/dist/tools/lifecycle/update/schemas.js +24 -0
- package/dist/tools/lifecycle/update/schemas.js.map +1 -0
- package/dist/tools/lifecycle/workspace-capabilities.consumer.d.ts +3 -0
- package/dist/tools/lifecycle/workspace-capabilities.consumer.d.ts.map +1 -0
- package/dist/tools/lifecycle/workspace-capabilities.consumer.js +3 -0
- package/dist/tools/lifecycle/workspace-capabilities.consumer.js.map +1 -0
- package/dist/tools/lifecycle/workspace-capabilities.d.ts +14 -0
- package/dist/tools/lifecycle/workspace-capabilities.d.ts.map +1 -0
- package/dist/tools/lifecycle/workspace-capabilities.js +38 -0
- package/dist/tools/lifecycle/workspace-capabilities.js.map +1 -0
- package/dist/tools/messages/delete/schemas.d.ts +37 -0
- package/dist/tools/messages/delete/schemas.d.ts.map +1 -0
- package/dist/tools/messages/delete/schemas.js +50 -0
- package/dist/tools/messages/delete/schemas.js.map +1 -0
- package/dist/tools/messages/handlers.d.ts +143 -0
- package/dist/tools/messages/handlers.d.ts.map +1 -0
- package/dist/tools/messages/handlers.js +564 -0
- package/dist/tools/messages/handlers.js.map +1 -0
- package/dist/tools/messages/list/schemas.d.ts +38 -0
- package/dist/tools/messages/list/schemas.d.ts.map +1 -0
- package/dist/tools/messages/list/schemas.js +40 -0
- package/dist/tools/messages/list/schemas.js.map +1 -0
- package/dist/tools/messages/mark-read/schemas.d.ts +63 -0
- package/dist/tools/messages/mark-read/schemas.d.ts.map +1 -0
- package/dist/tools/messages/mark-read/schemas.js +60 -0
- package/dist/tools/messages/mark-read/schemas.js.map +1 -0
- package/dist/tools/messages/send/schemas.d.ts +50 -0
- package/dist/tools/messages/send/schemas.d.ts.map +1 -0
- package/dist/tools/messages/send/schemas.js +61 -0
- package/dist/tools/messages/send/schemas.js.map +1 -0
- package/dist/tools/messages/tool/schemas/index.d.ts +70 -0
- package/dist/tools/messages/tool/schemas/index.d.ts.map +1 -0
- package/dist/tools/messages/tool/schemas/index.js +22 -0
- package/dist/tools/messages/tool/schemas/index.js.map +1 -0
- package/dist/tools/messages/tool/session-collaborate.descriptor.d.ts +8 -0
- package/dist/tools/messages/tool/session-collaborate.descriptor.d.ts.map +1 -0
- package/dist/tools/messages/tool/session-collaborate.descriptor.js +32 -0
- package/dist/tools/messages/tool/session-collaborate.descriptor.js.map +1 -0
- package/dist/tools/messages/tool/session-collaborate.variant.delete.impl.d.ts +10 -0
- package/dist/tools/messages/tool/session-collaborate.variant.delete.impl.d.ts.map +1 -0
- package/dist/tools/messages/tool/session-collaborate.variant.delete.impl.js +4 -0
- package/dist/tools/messages/tool/session-collaborate.variant.delete.impl.js.map +1 -0
- package/dist/tools/messages/tool/session-collaborate.variant.list.impl.d.ts +12 -0
- package/dist/tools/messages/tool/session-collaborate.variant.list.impl.d.ts.map +1 -0
- package/dist/tools/messages/tool/session-collaborate.variant.list.impl.js +4 -0
- package/dist/tools/messages/tool/session-collaborate.variant.list.impl.js.map +1 -0
- package/dist/tools/messages/tool/session-collaborate.variant.mark-read.impl.d.ts +11 -0
- package/dist/tools/messages/tool/session-collaborate.variant.mark-read.impl.d.ts.map +1 -0
- package/dist/tools/messages/tool/session-collaborate.variant.mark-read.impl.js +4 -0
- package/dist/tools/messages/tool/session-collaborate.variant.mark-read.impl.js.map +1 -0
- package/dist/tools/messages/tool/session-collaborate.variant.send.impl.d.ts +18 -0
- package/dist/tools/messages/tool/session-collaborate.variant.send.impl.d.ts.map +1 -0
- package/dist/tools/messages/tool/session-collaborate.variant.send.impl.js +4 -0
- package/dist/tools/messages/tool/session-collaborate.variant.send.impl.js.map +1 -0
- package/dist/tools/scratchpad/delete/schemas.d.ts +18 -0
- package/dist/tools/scratchpad/delete/schemas.d.ts.map +1 -0
- package/dist/tools/scratchpad/delete/schemas.js +16 -0
- package/dist/tools/scratchpad/delete/schemas.js.map +1 -0
- package/dist/tools/scratchpad/handlers.d.ts +88 -0
- package/dist/tools/scratchpad/handlers.d.ts.map +1 -0
- package/dist/tools/scratchpad/handlers.js +189 -0
- package/dist/tools/scratchpad/handlers.js.map +1 -0
- package/dist/tools/scratchpad/list/schemas.d.ts +34 -0
- package/dist/tools/scratchpad/list/schemas.d.ts.map +1 -0
- package/dist/tools/scratchpad/list/schemas.js +27 -0
- package/dist/tools/scratchpad/list/schemas.js.map +1 -0
- package/dist/tools/scratchpad/read/schemas.d.ts +34 -0
- package/dist/tools/scratchpad/read/schemas.d.ts.map +1 -0
- package/dist/tools/scratchpad/read/schemas.js +34 -0
- package/dist/tools/scratchpad/read/schemas.js.map +1 -0
- package/dist/tools/scratchpad/search/schemas.d.ts +28 -0
- package/dist/tools/scratchpad/search/schemas.d.ts.map +1 -0
- package/dist/tools/scratchpad/search/schemas.js +26 -0
- package/dist/tools/scratchpad/search/schemas.js.map +1 -0
- package/dist/tools/scratchpad/tool/schemas/index.d.ts +102 -0
- package/dist/tools/scratchpad/tool/schemas/index.d.ts.map +1 -0
- package/dist/tools/scratchpad/tool/schemas/index.js +42 -0
- package/dist/tools/scratchpad/tool/schemas/index.js.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.descriptor.d.ts +8 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.descriptor.d.ts.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.descriptor.js +36 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.descriptor.js.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.batch-delete.impl.d.ts +9 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.batch-delete.impl.d.ts.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.batch-delete.impl.js +4 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.batch-delete.impl.js.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.batch-read.impl.d.ts +8 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.batch-read.impl.d.ts.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.batch-read.impl.js +4 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.batch-read.impl.js.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.batch-write.impl.d.ts +14 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.batch-write.impl.d.ts.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.batch-write.impl.js +4 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.batch-write.impl.js.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.delete.impl.d.ts +9 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.delete.impl.d.ts.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.delete.impl.js +4 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.delete.impl.js.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.list.impl.d.ts +9 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.list.impl.d.ts.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.list.impl.js +4 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.list.impl.js.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.read.impl.d.ts +8 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.read.impl.d.ts.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.read.impl.js +4 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.read.impl.js.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.search.impl.d.ts +8 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.search.impl.d.ts.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.search.impl.js +4 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.search.impl.js.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.write.impl.d.ts +14 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.write.impl.d.ts.map +1 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.write.impl.js +4 -0
- package/dist/tools/scratchpad/tool/session-scratchpad.variant.write.impl.js.map +1 -0
- package/dist/tools/scratchpad/write/schemas/index.d.ts +47 -0
- package/dist/tools/scratchpad/write/schemas/index.d.ts.map +1 -0
- package/dist/tools/scratchpad/write/schemas/index.js +26 -0
- package/dist/tools/scratchpad/write/schemas/index.js.map +1 -0
- package/dist/tools/todos/add/handler.d.ts +37 -0
- package/dist/tools/todos/add/handler.d.ts.map +1 -0
- package/dist/tools/todos/add/handler.js +59 -0
- package/dist/tools/todos/add/handler.js.map +1 -0
- package/dist/tools/todos/add/schemas.d.ts +92 -0
- package/dist/tools/todos/add/schemas.d.ts.map +1 -0
- package/dist/tools/todos/add/schemas.js +59 -0
- package/dist/tools/todos/add/schemas.js.map +1 -0
- package/dist/tools/todos/batch-add/handler.d.ts +21 -0
- package/dist/tools/todos/batch-add/handler.d.ts.map +1 -0
- package/dist/tools/todos/batch-add/handler.js +46 -0
- package/dist/tools/todos/batch-add/handler.js.map +1 -0
- package/dist/tools/todos/batch-add/schemas.d.ts +65 -0
- package/dist/tools/todos/batch-add/schemas.d.ts.map +1 -0
- package/dist/tools/todos/batch-add/schemas.js +47 -0
- package/dist/tools/todos/batch-add/schemas.js.map +1 -0
- package/dist/tools/todos/batch-update/handler.d.ts +4 -0
- package/dist/tools/todos/batch-update/handler.d.ts.map +1 -0
- package/dist/tools/todos/batch-update/handler.js +25 -0
- package/dist/tools/todos/batch-update/handler.js.map +1 -0
- package/dist/tools/todos/batch-update/schemas.d.ts +35 -0
- package/dist/tools/todos/batch-update/schemas.d.ts.map +1 -0
- package/dist/tools/todos/batch-update/schemas.js +22 -0
- package/dist/tools/todos/batch-update/schemas.js.map +1 -0
- package/dist/tools/todos/complete/handler.d.ts +22 -0
- package/dist/tools/todos/complete/handler.d.ts.map +1 -0
- package/dist/tools/todos/complete/handler.js +76 -0
- package/dist/tools/todos/complete/handler.js.map +1 -0
- package/dist/tools/todos/complete/schemas.d.ts +27 -0
- package/dist/tools/todos/complete/schemas.d.ts.map +1 -0
- package/dist/tools/todos/complete/schemas.js +25 -0
- package/dist/tools/todos/complete/schemas.js.map +1 -0
- package/dist/tools/todos/delete/handler.d.ts +4 -0
- package/dist/tools/todos/delete/handler.d.ts.map +1 -0
- package/dist/tools/todos/delete/handler.js +58 -0
- package/dist/tools/todos/delete/handler.js.map +1 -0
- package/dist/tools/todos/delete/schemas.d.ts +25 -0
- package/dist/tools/todos/delete/schemas.d.ts.map +1 -0
- package/dist/tools/todos/delete/schemas.js +23 -0
- package/dist/tools/todos/delete/schemas.js.map +1 -0
- package/dist/tools/todos/get/handler.d.ts +45 -0
- package/dist/tools/todos/get/handler.d.ts.map +1 -0
- package/dist/tools/todos/get/handler.js +10 -0
- package/dist/tools/todos/get/handler.js.map +1 -0
- package/dist/tools/todos/get/schemas.d.ts +17 -0
- package/dist/tools/todos/get/schemas.d.ts.map +1 -0
- package/dist/tools/todos/get/schemas.js +15 -0
- package/dist/tools/todos/get/schemas.js.map +1 -0
- package/dist/tools/todos/list/handler.d.ts +58 -0
- package/dist/tools/todos/list/handler.d.ts.map +1 -0
- package/dist/tools/todos/list/handler.js +91 -0
- package/dist/tools/todos/list/handler.js.map +1 -0
- package/dist/tools/todos/list/schemas.d.ts +40 -0
- package/dist/tools/todos/list/schemas.d.ts.map +1 -0
- package/dist/tools/todos/list/schemas.js +34 -0
- package/dist/tools/todos/list/schemas.js.map +1 -0
- package/dist/tools/todos/project-ref.d.ts +19 -0
- package/dist/tools/todos/project-ref.d.ts.map +1 -0
- package/dist/tools/todos/project-ref.js +37 -0
- package/dist/tools/todos/project-ref.js.map +1 -0
- package/dist/tools/todos/tool/schemas/index.d.ts +219 -0
- package/dist/tools/todos/tool/schemas/index.d.ts.map +1 -0
- package/dist/tools/todos/tool/schemas/index.js +56 -0
- package/dist/tools/todos/tool/schemas/index.js.map +1 -0
- package/dist/tools/todos/tool/session-todo.descriptor.d.ts +8 -0
- package/dist/tools/todos/tool/session-todo.descriptor.d.ts.map +1 -0
- package/dist/tools/todos/tool/session-todo.descriptor.js +39 -0
- package/dist/tools/todos/tool/session-todo.descriptor.js.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.add.impl.d.ts +40 -0
- package/dist/tools/todos/tool/session-todo.variant.add.impl.d.ts.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.add.impl.js +4 -0
- package/dist/tools/todos/tool/session-todo.variant.add.impl.js.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.batch-add.impl.d.ts +24 -0
- package/dist/tools/todos/tool/session-todo.variant.batch-add.impl.d.ts.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.batch-add.impl.js +4 -0
- package/dist/tools/todos/tool/session-todo.variant.batch-add.impl.js.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.batch-complete.impl.d.ts +9 -0
- package/dist/tools/todos/tool/session-todo.variant.batch-complete.impl.d.ts.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.batch-complete.impl.js +4 -0
- package/dist/tools/todos/tool/session-todo.variant.batch-complete.impl.js.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.batch-delete.impl.d.ts +10 -0
- package/dist/tools/todos/tool/session-todo.variant.batch-delete.impl.d.ts.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.batch-delete.impl.js +4 -0
- package/dist/tools/todos/tool/session-todo.variant.batch-delete.impl.js.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.batch-update.impl.d.ts +9 -0
- package/dist/tools/todos/tool/session-todo.variant.batch-update.impl.d.ts.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.batch-update.impl.js +4 -0
- package/dist/tools/todos/tool/session-todo.variant.batch-update.impl.js.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.complete.impl.d.ts +9 -0
- package/dist/tools/todos/tool/session-todo.variant.complete.impl.d.ts.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.complete.impl.js +4 -0
- package/dist/tools/todos/tool/session-todo.variant.complete.impl.js.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.delete.impl.d.ts +10 -0
- package/dist/tools/todos/tool/session-todo.variant.delete.impl.d.ts.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.delete.impl.js +4 -0
- package/dist/tools/todos/tool/session-todo.variant.delete.impl.js.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.get.impl.d.ts +6 -0
- package/dist/tools/todos/tool/session-todo.variant.get.impl.d.ts.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.get.impl.js +4 -0
- package/dist/tools/todos/tool/session-todo.variant.get.impl.js.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.list.impl.d.ts +14 -0
- package/dist/tools/todos/tool/session-todo.variant.list.impl.d.ts.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.list.impl.js +4 -0
- package/dist/tools/todos/tool/session-todo.variant.list.impl.js.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.tree.impl.d.ts +7 -0
- package/dist/tools/todos/tool/session-todo.variant.tree.impl.d.ts.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.tree.impl.js +4 -0
- package/dist/tools/todos/tool/session-todo.variant.tree.impl.js.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.update.impl.d.ts +21 -0
- package/dist/tools/todos/tool/session-todo.variant.update.impl.d.ts.map +1 -0
- package/dist/tools/todos/tool/session-todo.variant.update.impl.js +4 -0
- package/dist/tools/todos/tool/session-todo.variant.update.impl.js.map +1 -0
- package/dist/tools/todos/tree/handler.d.ts +20 -0
- package/dist/tools/todos/tree/handler.d.ts.map +1 -0
- package/dist/tools/todos/tree/handler.js +41 -0
- package/dist/tools/todos/tree/handler.js.map +1 -0
- package/dist/tools/todos/tree/schemas.d.ts +21 -0
- package/dist/tools/todos/tree/schemas.d.ts.map +1 -0
- package/dist/tools/todos/tree/schemas.js +19 -0
- package/dist/tools/todos/tree/schemas.js.map +1 -0
- package/dist/tools/todos/update/handler.d.ts +54 -0
- package/dist/tools/todos/update/handler.d.ts.map +1 -0
- package/dist/tools/todos/update/handler.js +60 -0
- package/dist/tools/todos/update/handler.js.map +1 -0
- package/dist/tools/todos/update/schemas.d.ts +57 -0
- package/dist/tools/todos/update/schemas.d.ts.map +1 -0
- package/dist/tools/todos/update/schemas.js +35 -0
- package/dist/tools/todos/update/schemas.js.map +1 -0
- package/dist/tools/tool-runtime.d.ts +17 -0
- package/dist/tools/tool-runtime.d.ts.map +1 -0
- package/dist/tools/tool-runtime.js +39 -0
- package/dist/tools/tool-runtime.js.map +1 -0
- package/dist/tools/workspace/handlers.d.ts +75 -0
- package/dist/tools/workspace/handlers.d.ts.map +1 -0
- package/dist/tools/workspace/handlers.js +154 -0
- package/dist/tools/workspace/handlers.js.map +1 -0
- package/dist/tools/workspace/tool/schemas/index.d.ts +47 -0
- package/dist/tools/workspace/tool/schemas/index.d.ts.map +1 -0
- package/dist/tools/workspace/tool/schemas/index.js +33 -0
- package/dist/tools/workspace/tool/schemas/index.js.map +1 -0
- package/dist/tools/workspace/tool/session-workspace.descriptor.d.ts +8 -0
- package/dist/tools/workspace/tool/session-workspace.descriptor.d.ts.map +1 -0
- package/dist/tools/workspace/tool/session-workspace.descriptor.js +31 -0
- package/dist/tools/workspace/tool/session-workspace.descriptor.js.map +1 -0
- package/dist/tools/workspace/tool/session-workspace.variant.list.impl.d.ts +5 -0
- package/dist/tools/workspace/tool/session-workspace.variant.list.impl.d.ts.map +1 -0
- package/dist/tools/workspace/tool/session-workspace.variant.list.impl.js +7 -0
- package/dist/tools/workspace/tool/session-workspace.variant.list.impl.js.map +1 -0
- package/dist/tools/workspace/tool/session-workspace.variant.pair.impl.d.ts +8 -0
- package/dist/tools/workspace/tool/session-workspace.variant.pair.impl.d.ts.map +1 -0
- package/dist/tools/workspace/tool/session-workspace.variant.pair.impl.js +4 -0
- package/dist/tools/workspace/tool/session-workspace.variant.pair.impl.js.map +1 -0
- package/dist/tools/workspace/tool/session-workspace.variant.unpair.impl.d.ts +6 -0
- package/dist/tools/workspace/tool/session-workspace.variant.unpair.impl.d.ts.map +1 -0
- package/dist/tools/workspace/tool/session-workspace.variant.unpair.impl.js +4 -0
- package/dist/tools/workspace/tool/session-workspace.variant.unpair.impl.js.map +1 -0
- package/dist/tui/components/SessionDetailView.d.ts +17 -0
- package/dist/tui/components/SessionDetailView.d.ts.map +1 -0
- package/dist/tui/components/SessionDetailView.js +229 -0
- package/dist/tui/components/SessionDetailView.js.map +1 -0
- package/dist/tui/index.d.ts +7 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +7 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/session-actions.d.ts +20 -0
- package/dist/tui/session-actions.d.ts.map +1 -0
- package/dist/tui/session-actions.js +27 -0
- package/dist/tui/session-actions.js.map +1 -0
- package/dist/tui/session-tui.plugin.d.ts +10 -0
- package/dist/tui/session-tui.plugin.d.ts.map +1 -0
- package/dist/tui/session-tui.plugin.js +29 -0
- package/dist/tui/session-tui.plugin.js.map +1 -0
- package/dist/tui/sessions-open.descriptor.d.ts +9 -0
- package/dist/tui/sessions-open.descriptor.d.ts.map +1 -0
- package/dist/tui/sessions-open.descriptor.js +18 -0
- package/dist/tui/sessions-open.descriptor.js.map +1 -0
- package/dist/tui/sessions-open.impl.consumer.d.ts +9 -0
- package/dist/tui/sessions-open.impl.consumer.d.ts.map +1 -0
- package/dist/tui/sessions-open.impl.consumer.js +13 -0
- package/dist/tui/sessions-open.impl.consumer.js.map +1 -0
- package/dist/tui/sessions-recovery.d.ts +14 -0
- package/dist/tui/sessions-recovery.d.ts.map +1 -0
- package/dist/tui/sessions-recovery.js +23 -0
- package/dist/tui/sessions-recovery.js.map +1 -0
- package/dist/tui/sessions-registry.descriptor.d.ts +11 -0
- package/dist/tui/sessions-registry.descriptor.d.ts.map +1 -0
- package/dist/tui/sessions-registry.descriptor.js +17 -0
- package/dist/tui/sessions-registry.descriptor.js.map +1 -0
- package/dist/tui/sessions-registry.impl.consumer.d.ts +15 -0
- package/dist/tui/sessions-registry.impl.consumer.d.ts.map +1 -0
- package/dist/tui/sessions-registry.impl.consumer.js +79 -0
- package/dist/tui/sessions-registry.impl.consumer.js.map +1 -0
- package/dist/tui/sessions-registry.state-key.d.ts +32 -0
- package/dist/tui/sessions-registry.state-key.d.ts.map +1 -0
- package/dist/tui/sessions-registry.state-key.js +8 -0
- package/dist/tui/sessions-registry.state-key.js.map +1 -0
- package/dist/tui/sessions.descriptor.d.ts +10 -0
- package/dist/tui/sessions.descriptor.d.ts.map +1 -0
- package/dist/tui/sessions.descriptor.js +25 -0
- package/dist/tui/sessions.descriptor.js.map +1 -0
- package/dist/tui/sessions.impl.consumer.d.ts +4 -0
- package/dist/tui/sessions.impl.consumer.d.ts.map +1 -0
- package/dist/tui/sessions.impl.consumer.js +284 -0
- package/dist/tui/sessions.impl.consumer.js.map +1 -0
- package/dist/tui/sessions.logic.d.ts +33 -0
- package/dist/tui/sessions.logic.d.ts.map +1 -0
- package/dist/tui/sessions.logic.js +94 -0
- package/dist/tui/sessions.logic.js.map +1 -0
- package/package.json +273 -4
package/dist/manager.js
ADDED
|
@@ -0,0 +1,3029 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager
|
|
3
|
+
* Core business logic for session management
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* - Session lifecycle management (create, resume, pause, archive)
|
|
7
|
+
* - TODO tracking with dependencies and hierarchies
|
|
8
|
+
* - Scratchpad operations with expiration
|
|
9
|
+
* - Job reference tracking
|
|
10
|
+
* - Participant messaging
|
|
11
|
+
* - Knowledge context integration
|
|
12
|
+
* - Participant identification and connection tracking
|
|
13
|
+
*/
|
|
14
|
+
import * as z from 'zod/v4';
|
|
15
|
+
import { sha256Hex } from '@vibesdotdev/shared/hashing';
|
|
16
|
+
import { CapabilityError, CaughtFailureSchema, settle, valueFreeFailure } from '@vibesdotdev/kernel';
|
|
17
|
+
import { getVibesClient } from '@vibesdotdev/kernel/client';
|
|
18
|
+
import { JsonValueSchema as JsonSchema } from '@vibesdotdev/kernel/schemas/json';
|
|
19
|
+
import { getLogger } from '@vibesdotdev/logging';
|
|
20
|
+
import { StorageRecord } from '@vibesdotdev/storage/record';
|
|
21
|
+
import { storageFailureCondition } from '@vibesdotdev/storage/record/failure-condition';
|
|
22
|
+
import { ScratchpadContentTypeSchema } from "./schemas/index.js";
|
|
23
|
+
import { HarnessSeatRegistrationSchema, HarnessSeatSchema, harnessSeatParticipantId, harnessSeatSlug } from "./schemas/harness-seat.js";
|
|
24
|
+
import { ensureSessionContextWithManager, getOrCreateMCPParticipant, resolveParticipantReference, sanitizeSlugCandidate, scanParticipantsBySlug } from "./mcp/utils/participant.js";
|
|
25
|
+
import { emitSessionBound } from "./events/session.bound.event.descriptor.js";
|
|
26
|
+
const SESSION_STATE_LEGACY_KEY = 'session-state';
|
|
27
|
+
const SESSION_STATE_DEFAULT_KEY = 'default';
|
|
28
|
+
const SESSION_STATE_PARTICIPANT_PREFIX = 'participant:';
|
|
29
|
+
const SESSION_STATE_MCP_PREFIX = 'mcp:';
|
|
30
|
+
/** A participant seen within this window is active (the CLI roster uses the same window). */
|
|
31
|
+
const PARTICIPANT_ACTIVE_WINDOW_MS = 5 * 60 * 1000;
|
|
32
|
+
/** The runtime client id notifications register their delivery client under. */
|
|
33
|
+
const NOTIFICATIONS_CLIENT_ID = 'notifications.delivery';
|
|
34
|
+
/**
|
|
35
|
+
* Whether a storage write lost a unique-index race. LocalDB's driver classifies
|
|
36
|
+
* the collision on its typed refusal (`unique-constraint`); the adapter's text
|
|
37
|
+
* is never read. A conflict another adapter reports untyped is not recognised
|
|
38
|
+
* here and propagates (cross-lane: storage has no adapter-neutral conflict code).
|
|
39
|
+
*/
|
|
40
|
+
function isUniqueConstraintError(failure) {
|
|
41
|
+
return storageFailureCondition(failure) === 'unique-constraint';
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Run `write`; when it fails with a failure `recoverable` admits, run
|
|
45
|
+
* `recover` instead. Any other failure of `write`, and a recovery that finds
|
|
46
|
+
* nothing (`null`), rethrows the write's own failure; a recovery that throws
|
|
47
|
+
* carries its own failure. The write's failure is never dropped.
|
|
48
|
+
*/
|
|
49
|
+
async function writeOrRecover(write, recover, recoverable) {
|
|
50
|
+
let lost;
|
|
51
|
+
const steps = [
|
|
52
|
+
write,
|
|
53
|
+
async () => {
|
|
54
|
+
if (!recoverable(CaughtFailureSchema.parse(lost)))
|
|
55
|
+
throw lost;
|
|
56
|
+
const recovered = await recover();
|
|
57
|
+
if (recovered === null)
|
|
58
|
+
throw lost;
|
|
59
|
+
return recovered;
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
for (const step of steps) {
|
|
63
|
+
try {
|
|
64
|
+
return await step();
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
lost = error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw lost;
|
|
71
|
+
}
|
|
72
|
+
/** Any write failure admits a recovery read (the recovery itself confirms the race). */
|
|
73
|
+
function anyFailure() {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
/** Decode JSON text; a parse failure is returned as a value with its cause, never thrown. */
|
|
77
|
+
function decodeJson(text) {
|
|
78
|
+
try {
|
|
79
|
+
return { ok: true, value: JsonSchema.parse(JSON.parse(text)) };
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return { ok: false, cause: CaughtFailureSchema.parse(error) };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** A persisted routing row's value: each field read independently; a non-string id is no id. */
|
|
86
|
+
const RoutingValueSchema = z.object({
|
|
87
|
+
sessionId: z.string().nullable().optional().catch(undefined),
|
|
88
|
+
participantId: z.string().nullable().optional().catch(undefined)
|
|
89
|
+
});
|
|
90
|
+
/** The retired aggregate `session-state` row: each map entry is kept only when both sides are strings. */
|
|
91
|
+
const StringMapSchema = z
|
|
92
|
+
.record(z.string(), z.unknown())
|
|
93
|
+
.optional()
|
|
94
|
+
.catch(undefined)
|
|
95
|
+
.transform((entries) => {
|
|
96
|
+
const kept = new Map();
|
|
97
|
+
for (const [key, value] of Object.entries(entries ?? {})) {
|
|
98
|
+
const parsed = z.string().safeParse(value);
|
|
99
|
+
if (parsed.success)
|
|
100
|
+
kept.set(key, parsed.data);
|
|
101
|
+
}
|
|
102
|
+
return kept;
|
|
103
|
+
});
|
|
104
|
+
const LegacySessionStateSchema = z.object({
|
|
105
|
+
participantSessions: StringMapSchema,
|
|
106
|
+
mcpSessions: StringMapSchema,
|
|
107
|
+
defaultSessionId: z.string().nullable().optional().catch(undefined),
|
|
108
|
+
defaultParticipantId: z.string().nullable().optional().catch(undefined)
|
|
109
|
+
});
|
|
110
|
+
/** Whether a decoded JSON value is an object (not an array or a scalar). */
|
|
111
|
+
function isJsonObject(value) {
|
|
112
|
+
return Object(value) === value && !Array.isArray(value);
|
|
113
|
+
}
|
|
114
|
+
/** The list/search projection of one scratchpad entry. */
|
|
115
|
+
function scratchpadSummaryOf(entry) {
|
|
116
|
+
return {
|
|
117
|
+
id: entry.id,
|
|
118
|
+
key: entry.key,
|
|
119
|
+
tags: entry.tags ?? [],
|
|
120
|
+
contentType: entry.contentType,
|
|
121
|
+
contentLength: entry.content?.length ?? 0,
|
|
122
|
+
preview: entry.content?.substring(0, 100) ?? '',
|
|
123
|
+
createdAt: entry.createdAt,
|
|
124
|
+
updatedAt: entry.updatedAt,
|
|
125
|
+
language: entry.language,
|
|
126
|
+
expiresAt: entry.expiresAt
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** A typed `NOT_FOUND session-not-found` for a session id. */
|
|
130
|
+
function sessionNotFound(sessionId) {
|
|
131
|
+
return new CapabilityError(`Session ${sessionId} not found`, 'NOT_FOUND', {
|
|
132
|
+
reason: 'session-not-found',
|
|
133
|
+
details: { kind: 'session', id: sessionId }
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/** A typed `NOT_FOUND todo-not-found` for a todo id. */
|
|
137
|
+
function todoNotFound(todoId) {
|
|
138
|
+
return new CapabilityError(`TODO ${todoId} not found`, 'NOT_FOUND', {
|
|
139
|
+
reason: 'todo-not-found',
|
|
140
|
+
details: { kind: 'session.todo', id: todoId }
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/** A typed `CONFLICT session-not-writable` for a session that is paused or archived. */
|
|
144
|
+
function sessionNotWritable(sessionId, status) {
|
|
145
|
+
return new CapabilityError(`Session ${sessionId} is ${status}; resume it explicitly before writing to it.`, 'CONFLICT', { reason: 'session-not-writable', details: { sessionId, status } });
|
|
146
|
+
}
|
|
147
|
+
/** Default todo order; the id tiebreak keeps the pages of an exhaustive scan stable. */
|
|
148
|
+
const TODO_ORDER = { priority: 'desc', createdAt: 'asc', id: 'asc' };
|
|
149
|
+
/** Page size of an exhaustive todo scan. */
|
|
150
|
+
const TODO_SCAN_PAGE = 1000;
|
|
151
|
+
/** A work item id as the todo record stores it. */
|
|
152
|
+
const WorkItemIdSchema = z.string().min(1);
|
|
153
|
+
/** Whether the filters carry a dependency predicate, which storage cannot evaluate. */
|
|
154
|
+
function hasDependencyFilter(filters) {
|
|
155
|
+
return filters?.hasDependencies !== undefined || filters?.isBlocked !== undefined;
|
|
156
|
+
}
|
|
157
|
+
/** A storage search total: a finite count, or absent. */
|
|
158
|
+
const SearchTotalSchema = z.number().int().nonnegative();
|
|
159
|
+
/** The display fields a session summary reads from metadata; a non-string value is no value. */
|
|
160
|
+
const SessionDisplayMetadataSchema = z.object({
|
|
161
|
+
workspacePath: z.string().optional().catch(undefined),
|
|
162
|
+
workItemTitle: z.string().optional().catch(undefined),
|
|
163
|
+
gitBranch: z.string().optional().catch(undefined)
|
|
164
|
+
});
|
|
165
|
+
/**
|
|
166
|
+
* Key-order-independent JSON so metadata comparison can't flake on insertion
|
|
167
|
+
* order: every object's keys are emitted sorted, arrays keep their order, and
|
|
168
|
+
* `undefined` encodes as `null`.
|
|
169
|
+
*/
|
|
170
|
+
export function canonicalJson(value) {
|
|
171
|
+
return JSON.stringify(value ?? null, (_key, nested) => sortedObjectKeys(nested));
|
|
172
|
+
}
|
|
173
|
+
/** An object (not an array) re-created with its own keys in sorted order; any other value unchanged. */
|
|
174
|
+
function sortedObjectKeys(nested) {
|
|
175
|
+
if (Object(nested) !== nested || Array.isArray(nested))
|
|
176
|
+
return nested;
|
|
177
|
+
const entries = Object.entries(Object(nested));
|
|
178
|
+
entries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
179
|
+
return Object.fromEntries(entries);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Keep the indexed Work item id and its typed execution edge inseparable.
|
|
183
|
+
* A Work-linked todo always carries exactly one canonical work-item edge.
|
|
184
|
+
*/
|
|
185
|
+
export function normalizeTodoWorkLink(todo, options = {}) {
|
|
186
|
+
const workLinks = todo.links?.filter((link) => link.kind === 'work-item') ?? [];
|
|
187
|
+
if (todo.workItemId === undefined) {
|
|
188
|
+
if (workLinks.length > 0)
|
|
189
|
+
throw new Error('Todo work-item links require workItemId.');
|
|
190
|
+
return todo;
|
|
191
|
+
}
|
|
192
|
+
if (!options.rewriteExisting && workLinks.some((link) => link.id !== todo.workItemId)) {
|
|
193
|
+
throw new Error('Todo workItemId must match every work-item link.');
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
...todo,
|
|
197
|
+
links: [
|
|
198
|
+
...(todo.links ?? []).filter((link) => link.kind !== 'work-item'),
|
|
199
|
+
{ kind: 'work-item', id: todo.workItemId, source: 'session', role: 'executes' }
|
|
200
|
+
]
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** Attach one authoritative checkout project edge to every Work-linked todo. */
|
|
204
|
+
export function scopeTodoWorkLinks(todo, projectId) {
|
|
205
|
+
const normalized = normalizeTodoWorkLink(todo);
|
|
206
|
+
const workLinked = normalized.workItemId !== undefined ||
|
|
207
|
+
(normalized.links ?? []).some((link) => link.kind === 'work-item');
|
|
208
|
+
if (!workLinked)
|
|
209
|
+
return normalized;
|
|
210
|
+
if (!projectId) {
|
|
211
|
+
throw new CapabilityError('PROJECT_REF_UNBOUND: Work-linked session todos require a project-bound session', 'CONFLICT', {
|
|
212
|
+
reason: 'project-ref-unbound',
|
|
213
|
+
recoveryHint: 'Bind the session to a project or update the todo without Work links.',
|
|
214
|
+
details: { sessionProjectId: null, sessionCode: 'PROJECT_REF_UNBOUND' }
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const projectLinks = (normalized.links ?? []).filter((link) => link.kind === 'project' && link.role === 'belongs-to');
|
|
218
|
+
const conflict = projectLinks.find((link) => link.id.toLowerCase() !== projectId.toLowerCase());
|
|
219
|
+
if (conflict) {
|
|
220
|
+
throw new CapabilityError(`PROJECT_REF_CONFLICT: todo is linked to ${conflict.id}, not session project ${projectId}`, 'CONFLICT', {
|
|
221
|
+
reason: 'project-ref-conflict',
|
|
222
|
+
recoveryHint: 'Ensure the todo project link matches the session project.',
|
|
223
|
+
details: {
|
|
224
|
+
sessionProjectId: projectId,
|
|
225
|
+
linkedProjectId: conflict.id,
|
|
226
|
+
sessionCode: 'PROJECT_REF_CONFLICT'
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
...normalized,
|
|
232
|
+
links: [
|
|
233
|
+
...(normalized.links ?? []).filter((link) => !(link.kind === 'project' && link.role === 'belongs-to')),
|
|
234
|
+
{
|
|
235
|
+
kind: 'project',
|
|
236
|
+
id: projectId.toLowerCase(),
|
|
237
|
+
source: 'session.project-ref',
|
|
238
|
+
role: 'belongs-to'
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Does a durable row match the send a caller intended under the same
|
|
245
|
+
* deterministic id? Compares every caller-controlled field. A mismatch means
|
|
246
|
+
* the caller reused a batch identity for DIFFERENT content — that must
|
|
247
|
+
* surface as a conflict, never replay as a false "delivered".
|
|
248
|
+
*/
|
|
249
|
+
function messagePayloadMatches(existing, sessionId, intended) {
|
|
250
|
+
return (existing.sessionId === sessionId &&
|
|
251
|
+
existing.fromParticipant === intended.fromParticipant &&
|
|
252
|
+
existing.toParticipant === intended.toParticipant &&
|
|
253
|
+
existing.messageType === intended.messageType &&
|
|
254
|
+
existing.content === intended.content &&
|
|
255
|
+
canonicalJson(existing.metadata ?? {}) === canonicalJson(intended.metadata ?? {}));
|
|
256
|
+
}
|
|
257
|
+
const FIXTURE_PARTICIPANT_NAMES = new Set([
|
|
258
|
+
'senderx',
|
|
259
|
+
'betax',
|
|
260
|
+
'alphx',
|
|
261
|
+
'alpha',
|
|
262
|
+
'beta',
|
|
263
|
+
'ownerx',
|
|
264
|
+
'all',
|
|
265
|
+
'Durable owner',
|
|
266
|
+
'solox',
|
|
267
|
+
'everyone',
|
|
268
|
+
'realmember',
|
|
269
|
+
'stats-sender',
|
|
270
|
+
'stats-seat-b',
|
|
271
|
+
'stats-seat-a',
|
|
272
|
+
'activity-sender',
|
|
273
|
+
'ghostx',
|
|
274
|
+
'joinerx'
|
|
275
|
+
]);
|
|
276
|
+
const UUID_LIKE_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
277
|
+
function isFixtureProfile(profile) {
|
|
278
|
+
if (FIXTURE_PARTICIPANT_NAMES.has(profile.name))
|
|
279
|
+
return true;
|
|
280
|
+
if (profile.participantId.startsWith('participant-') && profile.participantId.length > 12)
|
|
281
|
+
return true;
|
|
282
|
+
if (profile.metadata?.autoRegistered === true && UUID_LIKE_PATTERN.test(profile.name))
|
|
283
|
+
return true;
|
|
284
|
+
if (profile.name === 'MCP Participant')
|
|
285
|
+
return true;
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* SessionManager
|
|
290
|
+
* High-level API for session operations with business logic
|
|
291
|
+
*
|
|
292
|
+
* Uses storage descriptors for adapter-agnostic storage operations.
|
|
293
|
+
* Supports any compliant storage adapter (localdb, drizzle, etc.)
|
|
294
|
+
*/
|
|
295
|
+
export class SessionManager {
|
|
296
|
+
hostRuntime;
|
|
297
|
+
logger;
|
|
298
|
+
/** A stable manager keeps state while dependencies follow its caller's view. */
|
|
299
|
+
get runtime() { return this.hostRuntime.invocationRuntime; }
|
|
300
|
+
/** The runtime this manager serves (its caller's view): per-root transport state keys on its root. */
|
|
301
|
+
get servingRuntime() { return this.runtime; }
|
|
302
|
+
bindRuntime(runtime) {
|
|
303
|
+
this.hostRuntime = runtime;
|
|
304
|
+
this.logger = getLogger('session:manager', runtime);
|
|
305
|
+
}
|
|
306
|
+
initialized = false;
|
|
307
|
+
// Storage references (lazy initialized via ensureStorage)
|
|
308
|
+
sessionsStorage;
|
|
309
|
+
todosStorage;
|
|
310
|
+
scratchpadStorage;
|
|
311
|
+
jobsStorage;
|
|
312
|
+
messagesStorage;
|
|
313
|
+
participantsStorage;
|
|
314
|
+
presenceStorage;
|
|
315
|
+
knowledgeRefsStorage;
|
|
316
|
+
connectionsStorage;
|
|
317
|
+
membershipLeasesStorage;
|
|
318
|
+
// In-memory state
|
|
319
|
+
participantSessions = new Map();
|
|
320
|
+
mcpSessions = new Map(); // mcpSessionId -> vibesSessionId
|
|
321
|
+
defaultSessionId = null;
|
|
322
|
+
defaultParticipantId = null;
|
|
323
|
+
persistMappingsTail = Promise.resolve();
|
|
324
|
+
constructor(runtime) {
|
|
325
|
+
this.hostRuntime = runtime;
|
|
326
|
+
this.logger = getLogger('session:manager', runtime);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Resolve Session's participant/session policy through the registered
|
|
330
|
+
* manager context. Consumers do not import MCP participant internals.
|
|
331
|
+
*/
|
|
332
|
+
async resolveContext(sessionId, participantId, mcpSessionId, options = {}) {
|
|
333
|
+
return ensureSessionContextWithManager(this, this.runtime, sessionId, participantId, mcpSessionId, options);
|
|
334
|
+
}
|
|
335
|
+
/** Identify this caller by UUID or slug, reusing Session's self-registration policy. */
|
|
336
|
+
async identifyParticipant(reference) {
|
|
337
|
+
const resolved = await resolveParticipantReference(reference, this, undefined, {
|
|
338
|
+
registerIfMissing: true
|
|
339
|
+
});
|
|
340
|
+
return resolved.participantId;
|
|
341
|
+
}
|
|
342
|
+
/** Resolve or create the caller's MCP participant under Session policy. */
|
|
343
|
+
async resolveMcpParticipant(options = {}, mcpSessionId, caller) {
|
|
344
|
+
return getOrCreateMCPParticipant(undefined, options, mcpSessionId, caller, this);
|
|
345
|
+
}
|
|
346
|
+
/** Commit and publish Session's canonical bound-session lifecycle event. */
|
|
347
|
+
async announceBound(sessionId, participantId, surface) {
|
|
348
|
+
await emitSessionBound({
|
|
349
|
+
runtime: this.runtime,
|
|
350
|
+
sessionId,
|
|
351
|
+
participantId,
|
|
352
|
+
surface
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Ambient consumers need one stable session identity, not a private map
|
|
357
|
+
* guess. The durable default is the process' current binding and is cleared
|
|
358
|
+
* when that session is paused, archived, or deleted.
|
|
359
|
+
*/
|
|
360
|
+
get activeSessionId() {
|
|
361
|
+
return this.defaultSessionId ?? undefined;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Resolve one `session.*` storage record through the runtime. A declared
|
|
365
|
+
* record whose implementation is not registered (the descriptor-only
|
|
366
|
+
* registration path) is bound directly to its descriptor; a record that is
|
|
367
|
+
* not declared at all is a typed NOT_FOUND. Every other resolution failure
|
|
368
|
+
* propagates unchanged.
|
|
369
|
+
*/
|
|
370
|
+
async resolveStorageRecord(id) {
|
|
371
|
+
const selected = await this.runtime
|
|
372
|
+
.query('storage/record')
|
|
373
|
+
.withId(id)
|
|
374
|
+
.enabled()
|
|
375
|
+
.select();
|
|
376
|
+
if (selected)
|
|
377
|
+
return selected.impl;
|
|
378
|
+
const descriptor = this.runtime
|
|
379
|
+
.query('storage/record')
|
|
380
|
+
.withId(id)
|
|
381
|
+
.firstDescriptor();
|
|
382
|
+
if (!descriptor) {
|
|
383
|
+
throw new CapabilityError(`Storage record ${id} is not registered.`, 'NOT_FOUND', {
|
|
384
|
+
reason: 'not-registered',
|
|
385
|
+
details: { kind: 'storage/record', id }
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return new StorageRecord(descriptor, { runtime: this.runtime });
|
|
389
|
+
}
|
|
390
|
+
async ensureStorage() {
|
|
391
|
+
if (this.sessionsStorage)
|
|
392
|
+
return; // Already initialized
|
|
393
|
+
const [sessions, todos, scratchpad, jobs, messages, participants, presence, knowledgeRefs, connections, membershipLeases] = await Promise.all([
|
|
394
|
+
this.resolveStorageRecord('session.sessions'),
|
|
395
|
+
this.resolveStorageRecord('session.todos'),
|
|
396
|
+
this.resolveStorageRecord('session.scratchpad'),
|
|
397
|
+
this.resolveStorageRecord('session.jobs'),
|
|
398
|
+
this.resolveStorageRecord('session.messages'),
|
|
399
|
+
this.resolveStorageRecord('session.participants'),
|
|
400
|
+
this.resolveStorageRecord('session.presence'),
|
|
401
|
+
this.resolveStorageRecord('session.knowledge-refs'),
|
|
402
|
+
this.resolveStorageRecord('session.connections'),
|
|
403
|
+
this.resolveStorageRecord('session.membership-leases')
|
|
404
|
+
]);
|
|
405
|
+
this.sessionsStorage = sessions;
|
|
406
|
+
this.todosStorage = todos;
|
|
407
|
+
this.scratchpadStorage = scratchpad;
|
|
408
|
+
this.jobsStorage = jobs;
|
|
409
|
+
this.messagesStorage = messages;
|
|
410
|
+
this.participantsStorage = participants;
|
|
411
|
+
this.presenceStorage = presence;
|
|
412
|
+
this.knowledgeRefsStorage = knowledgeRefs;
|
|
413
|
+
this.connectionsStorage = connections;
|
|
414
|
+
this.membershipLeasesStorage = membershipLeases;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* The notifications delivery client, or `undefined` when the composition
|
|
418
|
+
* does not provide one. Notifications are runtime-soft (SPEC): absence is
|
|
419
|
+
* confirmed by asking the runtime whether the client is declared, never by
|
|
420
|
+
* catching a failure. A declared client that fails to load propagates.
|
|
421
|
+
*/
|
|
422
|
+
async resolveNotifications() {
|
|
423
|
+
const runtime = this.runtime;
|
|
424
|
+
if (!runtime.hasKind('runtime/client'))
|
|
425
|
+
return undefined;
|
|
426
|
+
const declared = await runtime
|
|
427
|
+
.query('runtime/client')
|
|
428
|
+
.withId(NOTIFICATIONS_CLIENT_ID)
|
|
429
|
+
.existsAsync();
|
|
430
|
+
if (!declared)
|
|
431
|
+
return undefined;
|
|
432
|
+
return getVibesClient(NOTIFICATIONS_CLIENT_ID, runtime);
|
|
433
|
+
}
|
|
434
|
+
/** A true COUNT over `where` — never a loaded page's length (ledger L-11). */
|
|
435
|
+
countRecords(storage, where) {
|
|
436
|
+
return storage.count(where);
|
|
437
|
+
}
|
|
438
|
+
async buildSessionSummary(session) {
|
|
439
|
+
await this.ensureStorage();
|
|
440
|
+
const [todoCount, completedTodoCount, scratchpadEntryCount, jobCount, messageCount, owner, assigned] = await Promise.all([
|
|
441
|
+
this.countRecords(this.todosStorage, { sessionId: session.id }),
|
|
442
|
+
this.countRecords(this.todosStorage, { sessionId: session.id, status: 'completed' }),
|
|
443
|
+
this.countRecords(this.scratchpadStorage, { sessionId: session.id }),
|
|
444
|
+
this.countRecords(this.jobsStorage, { sessionId: session.id }),
|
|
445
|
+
this.countRecords(this.messagesStorage, { sessionId: session.id }),
|
|
446
|
+
this.projectParticipant(session.ownerParticipant),
|
|
447
|
+
session.assignedParticipant
|
|
448
|
+
? this.projectParticipant(session.assignedParticipant)
|
|
449
|
+
: Promise.resolve(undefined)
|
|
450
|
+
]);
|
|
451
|
+
const workspaceRoot = this.nonEmptyMetadata(session, 'workspacePath');
|
|
452
|
+
const workItemTitle = this.nonEmptyMetadata(session, 'workItemTitle');
|
|
453
|
+
const gitBranch = this.nonEmptyMetadata(session, 'gitBranch');
|
|
454
|
+
return {
|
|
455
|
+
id: session.id,
|
|
456
|
+
title: session.title,
|
|
457
|
+
tags: session.tags ?? [],
|
|
458
|
+
status: session.status,
|
|
459
|
+
ownerParticipant: session.ownerParticipant,
|
|
460
|
+
assignedParticipant: session.assignedParticipant,
|
|
461
|
+
ownerParticipantId: session.ownerParticipant,
|
|
462
|
+
ownerParticipantSlug: owner?.slug,
|
|
463
|
+
ownerParticipantName: owner?.name,
|
|
464
|
+
assignedParticipantId: session.assignedParticipant,
|
|
465
|
+
assignedParticipantSlug: assigned?.slug,
|
|
466
|
+
assignedParticipantName: assigned?.name,
|
|
467
|
+
workItemId: session.workItemId,
|
|
468
|
+
workItemTitle,
|
|
469
|
+
workspaceRoot,
|
|
470
|
+
gitBranch,
|
|
471
|
+
worktree: workspaceRoot,
|
|
472
|
+
createdAt: session.createdAt,
|
|
473
|
+
lastActivityAt: session.lastActivityAt,
|
|
474
|
+
todoCount,
|
|
475
|
+
completedTodoCount,
|
|
476
|
+
scratchpadEntryCount,
|
|
477
|
+
jobCount,
|
|
478
|
+
messageCount
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
nonEmptyMetadata(session, key) {
|
|
482
|
+
const value = SessionDisplayMetadataSchema.parse(session.metadata ?? {})[key]?.trim();
|
|
483
|
+
return value ? value : undefined;
|
|
484
|
+
}
|
|
485
|
+
async projectParticipant(participantId) {
|
|
486
|
+
const profile = await this.getParticipant(participantId);
|
|
487
|
+
if (!profile)
|
|
488
|
+
return undefined;
|
|
489
|
+
const storedSlug = profile.metadata?.slug?.trim();
|
|
490
|
+
const slug = storedSlug
|
|
491
|
+
? storedSlug.toLowerCase()
|
|
492
|
+
: profile.name
|
|
493
|
+
.trim()
|
|
494
|
+
.toLowerCase()
|
|
495
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
496
|
+
.replace(/-+/g, '-')
|
|
497
|
+
.replace(/^-+|-+$/g, '')
|
|
498
|
+
.slice(0, 64);
|
|
499
|
+
return { slug: slug || undefined, name: profile.name };
|
|
500
|
+
}
|
|
501
|
+
// LIFECYCLE
|
|
502
|
+
async initialize() {
|
|
503
|
+
if (this.initialized)
|
|
504
|
+
return;
|
|
505
|
+
// Storage descriptors handle their own initialization
|
|
506
|
+
// No need to call storage.initialize()
|
|
507
|
+
await this.ensureStorage();
|
|
508
|
+
this.initialized = true;
|
|
509
|
+
await this.recoverActiveSession();
|
|
510
|
+
}
|
|
511
|
+
async close() {
|
|
512
|
+
await this.persistMappingsTail;
|
|
513
|
+
// Instances this host kept open stop heartbeating and expire on their TTL.
|
|
514
|
+
// Closing writes nothing: storage refuses new work while its runtime ends.
|
|
515
|
+
this.stopInstanceHeartbeat();
|
|
516
|
+
this.hostedInstances.clear();
|
|
517
|
+
// Storage descriptors handle their own cleanup
|
|
518
|
+
this.initialized = false;
|
|
519
|
+
this.participantSessions.clear();
|
|
520
|
+
this.mcpSessions.clear();
|
|
521
|
+
this.defaultSessionId = null;
|
|
522
|
+
this.defaultParticipantId = null;
|
|
523
|
+
// Clear storage references
|
|
524
|
+
this.sessionsStorage = undefined;
|
|
525
|
+
this.todosStorage = undefined;
|
|
526
|
+
this.scratchpadStorage = undefined;
|
|
527
|
+
this.jobsStorage = undefined;
|
|
528
|
+
this.messagesStorage = undefined;
|
|
529
|
+
this.participantsStorage = undefined;
|
|
530
|
+
this.presenceStorage = undefined;
|
|
531
|
+
this.knowledgeRefsStorage = undefined;
|
|
532
|
+
this.membershipLeasesStorage = undefined;
|
|
533
|
+
this.connectionsStorage = undefined;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Resolves `true` when session storage answers a query. Storage that cannot
|
|
537
|
+
* answer is a typed `UNAVAILABLE session-storage-unavailable` refusal
|
|
538
|
+
* carrying the storage failure, never a silent `false`.
|
|
539
|
+
*/
|
|
540
|
+
async healthCheck() {
|
|
541
|
+
await this.ensureStorage();
|
|
542
|
+
try {
|
|
543
|
+
await this.sessionsStorage.list({ limit: 0 });
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
throw new CapabilityError('Session storage is unavailable.', 'UNAVAILABLE', {
|
|
547
|
+
reason: 'session-storage-unavailable',
|
|
548
|
+
cause: error
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
// SESSIONS
|
|
554
|
+
/**
|
|
555
|
+
* Create a session. An active creation becomes the workspace's durable
|
|
556
|
+
* default unless `adoptAsDefault: false` — a per-native-session continuity
|
|
557
|
+
* row minted by an external-tool hook is not "the process' current binding"
|
|
558
|
+
* and must not retarget every later anonymous bind onto itself.
|
|
559
|
+
*/
|
|
560
|
+
async createSession(session, options = {}) {
|
|
561
|
+
await this.ensureStorage();
|
|
562
|
+
const now = new Date();
|
|
563
|
+
const id = options.id === undefined ? undefined : z.uuid().parse(options.id);
|
|
564
|
+
if (id && (session.metadata?.source !== 'external-hook' || !session.metadata.nativeSessionId || !session.metadata.externalToolId)) {
|
|
565
|
+
throw new CapabilityError('An explicit continuity session id requires native identity.', 'INVALID_INPUT', { reason: 'session-native-identity-required' });
|
|
566
|
+
}
|
|
567
|
+
const write = () => this.sessionsStorage.create({
|
|
568
|
+
...session,
|
|
569
|
+
id,
|
|
570
|
+
status: session.status ?? 'active',
|
|
571
|
+
scope: session.scope ?? 'project',
|
|
572
|
+
tags: session.tags ?? [],
|
|
573
|
+
metadata: session.metadata ?? {},
|
|
574
|
+
lastActivityAt: now
|
|
575
|
+
});
|
|
576
|
+
const created = id ? await writeOrRecover(write, async () => {
|
|
577
|
+
const existing = await this.sessionsStorage.get(id);
|
|
578
|
+
if (!existing || existing.ownerParticipant !== session.ownerParticipant ||
|
|
579
|
+
existing.metadata?.source !== session.metadata?.source ||
|
|
580
|
+
existing.metadata?.nativeSessionId !== session.metadata?.nativeSessionId ||
|
|
581
|
+
existing.metadata?.externalToolId !== session.metadata?.externalToolId ||
|
|
582
|
+
existing.metadata?.workspacePath !== session.metadata?.workspacePath)
|
|
583
|
+
return null;
|
|
584
|
+
return existing;
|
|
585
|
+
}, isUniqueConstraintError) : await write();
|
|
586
|
+
if (created.status === 'active') {
|
|
587
|
+
if (options.adoptAsDefault !== false) {
|
|
588
|
+
await this.setDefaultActiveSession(created.id);
|
|
589
|
+
}
|
|
590
|
+
await this.setParticipantActiveSession(created.ownerParticipant, created.id);
|
|
591
|
+
if (created.assignedParticipant) {
|
|
592
|
+
await this.setParticipantActiveSession(created.assignedParticipant, created.id);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return created;
|
|
596
|
+
}
|
|
597
|
+
async getSession(id) {
|
|
598
|
+
await this.ensureStorage();
|
|
599
|
+
return this.sessionsStorage.get(id);
|
|
600
|
+
}
|
|
601
|
+
async updateSession(id, updates) {
|
|
602
|
+
await this.ensureStorage();
|
|
603
|
+
const { parentSessionId, ...rest } = updates;
|
|
604
|
+
if (parentSessionId !== undefined)
|
|
605
|
+
await this.setSessionParent(id, parentSessionId);
|
|
606
|
+
await this.sessionsStorage.update(id, {
|
|
607
|
+
...rest,
|
|
608
|
+
updatedAt: new Date()
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Place a session under a parent session (a subsession), or make it top
|
|
613
|
+
* level with `null`. The parent must exist, and may not be the session itself
|
|
614
|
+
* or any of its descendants: a cycle is refused `INVALID_INPUT
|
|
615
|
+
* session-parent-cycle`.
|
|
616
|
+
*/
|
|
617
|
+
async setSessionParent(sessionId, parentSessionId) {
|
|
618
|
+
await this.ensureStorage();
|
|
619
|
+
if (parentSessionId !== null) {
|
|
620
|
+
const seen = new Set();
|
|
621
|
+
for (let ancestor = parentSessionId; ancestor;) {
|
|
622
|
+
if (ancestor === sessionId || seen.has(ancestor)) {
|
|
623
|
+
throw new CapabilityError('A session cannot be placed under itself or one of its own subsessions.', 'INVALID_INPUT', {
|
|
624
|
+
reason: 'session-parent-cycle',
|
|
625
|
+
details: { sessionId, parentSessionId }
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
seen.add(ancestor);
|
|
629
|
+
const node = await this.sessionsStorage.get(ancestor);
|
|
630
|
+
if (!node && ancestor === parentSessionId) {
|
|
631
|
+
throw new CapabilityError('The parent session does not exist.', 'NOT_FOUND', {
|
|
632
|
+
reason: 'session-parent-not-found',
|
|
633
|
+
details: { parentSessionId }
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
// A dangling ancestor further up ends the chain; only the parent must exist.
|
|
637
|
+
ancestor = node?.parentSessionId;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
await this.sessionsStorage.update(sessionId, { parentSessionId, updatedAt: new Date() });
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Bump a session's activity clock to reflect real traffic (messages), so
|
|
644
|
+
* `lastActivityAt` tracks when the session was last USED — not merely when its
|
|
645
|
+
* record was created. Before this, `lastActivityAt` was stamped only at
|
|
646
|
+
* createSession, so a live session reads hours-stale while agents are actively
|
|
647
|
+
* messaging on it — a false "dark/idle" signal that mislead liveness reads.
|
|
648
|
+
* Best-effort: an activity touch must NEVER fail the underlying operation.
|
|
649
|
+
*/
|
|
650
|
+
async touchSessionActivity(sessionId, at) {
|
|
651
|
+
const logger = getLogger('session:manager', this.runtime);
|
|
652
|
+
try {
|
|
653
|
+
await this.sessionsStorage.update(sessionId, { lastActivityAt: at });
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
// Activity tracking is advisory; never break the caller on it — but a
|
|
657
|
+
// swallowed failure here leaves lastActivityAt silently stale, which
|
|
658
|
+
// reads as a false "idle" liveness signal. Report the value-free facts
|
|
659
|
+
// so the failure is observable without repeating storage text.
|
|
660
|
+
logger.warn('Failed to bump session lastActivityAt', {
|
|
661
|
+
sessionId,
|
|
662
|
+
error: valueFreeFailure(CaughtFailureSchema.parse(error))
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async listSessions(filters) {
|
|
667
|
+
await this.ensureStorage();
|
|
668
|
+
const where = {};
|
|
669
|
+
if (filters?.status)
|
|
670
|
+
where.status = filters.status;
|
|
671
|
+
if (filters?.ownerParticipant)
|
|
672
|
+
where.ownerParticipant = filters.ownerParticipant;
|
|
673
|
+
const result = await this.sessionsStorage.list({
|
|
674
|
+
where,
|
|
675
|
+
orderBy: { lastActivityAt: 'desc' },
|
|
676
|
+
limit: filters?.limit ?? 100,
|
|
677
|
+
offset: filters?.offset ?? 0
|
|
678
|
+
});
|
|
679
|
+
return Promise.all(result.items.map((session) => this.buildSessionSummary(session)));
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Count sessions matching the filter without pagination.
|
|
683
|
+
*
|
|
684
|
+
* Routines need a way to distinguish "this page is empty" from
|
|
685
|
+
* "the underlying set is empty," so this exposes the unfiltered
|
|
686
|
+
* count even when callers paginate via `listSessions`.
|
|
687
|
+
*/
|
|
688
|
+
async countSessions(filters) {
|
|
689
|
+
await this.ensureStorage();
|
|
690
|
+
const where = {};
|
|
691
|
+
if (filters?.status)
|
|
692
|
+
where.status = filters.status;
|
|
693
|
+
if (filters?.ownerParticipant)
|
|
694
|
+
where.ownerParticipant = filters.ownerParticipant;
|
|
695
|
+
// True COUNT(*) — never a loaded page's length (ledger L-11).
|
|
696
|
+
return this.countRecords(this.sessionsStorage, where);
|
|
697
|
+
}
|
|
698
|
+
async getActiveSession() {
|
|
699
|
+
await this.ensureStorage();
|
|
700
|
+
const result = await this.sessionsStorage.list({
|
|
701
|
+
where: { status: 'active' },
|
|
702
|
+
orderBy: { lastActivityAt: 'desc' },
|
|
703
|
+
limit: 1
|
|
704
|
+
});
|
|
705
|
+
return result.items[0] ?? null;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Pause a specific session by id. Prefer this over pauseActiveSession when
|
|
709
|
+
* the caller has a concrete target (e.g. a UI selection): with more than one
|
|
710
|
+
* active session, "the active session" is ambiguous and pausing the wrong
|
|
711
|
+
* one is a real hazard.
|
|
712
|
+
*/
|
|
713
|
+
async pauseSession(id) {
|
|
714
|
+
await this.ensureStorage();
|
|
715
|
+
await this.updateSession(id, { status: 'paused' });
|
|
716
|
+
// Keep participant and MCP mappings. Concurrent lanes share one manager;
|
|
717
|
+
// wiping bindings on pause is what made the next send unresolvable and
|
|
718
|
+
// forced a mint. Archive still clears mappings.
|
|
719
|
+
if (this.defaultSessionId === id) {
|
|
720
|
+
await this.setDefaultActiveSession(null);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Pause the active session and optionally create a new one
|
|
725
|
+
*/
|
|
726
|
+
async pauseActiveSession() {
|
|
727
|
+
const active = await this.getActiveSession();
|
|
728
|
+
if (active) {
|
|
729
|
+
await this.pauseSession(active.id);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Resume a paused session (sets it to active)
|
|
734
|
+
*/
|
|
735
|
+
async resumeSession(id) {
|
|
736
|
+
await this.ensureStorage();
|
|
737
|
+
if (!(await this.getSession(id)))
|
|
738
|
+
throw sessionNotFound(id);
|
|
739
|
+
// Several collaborations can be active at once. Pausing "the" current
|
|
740
|
+
// session here is what severed every other bound lane tonight.
|
|
741
|
+
await this.updateSession(id, { status: 'active' });
|
|
742
|
+
const resumed = await this.getSession(id);
|
|
743
|
+
if (!resumed) {
|
|
744
|
+
throw sessionNotFound(id);
|
|
745
|
+
}
|
|
746
|
+
await this.setDefaultActiveSession(resumed.id);
|
|
747
|
+
await this.setParticipantActiveSession(resumed.ownerParticipant, resumed.id);
|
|
748
|
+
if (resumed.assignedParticipant) {
|
|
749
|
+
await this.setParticipantActiveSession(resumed.assignedParticipant, resumed.id);
|
|
750
|
+
}
|
|
751
|
+
return resumed;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Open a session for bound work. Paused sessions resume in place.
|
|
755
|
+
* Archived sessions stay closed until the explicit resume action.
|
|
756
|
+
*/
|
|
757
|
+
async openSessionForWork(sessionId) {
|
|
758
|
+
await this.ensureStorage();
|
|
759
|
+
const session = await this.getSession(sessionId);
|
|
760
|
+
if (!session) {
|
|
761
|
+
throw sessionNotFound(sessionId);
|
|
762
|
+
}
|
|
763
|
+
if (session.status === 'archived') {
|
|
764
|
+
throw sessionNotWritable(sessionId, session.status);
|
|
765
|
+
}
|
|
766
|
+
if (session.status === 'paused') {
|
|
767
|
+
return this.resumeSession(sessionId);
|
|
768
|
+
}
|
|
769
|
+
return session;
|
|
770
|
+
}
|
|
771
|
+
/** Participant ids currently mapped to this session. */
|
|
772
|
+
async listBoundParticipantIds(sessionId) {
|
|
773
|
+
await this.ensureStorage();
|
|
774
|
+
const ids = [];
|
|
775
|
+
for (const [participantId, boundId] of this.participantSessions) {
|
|
776
|
+
if (boundId === sessionId)
|
|
777
|
+
ids.push(participantId);
|
|
778
|
+
}
|
|
779
|
+
return ids;
|
|
780
|
+
}
|
|
781
|
+
async releaseAllMembershipLeases(sessionId) {
|
|
782
|
+
const pageSize = 500;
|
|
783
|
+
const where = {
|
|
784
|
+
sessionId,
|
|
785
|
+
releasedAt: { isNull: true }
|
|
786
|
+
};
|
|
787
|
+
for (;;) {
|
|
788
|
+
const result = await this.membershipLeasesStorage.list({
|
|
789
|
+
where,
|
|
790
|
+
limit: pageSize,
|
|
791
|
+
offset: 0
|
|
792
|
+
});
|
|
793
|
+
if (result.items.length === 0)
|
|
794
|
+
return;
|
|
795
|
+
const releasedAt = new Date().toISOString();
|
|
796
|
+
await Promise.all(result.items.map((lease) => this.membershipLeasesStorage.update(lease.id, {
|
|
797
|
+
releasedAt,
|
|
798
|
+
activeConnectionKey: null
|
|
799
|
+
})));
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async archiveSession(id) {
|
|
803
|
+
await this.ensureStorage();
|
|
804
|
+
await this.releaseAllMembershipLeases(id);
|
|
805
|
+
await this.sessionsStorage.update(id, {
|
|
806
|
+
status: 'archived',
|
|
807
|
+
archivedAt: new Date()
|
|
808
|
+
});
|
|
809
|
+
await this.clearMappingsForSession(id);
|
|
810
|
+
if (this.defaultSessionId === id) {
|
|
811
|
+
await this.setDefaultActiveSession(null);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
async deleteSession(id) {
|
|
815
|
+
await this.ensureStorage();
|
|
816
|
+
await this.sessionsStorage.delete(id);
|
|
817
|
+
await this.clearMappingsForSession(id);
|
|
818
|
+
if (this.defaultSessionId === id) {
|
|
819
|
+
await this.setDefaultActiveSession(null);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
async searchSessions(query, filters) {
|
|
823
|
+
const { results } = await this.searchSessionsPaged(query, filters);
|
|
824
|
+
return results;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Session search with true match total (L-11 class).
|
|
828
|
+
* Storage search already returns COUNT over MATCH+where before limit;
|
|
829
|
+
* discard-only wrappers were re-introducing page-as-total at CLI/MCP.
|
|
830
|
+
*/
|
|
831
|
+
async searchSessionsPaged(query, filters) {
|
|
832
|
+
await this.ensureStorage();
|
|
833
|
+
const where = {};
|
|
834
|
+
if (filters?.status)
|
|
835
|
+
where.status = filters.status;
|
|
836
|
+
const result = await this.sessionsStorage.search(query, {
|
|
837
|
+
where,
|
|
838
|
+
limit: filters?.limit ?? 100,
|
|
839
|
+
offset: filters?.offset ?? 0
|
|
840
|
+
});
|
|
841
|
+
const results = await Promise.all(result.items.map((session) => this.buildSessionSummary(session)));
|
|
842
|
+
// Prefer storage COUNT; never silently fall back to page length (L-31 advisory).
|
|
843
|
+
const total = SearchTotalSchema.safeParse(result.total);
|
|
844
|
+
if (!total.success) {
|
|
845
|
+
throw new CapabilityError('Session search returned no match total; a page length is not a match count.', 'INTERNAL', { reason: 'search-total-missing' });
|
|
846
|
+
}
|
|
847
|
+
return { results, total: total.data };
|
|
848
|
+
}
|
|
849
|
+
// SESSION HIERARCHY (Parent/Child Sessions)
|
|
850
|
+
/**
|
|
851
|
+
* Get child sessions for a parent session
|
|
852
|
+
*/
|
|
853
|
+
async getChildSessions(parentSessionId) {
|
|
854
|
+
await this.ensureStorage();
|
|
855
|
+
const result = await this.sessionsStorage.list({
|
|
856
|
+
where: { parentSessionId },
|
|
857
|
+
limit: 1000
|
|
858
|
+
});
|
|
859
|
+
return result.items;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Get parent session if one exists
|
|
863
|
+
*/
|
|
864
|
+
async getParentSession(childSessionId) {
|
|
865
|
+
await this.ensureStorage();
|
|
866
|
+
const child = await this.sessionsStorage.get(childSessionId);
|
|
867
|
+
if (!child || !child.parentSessionId) {
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
return this.sessionsStorage.get(child.parentSessionId);
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Create a child session linked to a parent
|
|
874
|
+
*/
|
|
875
|
+
async createChildSession(parentSessionId, session) {
|
|
876
|
+
await this.ensureStorage();
|
|
877
|
+
return this.createSession({
|
|
878
|
+
...session,
|
|
879
|
+
parentSessionId
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
async createTodo(sessionId, todo) {
|
|
883
|
+
await this.ensureStorage();
|
|
884
|
+
const session = await this.sessionsStorage.get(sessionId);
|
|
885
|
+
if (!session)
|
|
886
|
+
throw sessionNotFound(sessionId);
|
|
887
|
+
const scopedTodo = scopeTodoWorkLinks(todo, session.projectId);
|
|
888
|
+
return this.todosStorage.create({
|
|
889
|
+
...scopedTodo,
|
|
890
|
+
status: scopedTodo.status ?? 'pending',
|
|
891
|
+
priority: scopedTodo.priority ?? 'medium',
|
|
892
|
+
dependencies: scopedTodo.dependencies ?? [],
|
|
893
|
+
tags: scopedTodo.tags ?? [],
|
|
894
|
+
metadata: scopedTodo.metadata ?? {},
|
|
895
|
+
sessionId
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
/** Create once for one native todo identity; never overwrite another record. */
|
|
899
|
+
async ensureNativeTodo(sessionId, native, todo) {
|
|
900
|
+
await this.ensureStorage();
|
|
901
|
+
const identity = z.object({ tool: z.string().min(1), session: z.string().min(1), id: z.string().min(1) }).parse(native);
|
|
902
|
+
const session = await this.sessionsStorage.get(sessionId);
|
|
903
|
+
if (!session)
|
|
904
|
+
throw sessionNotFound(sessionId);
|
|
905
|
+
const scoped = scopeTodoWorkLinks(todo, session.projectId);
|
|
906
|
+
const hex = sha256Hex(JSON.stringify(['native-todo', sessionId, identity.tool, identity.session, identity.id])).slice(0, 32);
|
|
907
|
+
const variant = ((parseInt(hex[16], 16) & 0x3) | 0x8).toString(16);
|
|
908
|
+
const id = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-${variant}${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
909
|
+
const metadata = { ...scoped.metadata, nativeTool: identity.tool, nativeSession: identity.session, nativeTodo: identity.id };
|
|
910
|
+
return writeOrRecover(() => this.todosStorage.create({
|
|
911
|
+
...scoped, id, sessionId, metadata, status: scoped.status ?? 'pending', priority: scoped.priority ?? 'medium',
|
|
912
|
+
dependencies: scoped.dependencies ?? [], tags: scoped.tags ?? []
|
|
913
|
+
}), async () => {
|
|
914
|
+
const existing = await this.todosStorage.get(id);
|
|
915
|
+
return existing && existing.sessionId === sessionId && existing.metadata.nativeTool === identity.tool &&
|
|
916
|
+
existing.metadata.nativeSession === identity.session && existing.metadata.nativeTodo === identity.id ? existing : null;
|
|
917
|
+
}, isUniqueConstraintError);
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Legacy alias retained for CLI compatibility.
|
|
921
|
+
*/
|
|
922
|
+
addTodo(sessionId, todo) {
|
|
923
|
+
return this.createTodo(sessionId, todo);
|
|
924
|
+
}
|
|
925
|
+
async getTodo(id) {
|
|
926
|
+
await this.ensureStorage();
|
|
927
|
+
return this.todosStorage.get(id);
|
|
928
|
+
}
|
|
929
|
+
async updateTodo(id, updates) {
|
|
930
|
+
await this.ensureStorage();
|
|
931
|
+
const current = await this.todosStorage.get(id);
|
|
932
|
+
if (!current)
|
|
933
|
+
throw todoNotFound(id);
|
|
934
|
+
const session = await this.sessionsStorage.get(current.sessionId);
|
|
935
|
+
if (!session)
|
|
936
|
+
throw sessionNotFound(current.sessionId);
|
|
937
|
+
const merged = normalizeTodoWorkLink({ ...current, ...updates }, { rewriteExisting: updates.workItemId !== undefined });
|
|
938
|
+
const scoped = scopeTodoWorkLinks(merged, session.projectId);
|
|
939
|
+
const carriesWorkScope = scoped.workItemId !== undefined ||
|
|
940
|
+
(scoped.links ?? []).some((link) => link.kind === 'work-item' || (link.kind === 'project' && link.role === 'belongs-to'));
|
|
941
|
+
const persistedUpdates = { ...updates, updatedAt: new Date() };
|
|
942
|
+
if (carriesWorkScope) {
|
|
943
|
+
persistedUpdates.workItemId = scoped.workItemId;
|
|
944
|
+
persistedUpdates.links = scoped.links;
|
|
945
|
+
}
|
|
946
|
+
await this.todosStorage.update(id, persistedUpdates);
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Legacy alias retained for CLI compatibility.
|
|
950
|
+
*/
|
|
951
|
+
async completeTodo(id, updates = {}) {
|
|
952
|
+
await this.updateTodo(id, {
|
|
953
|
+
...updates,
|
|
954
|
+
status: 'completed',
|
|
955
|
+
completedAt: new Date()
|
|
956
|
+
});
|
|
957
|
+
const todo = await this.todosStorage.get(id);
|
|
958
|
+
if (!todo)
|
|
959
|
+
throw todoNotFound(id);
|
|
960
|
+
return todo;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* The storage filter for a session's todos. `tags` is exact all-of
|
|
964
|
+
* membership; `workItemId` is indexed. `hasDependencies`/`isBlocked` are
|
|
965
|
+
* not storage predicates: {@link scanTodosByDependency} applies them.
|
|
966
|
+
*/
|
|
967
|
+
buildTodoWhere(sessionId, filters) {
|
|
968
|
+
const where = { sessionId };
|
|
969
|
+
if (filters?.status)
|
|
970
|
+
where.status = filters.status;
|
|
971
|
+
if (filters?.priority)
|
|
972
|
+
where.priority = filters.priority;
|
|
973
|
+
if (filters?.assignedParticipant)
|
|
974
|
+
where.assignedParticipant = filters.assignedParticipant;
|
|
975
|
+
if (filters?.workItemId)
|
|
976
|
+
where.workItemId = filters.workItemId;
|
|
977
|
+
if (filters?.tags !== undefined)
|
|
978
|
+
where.tags = { hasAll: filters.tags };
|
|
979
|
+
if (filters?.parentId !== undefined) {
|
|
980
|
+
where.parentId = filters.parentId === null ? { isNull: true } : filters.parentId;
|
|
981
|
+
}
|
|
982
|
+
return where;
|
|
983
|
+
}
|
|
984
|
+
async countTodos(sessionId, filters) {
|
|
985
|
+
await this.ensureStorage();
|
|
986
|
+
if (hasDependencyFilter(filters))
|
|
987
|
+
return (await this.scanTodosByDependency(sessionId, filters)).length;
|
|
988
|
+
return this.countRecords(this.todosStorage, this.buildTodoWhere(sessionId, filters));
|
|
989
|
+
}
|
|
990
|
+
async listTodos(sessionId, filters) {
|
|
991
|
+
await this.ensureStorage();
|
|
992
|
+
const limit = filters?.limit ?? 100;
|
|
993
|
+
const offset = filters?.offset ?? 0;
|
|
994
|
+
if (hasDependencyFilter(filters)) {
|
|
995
|
+
return (await this.scanTodosByDependency(sessionId, filters)).slice(offset, offset + limit);
|
|
996
|
+
}
|
|
997
|
+
return this.listTodoPage(this.buildTodoWhere(sessionId, filters), limit, offset);
|
|
998
|
+
}
|
|
999
|
+
/** One page of todos in the default order; the id tiebreak keeps successive pages stable. */
|
|
1000
|
+
async listTodoPage(where, limit, offset) {
|
|
1001
|
+
const result = await this.todosStorage.list({ where, orderBy: TODO_ORDER, limit, offset });
|
|
1002
|
+
return result.items;
|
|
1003
|
+
}
|
|
1004
|
+
/** Every todo matching `where`, paged exhaustively (no row ceiling). */
|
|
1005
|
+
async scanTodos(where) {
|
|
1006
|
+
const all = [];
|
|
1007
|
+
for (let offset = 0;; offset += TODO_SCAN_PAGE) {
|
|
1008
|
+
const page = await this.listTodoPage(where, TODO_SCAN_PAGE, offset);
|
|
1009
|
+
all.push(...page);
|
|
1010
|
+
if (page.length < TODO_SCAN_PAGE)
|
|
1011
|
+
return all;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* The session's todos matching the storage filters and the dependency
|
|
1016
|
+
* predicates, in the default order. Exhaustive, so callers page and count
|
|
1017
|
+
* the full match set: `isBlocked` follows {@link computeTodo}.
|
|
1018
|
+
*/
|
|
1019
|
+
async scanTodosByDependency(sessionId, filters) {
|
|
1020
|
+
const scanned = await this.scanTodos(this.buildTodoWhere(sessionId, filters));
|
|
1021
|
+
const known = new Map(scanned.map((todo) => [todo.id, todo.status]));
|
|
1022
|
+
const matches = [];
|
|
1023
|
+
for (const todo of scanned) {
|
|
1024
|
+
if (filters.hasDependencies !== undefined && (todo.dependencies ?? []).length > 0 !== filters.hasDependencies)
|
|
1025
|
+
continue;
|
|
1026
|
+
if (filters.isBlocked !== undefined && (await this.incompleteDependencies(todo, known)).length > 0 !== filters.isBlocked)
|
|
1027
|
+
continue;
|
|
1028
|
+
matches.push(todo);
|
|
1029
|
+
}
|
|
1030
|
+
return matches;
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* The dependencies of `todo` not yet completed. A missing dependency row
|
|
1034
|
+
* does not block; one absent from `known` (another session's todo)
|
|
1035
|
+
* resolves through storage. The one blocked-dependency predicate.
|
|
1036
|
+
*/
|
|
1037
|
+
async incompleteDependencies(todo, known) {
|
|
1038
|
+
const incomplete = [];
|
|
1039
|
+
for (const dependencyId of todo.dependencies ?? []) {
|
|
1040
|
+
const status = known.has(dependencyId)
|
|
1041
|
+
? known.get(dependencyId)
|
|
1042
|
+
: (await this.todosStorage.get(dependencyId))?.status;
|
|
1043
|
+
if (status !== undefined && status !== 'completed')
|
|
1044
|
+
incomplete.push(dependencyId);
|
|
1045
|
+
}
|
|
1046
|
+
return incomplete;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Every todo linked to a work item, across all sessions, most recently
|
|
1050
|
+
* updated first. Exhaustive: the work-item trace reads it whole.
|
|
1051
|
+
*/
|
|
1052
|
+
async listTodosForWorkItem(workItemId) {
|
|
1053
|
+
await this.ensureStorage();
|
|
1054
|
+
const parsed = WorkItemIdSchema.safeParse(workItemId);
|
|
1055
|
+
if (!parsed.success) {
|
|
1056
|
+
throw new CapabilityError('A work item id must be a non-empty string.', 'INVALID_INPUT', {
|
|
1057
|
+
reason: 'work-item-id-invalid',
|
|
1058
|
+
retryable: false
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
const id = parsed.data;
|
|
1062
|
+
const all = [];
|
|
1063
|
+
for (let offset = 0;; offset += TODO_SCAN_PAGE) {
|
|
1064
|
+
const result = await this.todosStorage.list({
|
|
1065
|
+
where: { workItemId: id },
|
|
1066
|
+
orderBy: { updatedAt: 'desc', id: 'asc' },
|
|
1067
|
+
limit: TODO_SCAN_PAGE,
|
|
1068
|
+
offset
|
|
1069
|
+
});
|
|
1070
|
+
all.push(...result.items);
|
|
1071
|
+
if (result.items.length < TODO_SCAN_PAGE)
|
|
1072
|
+
return all;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
async getTodoWithComputed(id) {
|
|
1076
|
+
await this.ensureStorage();
|
|
1077
|
+
const todo = await this.todosStorage.get(id);
|
|
1078
|
+
return todo ? this.computeTodo(todo) : null;
|
|
1079
|
+
}
|
|
1080
|
+
async listTodosWithComputed(sessionId, filters) {
|
|
1081
|
+
await this.ensureStorage();
|
|
1082
|
+
const todos = await this.listTodos(sessionId, filters);
|
|
1083
|
+
return Promise.all(todos.map((todo) => this.computeTodo(todo)));
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* A todo with its display fields computed from storage: the dependencies
|
|
1087
|
+
* not yet completed (`blockedBy`; a missing dependency row does not block),
|
|
1088
|
+
* its direct subtask counts, and progress (completed share of subtasks, or
|
|
1089
|
+
* 0/100 by its own status when it has none).
|
|
1090
|
+
*/
|
|
1091
|
+
async computeTodo(todo) {
|
|
1092
|
+
const blockedBy = await this.incompleteDependencies(todo, new Map());
|
|
1093
|
+
const [subtaskCount, completedSubtaskCount] = await Promise.all([
|
|
1094
|
+
this.countRecords(this.todosStorage, { parentId: todo.id }),
|
|
1095
|
+
this.countRecords(this.todosStorage, { parentId: todo.id, status: 'completed' })
|
|
1096
|
+
]);
|
|
1097
|
+
const progress = subtaskCount > 0
|
|
1098
|
+
? Math.round((completedSubtaskCount / subtaskCount) * 100)
|
|
1099
|
+
: todo.status === 'completed'
|
|
1100
|
+
? 100
|
|
1101
|
+
: 0;
|
|
1102
|
+
return {
|
|
1103
|
+
...todo,
|
|
1104
|
+
isBlocked: blockedBy.length > 0,
|
|
1105
|
+
blockedBy,
|
|
1106
|
+
subtaskCount,
|
|
1107
|
+
completedSubtaskCount,
|
|
1108
|
+
progress
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
async deleteTodo(id) {
|
|
1112
|
+
await this.ensureStorage();
|
|
1113
|
+
await this.todosStorage.delete(id);
|
|
1114
|
+
}
|
|
1115
|
+
async getTodoHierarchy(parentId) {
|
|
1116
|
+
await this.ensureStorage();
|
|
1117
|
+
const result = await this.todosStorage.list({
|
|
1118
|
+
where: { parentId },
|
|
1119
|
+
limit: 100
|
|
1120
|
+
});
|
|
1121
|
+
return result.items;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Blocked means the todo's own explicit status — `status === 'blocked'`.
|
|
1125
|
+
* A todo waiting on incomplete dependencies is a different, truthful state;
|
|
1126
|
+
* ask {@link hasIncompleteDependencies} for that.
|
|
1127
|
+
*/
|
|
1128
|
+
async isTodoBlocked(id) {
|
|
1129
|
+
await this.ensureStorage();
|
|
1130
|
+
const todo = await this.todosStorage.get(id);
|
|
1131
|
+
return todo?.status === 'blocked';
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* True when the todo declares at least one dependency that is not yet
|
|
1135
|
+
* completed. Reports dependency state only — it says nothing about the
|
|
1136
|
+
* todo's own status (a completed todo with a stale incomplete dependency
|
|
1137
|
+
* still answers true here).
|
|
1138
|
+
*/
|
|
1139
|
+
async hasIncompleteDependencies(id) {
|
|
1140
|
+
await this.ensureStorage();
|
|
1141
|
+
const todo = await this.todosStorage.get(id);
|
|
1142
|
+
return todo ? (await this.incompleteDependencies(todo, new Map())).length > 0 : false;
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Get all root-level TODOs (no parent)
|
|
1146
|
+
*/
|
|
1147
|
+
async getRootTodos(sessionId, filters) {
|
|
1148
|
+
await this.ensureStorage();
|
|
1149
|
+
return this.listTodos(sessionId, {
|
|
1150
|
+
...filters,
|
|
1151
|
+
parentId: null
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Get subtasks for a parent TODO
|
|
1156
|
+
*/
|
|
1157
|
+
async getSubtasks(parentId) {
|
|
1158
|
+
await this.ensureStorage();
|
|
1159
|
+
const todo = await this.getTodo(parentId);
|
|
1160
|
+
if (!todo)
|
|
1161
|
+
return [];
|
|
1162
|
+
return this.listTodos(todo.sessionId, {
|
|
1163
|
+
parentId,
|
|
1164
|
+
limit: 100,
|
|
1165
|
+
offset: 0
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
// SCRATCHPAD
|
|
1169
|
+
async writeScratchpad(sessionId, entry) {
|
|
1170
|
+
await this.ensureStorage();
|
|
1171
|
+
// (sessionId, key) is unique — a second write to the same key must replace
|
|
1172
|
+
// the entry in place, not collide on the index.
|
|
1173
|
+
const existing = await this.readScratchpad(sessionId, entry.key);
|
|
1174
|
+
if (existing) {
|
|
1175
|
+
return this.replaceScratchpadEntry(existing, entry);
|
|
1176
|
+
}
|
|
1177
|
+
// TOCTOU: a concurrent first write can land between the read above and
|
|
1178
|
+
// this create. On a unique-index collision, replace the entry that won.
|
|
1179
|
+
return writeOrRecover(() => this.scratchpadStorage.create({
|
|
1180
|
+
...entry,
|
|
1181
|
+
tags: entry.tags ?? [],
|
|
1182
|
+
metadata: entry.metadata ?? {},
|
|
1183
|
+
sessionId
|
|
1184
|
+
}), async () => {
|
|
1185
|
+
const winner = await this.readScratchpad(sessionId, entry.key);
|
|
1186
|
+
return winner ? this.replaceScratchpadEntry(winner, entry) : null;
|
|
1187
|
+
}, isUniqueConstraintError);
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Replace an existing scratchpad entry with a fresh write.
|
|
1191
|
+
*
|
|
1192
|
+
* A write is a full replace, not a merge: every ScratchpadEntryCreate field
|
|
1193
|
+
* is passed explicitly with the create-branch defaults, so omitted optional
|
|
1194
|
+
* fields (tags/metadata/expiresAt/language) reset instead of silently
|
|
1195
|
+
* inheriting the previous entry's values. Only id and createdAt survive.
|
|
1196
|
+
*/
|
|
1197
|
+
async replaceScratchpadEntry(existing, entry) {
|
|
1198
|
+
await this.scratchpadStorage.update(existing.id, {
|
|
1199
|
+
content: entry.content,
|
|
1200
|
+
contentType: entry.contentType,
|
|
1201
|
+
language: entry.language,
|
|
1202
|
+
tags: entry.tags ?? [],
|
|
1203
|
+
metadata: entry.metadata ?? {},
|
|
1204
|
+
expiresAt: entry.expiresAt,
|
|
1205
|
+
updatedAt: new Date()
|
|
1206
|
+
});
|
|
1207
|
+
return (await this.scratchpadStorage.get(existing.id)) ?? existing;
|
|
1208
|
+
}
|
|
1209
|
+
async readScratchpad(sessionId, key) {
|
|
1210
|
+
await this.ensureStorage();
|
|
1211
|
+
const result = await this.scratchpadStorage.list({
|
|
1212
|
+
where: { sessionId, key },
|
|
1213
|
+
limit: 1
|
|
1214
|
+
});
|
|
1215
|
+
return result.items[0] ?? null;
|
|
1216
|
+
}
|
|
1217
|
+
async getScratchpadEntry(id) {
|
|
1218
|
+
await this.ensureStorage();
|
|
1219
|
+
return this.scratchpadStorage.get(id);
|
|
1220
|
+
}
|
|
1221
|
+
async updateScratchpadEntry(id, updates) {
|
|
1222
|
+
await this.ensureStorage();
|
|
1223
|
+
await this.scratchpadStorage.update(id, {
|
|
1224
|
+
...updates,
|
|
1225
|
+
updatedAt: new Date()
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
buildScratchpadWhere(sessionId, filters) {
|
|
1229
|
+
const where = { sessionId };
|
|
1230
|
+
if (filters?.contentType)
|
|
1231
|
+
where.contentType = filters.contentType;
|
|
1232
|
+
return where;
|
|
1233
|
+
}
|
|
1234
|
+
async countScratchpad(sessionId, filters) {
|
|
1235
|
+
await this.ensureStorage();
|
|
1236
|
+
return this.countRecords(this.scratchpadStorage, this.buildScratchpadWhere(sessionId, filters));
|
|
1237
|
+
}
|
|
1238
|
+
async listScratchpad(sessionId, filters) {
|
|
1239
|
+
await this.ensureStorage();
|
|
1240
|
+
const where = this.buildScratchpadWhere(sessionId, filters);
|
|
1241
|
+
const result = await this.scratchpadStorage.list({
|
|
1242
|
+
where,
|
|
1243
|
+
orderBy: { createdAt: 'desc' },
|
|
1244
|
+
limit: filters?.limit ?? 100,
|
|
1245
|
+
offset: filters?.offset ?? 0
|
|
1246
|
+
});
|
|
1247
|
+
return result.items.map(scratchpadSummaryOf);
|
|
1248
|
+
}
|
|
1249
|
+
async deleteScratchpadEntry(id) {
|
|
1250
|
+
await this.ensureStorage();
|
|
1251
|
+
await this.scratchpadStorage.delete(id);
|
|
1252
|
+
}
|
|
1253
|
+
async cleanupExpiredScratchpad() {
|
|
1254
|
+
await this.ensureStorage();
|
|
1255
|
+
// expiresAt is persisted as an ISO string; storage encodes a Date filter
|
|
1256
|
+
// operand as ISO text (ISO strings order lexicographically = temporally).
|
|
1257
|
+
const result = await this.scratchpadStorage.list({
|
|
1258
|
+
where: { expiresAt: { lt: new Date() } },
|
|
1259
|
+
limit: 1000
|
|
1260
|
+
});
|
|
1261
|
+
// Delete them
|
|
1262
|
+
for (const entry of result.items) {
|
|
1263
|
+
await this.scratchpadStorage.delete(entry.id);
|
|
1264
|
+
}
|
|
1265
|
+
return result.items.length;
|
|
1266
|
+
}
|
|
1267
|
+
async searchScratchpad(sessionId, query, filters) {
|
|
1268
|
+
await this.ensureStorage();
|
|
1269
|
+
const where = this.buildScratchpadWhere(sessionId, filters);
|
|
1270
|
+
const result = await this.scratchpadStorage.search(query, {
|
|
1271
|
+
where,
|
|
1272
|
+
limit: filters?.limit ?? 100,
|
|
1273
|
+
offset: filters?.offset ?? 0
|
|
1274
|
+
});
|
|
1275
|
+
// Convert to summary format; total is the true FTS match count from the
|
|
1276
|
+
// storage layer, not the returned page length (ledger L-11).
|
|
1277
|
+
return {
|
|
1278
|
+
items: result.items.map(scratchpadSummaryOf),
|
|
1279
|
+
total: result.total ?? result.items.length
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
// JOBS
|
|
1283
|
+
async addJobRef(sessionId, job) {
|
|
1284
|
+
await this.ensureStorage();
|
|
1285
|
+
return this.jobsStorage.create({
|
|
1286
|
+
...job,
|
|
1287
|
+
status: job.status ?? 'pending',
|
|
1288
|
+
metadata: job.metadata ?? {},
|
|
1289
|
+
sessionId
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
async getJobRef(id) {
|
|
1293
|
+
await this.ensureStorage();
|
|
1294
|
+
return this.jobsStorage.get(id);
|
|
1295
|
+
}
|
|
1296
|
+
async getJobRefByJobId(jobId) {
|
|
1297
|
+
await this.ensureStorage();
|
|
1298
|
+
const result = await this.jobsStorage.list({
|
|
1299
|
+
where: { jobId },
|
|
1300
|
+
limit: 1
|
|
1301
|
+
});
|
|
1302
|
+
return result.items[0] ?? null;
|
|
1303
|
+
}
|
|
1304
|
+
async updateJobRef(id, updates) {
|
|
1305
|
+
await this.ensureStorage();
|
|
1306
|
+
await this.jobsStorage.update(id, updates);
|
|
1307
|
+
}
|
|
1308
|
+
buildJobWhere(sessionId, filters) {
|
|
1309
|
+
const where = { sessionId };
|
|
1310
|
+
if (filters?.status)
|
|
1311
|
+
where.status = filters.status;
|
|
1312
|
+
if (filters?.jobType)
|
|
1313
|
+
where.jobType = filters.jobType;
|
|
1314
|
+
return where;
|
|
1315
|
+
}
|
|
1316
|
+
async countJobRefs(sessionId, filters) {
|
|
1317
|
+
await this.ensureStorage();
|
|
1318
|
+
return this.countRecords(this.jobsStorage, this.buildJobWhere(sessionId, filters));
|
|
1319
|
+
}
|
|
1320
|
+
async getJobRefs(sessionId, filters) {
|
|
1321
|
+
await this.ensureStorage();
|
|
1322
|
+
// Shared with countJobRefs so list and count cannot drift.
|
|
1323
|
+
const where = this.buildJobWhere(sessionId, filters);
|
|
1324
|
+
const result = await this.jobsStorage.list({
|
|
1325
|
+
where,
|
|
1326
|
+
orderBy: { createdAt: 'desc' },
|
|
1327
|
+
limit: filters?.limit ?? 100,
|
|
1328
|
+
offset: filters?.offset ?? 0
|
|
1329
|
+
});
|
|
1330
|
+
// Convert to summary format
|
|
1331
|
+
return result.items.map((job) => ({
|
|
1332
|
+
id: job.id,
|
|
1333
|
+
jobId: job.jobId,
|
|
1334
|
+
jobType: job.jobType,
|
|
1335
|
+
queueName: job.queueName,
|
|
1336
|
+
description: job.description,
|
|
1337
|
+
status: job.status,
|
|
1338
|
+
progress: job.progress,
|
|
1339
|
+
createdAt: job.createdAt,
|
|
1340
|
+
completedAt: job.completedAt
|
|
1341
|
+
}));
|
|
1342
|
+
}
|
|
1343
|
+
async deleteJobRef(id) {
|
|
1344
|
+
await this.ensureStorage();
|
|
1345
|
+
await this.jobsStorage.delete(id);
|
|
1346
|
+
}
|
|
1347
|
+
// MESSAGES
|
|
1348
|
+
async sendMessage(sessionId, message) {
|
|
1349
|
+
await this.ensureStorage();
|
|
1350
|
+
const now = new Date();
|
|
1351
|
+
const saved = await this.messagesStorage.create({
|
|
1352
|
+
...message,
|
|
1353
|
+
attachments: message.attachments ?? [],
|
|
1354
|
+
metadata: message.metadata ?? {},
|
|
1355
|
+
sessionId,
|
|
1356
|
+
isRead: false
|
|
1357
|
+
});
|
|
1358
|
+
await this.touchSessionActivity(sessionId, now);
|
|
1359
|
+
if (message.fromParticipant)
|
|
1360
|
+
await this.touchParticipantLastSeen(message.fromParticipant);
|
|
1361
|
+
if (message.toParticipant)
|
|
1362
|
+
await this.notifyUnread(sessionId, message.toParticipant);
|
|
1363
|
+
return saved;
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Advisory: bump the sender's lastSeen on a send. A failure never fails
|
|
1367
|
+
* the send; it is reported with value-free facts.
|
|
1368
|
+
*/
|
|
1369
|
+
async touchParticipantLastSeen(participantId) {
|
|
1370
|
+
const logger = getLogger('session:manager', this.runtime);
|
|
1371
|
+
try {
|
|
1372
|
+
await this.updateParticipantLastSeen(participantId);
|
|
1373
|
+
}
|
|
1374
|
+
catch (error) {
|
|
1375
|
+
logger.warn('Failed to update participant lastSeen on message send', {
|
|
1376
|
+
participantId,
|
|
1377
|
+
error: valueFreeFailure(CaughtFailureSchema.parse(error))
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Runtime-soft (SPEC): fan an unread-count notice out through the
|
|
1383
|
+
* notifications client at the 1st, 5th and 10th unread message. A
|
|
1384
|
+
* composition without notifications is confirmed absent and skipped; any
|
|
1385
|
+
* other failure never fails the send and is reported with value-free facts.
|
|
1386
|
+
*/
|
|
1387
|
+
async notifyUnread(sessionId, toParticipant) {
|
|
1388
|
+
const logger = getLogger('session:manager', this.runtime);
|
|
1389
|
+
try {
|
|
1390
|
+
const notifications = await this.resolveNotifications();
|
|
1391
|
+
if (!notifications)
|
|
1392
|
+
return;
|
|
1393
|
+
const unreadCount = await this.getUnreadCount(sessionId, toParticipant);
|
|
1394
|
+
if (unreadCount !== 1 && unreadCount !== 5 && unreadCount !== 10)
|
|
1395
|
+
return;
|
|
1396
|
+
// ParticipantProfile is keyed by participantId — route notifications to
|
|
1397
|
+
// that identity; consumers map participantId → user id out-of-band.
|
|
1398
|
+
const participant = await this.getParticipant(toParticipant);
|
|
1399
|
+
if (!participant)
|
|
1400
|
+
return;
|
|
1401
|
+
const plural = unreadCount > 1 ? 's' : '';
|
|
1402
|
+
await notifications.send({
|
|
1403
|
+
userId: participant.participantId,
|
|
1404
|
+
channel: 'in-app',
|
|
1405
|
+
priority: unreadCount >= 10 ? 'high' : 'normal',
|
|
1406
|
+
title: `${unreadCount} unread message${plural} in session`,
|
|
1407
|
+
body: `You have ${unreadCount} unread message${plural} waiting.`,
|
|
1408
|
+
actionUrl: `/sessions/${sessionId}`
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
catch (error) {
|
|
1412
|
+
logger.warn('Unread-message notification failed; the message was delivered', {
|
|
1413
|
+
sessionId,
|
|
1414
|
+
error: valueFreeFailure(CaughtFailureSchema.parse(error))
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Exactly-once send keyed on a caller-supplied deterministic id.
|
|
1420
|
+
*
|
|
1421
|
+
* A batch retry after a mid-batch failure re-sends entries whose first
|
|
1422
|
+
* attempt may or may not have persisted. With a stable per-entry id the
|
|
1423
|
+
* replayed create collides on the primary key instead of duplicating;
|
|
1424
|
+
* the existing row is returned with `replay: true` and the side effects
|
|
1425
|
+
* (activity bump, notification) are not re-fired.
|
|
1426
|
+
*
|
|
1427
|
+
* On ANY failure — not only a unique-constraint collision — durable state
|
|
1428
|
+
* is consulted by the deterministic id: a row can persist and a LATER
|
|
1429
|
+
* effect inside sendMessage throw, and classifying that schedule as
|
|
1430
|
+
* "failed" would make the receipt deny a delivery that exists. And a
|
|
1431
|
+
* found row only counts as a replay when its payload MATCHES the intended
|
|
1432
|
+
* send: the same id with different content/recipient is a caller bug that
|
|
1433
|
+
* must surface as a CONFLICT, never as a silent old-row "delivery".
|
|
1434
|
+
*/
|
|
1435
|
+
async sendMessageIdempotent(sessionId, message) {
|
|
1436
|
+
await this.ensureStorage();
|
|
1437
|
+
return writeOrRecover(async () => ({ message: await this.sendMessage(sessionId, message), replay: false }), async () => {
|
|
1438
|
+
const existing = await this.messagesStorage.get(message.id);
|
|
1439
|
+
if (!existing)
|
|
1440
|
+
return null;
|
|
1441
|
+
if (!messagePayloadMatches(existing, sessionId, message)) {
|
|
1442
|
+
throw new CapabilityError(`Idempotency conflict: message ${message.id} already exists with a different payload — a retry must not change recipient, type, content, or metadata under the same batch identity.`, 'CONFLICT', {
|
|
1443
|
+
reason: 'batch-idempotency-conflict',
|
|
1444
|
+
retryable: false,
|
|
1445
|
+
recoveryHint: 'Retry a failed batch with the SAME entries, or use a new batchId for changed content.',
|
|
1446
|
+
details: {
|
|
1447
|
+
kind: 'BATCH_IDEMPOTENCY_CONFLICT',
|
|
1448
|
+
messageId: message.id,
|
|
1449
|
+
storedToParticipant: existing.toParticipant,
|
|
1450
|
+
intendedToParticipant: message.toParticipant
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
return { message: existing, replay: true };
|
|
1455
|
+
}, anyFailure);
|
|
1456
|
+
}
|
|
1457
|
+
async getMessage(id) {
|
|
1458
|
+
await this.ensureStorage();
|
|
1459
|
+
return this.messagesStorage.get(id);
|
|
1460
|
+
}
|
|
1461
|
+
buildMessageWhere(sessionId, filters) {
|
|
1462
|
+
const where = { sessionId };
|
|
1463
|
+
if (filters?.fromParticipant)
|
|
1464
|
+
where.fromParticipant = filters.fromParticipant;
|
|
1465
|
+
if (filters?.toParticipant)
|
|
1466
|
+
where.toParticipant = filters.toParticipant;
|
|
1467
|
+
if (filters?.isRead !== undefined)
|
|
1468
|
+
where.isRead = filters.isRead;
|
|
1469
|
+
return where;
|
|
1470
|
+
}
|
|
1471
|
+
async countMessages(sessionId, filters) {
|
|
1472
|
+
await this.ensureStorage();
|
|
1473
|
+
return this.countRecords(this.messagesStorage, this.buildMessageWhere(sessionId, filters));
|
|
1474
|
+
}
|
|
1475
|
+
async getMessages(sessionId, filters) {
|
|
1476
|
+
await this.ensureStorage();
|
|
1477
|
+
// Shared with countMessages so list and count cannot drift.
|
|
1478
|
+
const where = this.buildMessageWhere(sessionId, filters);
|
|
1479
|
+
const result = await this.messagesStorage.list({
|
|
1480
|
+
where,
|
|
1481
|
+
orderBy: { createdAt: 'desc' },
|
|
1482
|
+
limit: filters?.limit ?? 100,
|
|
1483
|
+
offset: filters?.offset ?? 0
|
|
1484
|
+
});
|
|
1485
|
+
return result.items;
|
|
1486
|
+
}
|
|
1487
|
+
async markMessageRead(id) {
|
|
1488
|
+
await this.ensureStorage();
|
|
1489
|
+
await this.messagesStorage.update(id, {
|
|
1490
|
+
isRead: true,
|
|
1491
|
+
readAt: new Date()
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
async markAllMessagesRead(sessionId, toParticipant) {
|
|
1495
|
+
await this.ensureStorage();
|
|
1496
|
+
// Drain the WHOLE unread backlog in pages. A single capped list silently
|
|
1497
|
+
// stopped at 1000 and reported the cap as the full count (ledger L-11);
|
|
1498
|
+
// marking rows read removes them from the isRead:false filter, so
|
|
1499
|
+
// re-listing page 0 walks the backlog to a true zero.
|
|
1500
|
+
const PAGE_SIZE = 1000;
|
|
1501
|
+
let marked = 0;
|
|
1502
|
+
while (true) {
|
|
1503
|
+
const result = await this.messagesStorage.list({
|
|
1504
|
+
where: { sessionId, toParticipant, isRead: false },
|
|
1505
|
+
limit: PAGE_SIZE
|
|
1506
|
+
});
|
|
1507
|
+
if (result.items.length === 0)
|
|
1508
|
+
break;
|
|
1509
|
+
const now = new Date();
|
|
1510
|
+
for (const message of result.items) {
|
|
1511
|
+
await this.messagesStorage.update(message.id, {
|
|
1512
|
+
isRead: true,
|
|
1513
|
+
readAt: now
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
marked += result.items.length;
|
|
1517
|
+
if (result.items.length < PAGE_SIZE)
|
|
1518
|
+
break;
|
|
1519
|
+
}
|
|
1520
|
+
return marked;
|
|
1521
|
+
}
|
|
1522
|
+
async deleteMessage(id) {
|
|
1523
|
+
await this.ensureStorage();
|
|
1524
|
+
await this.messagesStorage.delete(id);
|
|
1525
|
+
}
|
|
1526
|
+
async getUnreadCount(sessionId, toParticipant) {
|
|
1527
|
+
// A true count — not the length of a capped page. A busy inbox past any
|
|
1528
|
+
// page ceiling must still report its real backlog.
|
|
1529
|
+
return this.countMessages(sessionId, { toParticipant, isRead: false });
|
|
1530
|
+
}
|
|
1531
|
+
// PARTICIPANTS
|
|
1532
|
+
async registerParticipant(profile) {
|
|
1533
|
+
await this.ensureStorage();
|
|
1534
|
+
const now = new Date();
|
|
1535
|
+
return this.participantsStorage.create({
|
|
1536
|
+
...profile,
|
|
1537
|
+
capabilities: profile.capabilities ?? [],
|
|
1538
|
+
preferences: profile.preferences ?? {},
|
|
1539
|
+
metadata: profile.metadata ?? {},
|
|
1540
|
+
lastSeenAt: now
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
async getParticipant(participantId) {
|
|
1544
|
+
await this.ensureStorage();
|
|
1545
|
+
return this.participantsStorage.get(participantId);
|
|
1546
|
+
}
|
|
1547
|
+
async getParticipantByConnection(connectionId) {
|
|
1548
|
+
await this.ensureStorage();
|
|
1549
|
+
// Find connection and get associated participant
|
|
1550
|
+
const connection = await this.connectionsStorage.get(connectionId);
|
|
1551
|
+
if (!connection)
|
|
1552
|
+
return null;
|
|
1553
|
+
if (!connection.participantId)
|
|
1554
|
+
return null;
|
|
1555
|
+
return this.participantsStorage.get(connection.participantId);
|
|
1556
|
+
}
|
|
1557
|
+
async updateParticipant(participantId, updates) {
|
|
1558
|
+
await this.ensureStorage();
|
|
1559
|
+
await this.participantsStorage.update(participantId, updates);
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* The seat for one harness conversation, read-only. See
|
|
1563
|
+
* `schemas/harness-seat.ts`: the id is derived, so this is a point read.
|
|
1564
|
+
*/
|
|
1565
|
+
async findHarnessSeat(seat) {
|
|
1566
|
+
return this.getParticipant(harnessSeatParticipantId(HarnessSeatSchema.parse(seat)));
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* The seat for one harness conversation, registered on first sight. The
|
|
1570
|
+
* harness hook and the MCP connection both call this, so they converge on
|
|
1571
|
+
* one participant whichever arrives first. The seat's identity (agent,
|
|
1572
|
+
* harness session, slug) is written onto rows registered before it was.
|
|
1573
|
+
*/
|
|
1574
|
+
async ensureHarnessSeat(input) {
|
|
1575
|
+
const registration = HarnessSeatRegistrationSchema.parse(input);
|
|
1576
|
+
const seat = { agentId: registration.agentId, harnessSessionId: registration.harnessSessionId };
|
|
1577
|
+
const participantId = harnessSeatParticipantId(seat);
|
|
1578
|
+
const slug = harnessSeatSlug(seat);
|
|
1579
|
+
const identity = { agentId: seat.agentId, harnessSessionId: seat.harnessSessionId };
|
|
1580
|
+
let existing = await this.getParticipant(participantId);
|
|
1581
|
+
if (!existing) {
|
|
1582
|
+
// The hook and the MCP connection of one conversation can race the
|
|
1583
|
+
// create; the re-read confirms the race — the derived row must exist.
|
|
1584
|
+
const outcome = await writeOrRecover(async () => ({
|
|
1585
|
+
created: true,
|
|
1586
|
+
profile: await this.registerParticipant({
|
|
1587
|
+
participantId,
|
|
1588
|
+
name: registration.name ?? seat.agentId,
|
|
1589
|
+
role: 'assistant',
|
|
1590
|
+
capabilities: registration.capabilities ?? ['session-management'],
|
|
1591
|
+
metadata: {
|
|
1592
|
+
source: registration.source ?? 'harness',
|
|
1593
|
+
slug,
|
|
1594
|
+
clientInfo: { name: registration.name ?? seat.agentId, ...identity }
|
|
1595
|
+
}
|
|
1596
|
+
})
|
|
1597
|
+
}), async () => {
|
|
1598
|
+
const raced = await this.getParticipant(participantId);
|
|
1599
|
+
return raced ? { created: false, profile: raced } : null;
|
|
1600
|
+
}, anyFailure);
|
|
1601
|
+
if (outcome.created)
|
|
1602
|
+
return outcome.profile;
|
|
1603
|
+
existing = outcome.profile;
|
|
1604
|
+
}
|
|
1605
|
+
const metadata = existing.metadata ?? {};
|
|
1606
|
+
const clientInfo = metadata.clientInfo;
|
|
1607
|
+
if (metadata.slug === slug &&
|
|
1608
|
+
clientInfo?.agentId === identity.agentId &&
|
|
1609
|
+
clientInfo?.harnessSessionId === identity.harnessSessionId) {
|
|
1610
|
+
return existing;
|
|
1611
|
+
}
|
|
1612
|
+
const updated = {
|
|
1613
|
+
...metadata,
|
|
1614
|
+
slug,
|
|
1615
|
+
clientInfo: { name: existing.name, ...clientInfo, ...identity }
|
|
1616
|
+
};
|
|
1617
|
+
await this.updateParticipant(participantId, { metadata: updated });
|
|
1618
|
+
return { ...existing, metadata: updated };
|
|
1619
|
+
}
|
|
1620
|
+
async updateParticipantLastSeen(participantId) {
|
|
1621
|
+
await this.ensureStorage();
|
|
1622
|
+
await this.participantsStorage.update(participantId, {
|
|
1623
|
+
lastSeenAt: new Date()
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
async listParticipants(filters) {
|
|
1627
|
+
await this.ensureStorage();
|
|
1628
|
+
const where = {};
|
|
1629
|
+
// `active` (legacy alias `isActive`): last seen within the activity window.
|
|
1630
|
+
const active = filters?.active ?? filters?.isActive;
|
|
1631
|
+
if (active !== undefined) {
|
|
1632
|
+
const threshold = new Date(Date.now() - PARTICIPANT_ACTIVE_WINDOW_MS);
|
|
1633
|
+
where.lastSeenAt = active ? { gte: threshold } : { lt: threshold };
|
|
1634
|
+
}
|
|
1635
|
+
if (filters?.role !== undefined) {
|
|
1636
|
+
where.role = filters.role;
|
|
1637
|
+
}
|
|
1638
|
+
if (filters?.search !== undefined && filters.search.trim().length > 0) {
|
|
1639
|
+
where.name = { contains: filters.search.trim() };
|
|
1640
|
+
}
|
|
1641
|
+
if (filters?.after !== undefined) {
|
|
1642
|
+
if (filters.orderBy !== 'stable') {
|
|
1643
|
+
throw new CapabilityError('A participant keyset cursor pages only the stable order.', 'INVALID_INPUT', {
|
|
1644
|
+
reason: 'participant-cursor-unordered',
|
|
1645
|
+
retryable: false
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
where.participantId = { gt: filters.after };
|
|
1649
|
+
}
|
|
1650
|
+
const result = await this.participantsStorage.list({
|
|
1651
|
+
where,
|
|
1652
|
+
// 'stable' = participantId asc (immutable, unique) so paged
|
|
1653
|
+
// enumerations cannot skip/double-visit rows when lastSeenAt moves
|
|
1654
|
+
// mid-scan (ledger L-19). Default stays recency for display callers.
|
|
1655
|
+
orderBy: filters?.orderBy === 'stable' ? { participantId: 'asc' } : { lastSeenAt: 'desc' },
|
|
1656
|
+
limit: filters?.limit ?? 100,
|
|
1657
|
+
offset: filters?.offset ?? 0
|
|
1658
|
+
});
|
|
1659
|
+
return result.items;
|
|
1660
|
+
}
|
|
1661
|
+
async deleteParticipant(participantId) {
|
|
1662
|
+
await this.ensureStorage();
|
|
1663
|
+
await this.participantsStorage.delete(participantId);
|
|
1664
|
+
}
|
|
1665
|
+
async pruneStaleSessionMappings() {
|
|
1666
|
+
await this.ensureStorage();
|
|
1667
|
+
const staleKeys = [];
|
|
1668
|
+
// A storage failure reading a session propagates: an unreadable store
|
|
1669
|
+
// is not proof the session is gone, and pruning on it would drop live
|
|
1670
|
+
// mappings wholesale.
|
|
1671
|
+
for (const [participantId, sessionId] of this.participantSessions) {
|
|
1672
|
+
const session = await this.sessionsStorage.get(sessionId);
|
|
1673
|
+
if (!session || session.status === 'archived') {
|
|
1674
|
+
staleKeys.push(participantId);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
for (const key of staleKeys) {
|
|
1678
|
+
this.participantSessions.delete(key);
|
|
1679
|
+
}
|
|
1680
|
+
if (staleKeys.length > 0) {
|
|
1681
|
+
await Promise.all(staleKeys.map((key) => this.deleteRoutingRow(`${SESSION_STATE_PARTICIPANT_PREFIX}${key}`)));
|
|
1682
|
+
}
|
|
1683
|
+
return {
|
|
1684
|
+
prunedEntries: staleKeys.length,
|
|
1685
|
+
remainingEntries: this.participantSessions.size,
|
|
1686
|
+
prunedKeys: staleKeys
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
async pruneOrphanedParticipantProfiles() {
|
|
1690
|
+
await this.ensureStorage();
|
|
1691
|
+
const activeSessionIds = new Set();
|
|
1692
|
+
const activeSessions = await this.listSessions({ status: 'active', limit: 10000 });
|
|
1693
|
+
for (const s of activeSessions)
|
|
1694
|
+
activeSessionIds.add(s.id);
|
|
1695
|
+
const pausedSessions = await this.listSessions({ status: 'paused', limit: 10000 });
|
|
1696
|
+
for (const s of pausedSessions)
|
|
1697
|
+
activeSessionIds.add(s.id);
|
|
1698
|
+
const protectedIds = new Set();
|
|
1699
|
+
for (const s of activeSessions) {
|
|
1700
|
+
if (s.ownerParticipant)
|
|
1701
|
+
protectedIds.add(s.ownerParticipant);
|
|
1702
|
+
if (s.assignedParticipant)
|
|
1703
|
+
protectedIds.add(s.assignedParticipant);
|
|
1704
|
+
}
|
|
1705
|
+
for (const s of pausedSessions) {
|
|
1706
|
+
if (s.ownerParticipant)
|
|
1707
|
+
protectedIds.add(s.ownerParticipant);
|
|
1708
|
+
if (s.assignedParticipant)
|
|
1709
|
+
protectedIds.add(s.assignedParticipant);
|
|
1710
|
+
}
|
|
1711
|
+
for (const [participantId, sessionId] of this.participantSessions) {
|
|
1712
|
+
if (activeSessionIds.has(sessionId))
|
|
1713
|
+
protectedIds.add(participantId);
|
|
1714
|
+
}
|
|
1715
|
+
const messageParticipants = new Set();
|
|
1716
|
+
try {
|
|
1717
|
+
const PAGE = 500;
|
|
1718
|
+
let msgOffset = 0;
|
|
1719
|
+
for (;;) {
|
|
1720
|
+
const msgPage = await this.messagesStorage.list({ limit: PAGE, offset: msgOffset });
|
|
1721
|
+
for (const msg of msgPage.items) {
|
|
1722
|
+
if (activeSessionIds.has(msg.sessionId)) {
|
|
1723
|
+
if (msg.fromParticipant)
|
|
1724
|
+
messageParticipants.add(msg.fromParticipant);
|
|
1725
|
+
if (msg.toParticipant)
|
|
1726
|
+
messageParticipants.add(msg.toParticipant);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
if (msgPage.items.length < PAGE)
|
|
1730
|
+
break;
|
|
1731
|
+
msgOffset += msgPage.items.length;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
catch (error) {
|
|
1735
|
+
throw new CapabilityError('Cannot safely prune participant profiles: message scan failed. A prune without full protection data is unsafe.', 'UNAVAILABLE', { reason: 'prune-protection-scan-failed', cause: error });
|
|
1736
|
+
}
|
|
1737
|
+
for (const pid of messageParticipants)
|
|
1738
|
+
protectedIds.add(pid);
|
|
1739
|
+
const prunedIds = [];
|
|
1740
|
+
let totalProcessed = 0;
|
|
1741
|
+
let offset = 0;
|
|
1742
|
+
const PAGE_SIZE = 100;
|
|
1743
|
+
while (true) {
|
|
1744
|
+
const result = await this.participantsStorage.list({
|
|
1745
|
+
orderBy: { participantId: 'asc' },
|
|
1746
|
+
limit: PAGE_SIZE,
|
|
1747
|
+
offset
|
|
1748
|
+
});
|
|
1749
|
+
if (result.items.length === 0)
|
|
1750
|
+
break;
|
|
1751
|
+
for (const profile of result.items) {
|
|
1752
|
+
totalProcessed++;
|
|
1753
|
+
if (!isFixtureProfile(profile))
|
|
1754
|
+
continue;
|
|
1755
|
+
if (protectedIds.has(profile.participantId))
|
|
1756
|
+
continue;
|
|
1757
|
+
prunedIds.push(profile.participantId);
|
|
1758
|
+
}
|
|
1759
|
+
offset += result.items.length;
|
|
1760
|
+
if (result.items.length < PAGE_SIZE)
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1763
|
+
// A failed delete propagates: reporting it as pruned would be false.
|
|
1764
|
+
for (const id of prunedIds) {
|
|
1765
|
+
await this.participantsStorage.delete(id);
|
|
1766
|
+
}
|
|
1767
|
+
return {
|
|
1768
|
+
prunedProfiles: prunedIds.length,
|
|
1769
|
+
remainingProfiles: totalProcessed - prunedIds.length,
|
|
1770
|
+
protectedCount: protectedIds.size,
|
|
1771
|
+
prunedIds
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
async recordConnection(connection) {
|
|
1775
|
+
await this.ensureStorage();
|
|
1776
|
+
const now = new Date();
|
|
1777
|
+
const existing = await this.connectionsStorage.get(connection.connectionId);
|
|
1778
|
+
if (existing) {
|
|
1779
|
+
const updated = {
|
|
1780
|
+
...connection,
|
|
1781
|
+
connectedAt: now,
|
|
1782
|
+
disconnectedAt: null
|
|
1783
|
+
};
|
|
1784
|
+
await this.connectionsStorage.update(connection.connectionId, updated);
|
|
1785
|
+
return { ...existing, ...updated };
|
|
1786
|
+
}
|
|
1787
|
+
return writeOrRecover(() => this.connectionsStorage.create({
|
|
1788
|
+
...connection,
|
|
1789
|
+
connectedAt: now,
|
|
1790
|
+
disconnectedAt: undefined
|
|
1791
|
+
}), async () => {
|
|
1792
|
+
await this.connectionsStorage.update(connection.connectionId, {
|
|
1793
|
+
...connection,
|
|
1794
|
+
connectedAt: now,
|
|
1795
|
+
disconnectedAt: null
|
|
1796
|
+
});
|
|
1797
|
+
return this.connectionsStorage.get(connection.connectionId);
|
|
1798
|
+
}, isUniqueConstraintError);
|
|
1799
|
+
}
|
|
1800
|
+
async closeConnection(connectionId) {
|
|
1801
|
+
await this.ensureStorage();
|
|
1802
|
+
await this.connectionsStorage.update(connectionId, {
|
|
1803
|
+
disconnectedAt: new Date()
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
// INSTANCES
|
|
1807
|
+
//
|
|
1808
|
+
// One `session.connections` row per running instance, whatever surface it
|
|
1809
|
+
// came through. Live = not disconnected and `expiresAt` in the future. Every
|
|
1810
|
+
// admitted call touches its instance; a host that keeps a seat open while
|
|
1811
|
+
// idle registers it `hosted`, and this manager heartbeats hosted instances
|
|
1812
|
+
// until it closes them. A crashed host's instances simply expire.
|
|
1813
|
+
static INSTANCE_TTL_MS = 10 * 60 * 1000;
|
|
1814
|
+
static INSTANCE_HEARTBEAT_MS = 60 * 1000;
|
|
1815
|
+
/** Touches inside this window are not rewritten: one write per instance per window. */
|
|
1816
|
+
static INSTANCE_TOUCH_MIN_MS = 30 * 1000;
|
|
1817
|
+
static INSTANCE_HANDLE_ATTEMPTS = 6;
|
|
1818
|
+
/** This host's own key: distinguishes its in-process seats from another host's. */
|
|
1819
|
+
hostKey = crypto.randomUUID();
|
|
1820
|
+
hostedInstances = new Set();
|
|
1821
|
+
instanceTouchedAt = new Map();
|
|
1822
|
+
/** Handles of instances this host registered, so a touch need not reread them. */
|
|
1823
|
+
instanceHandles = new Map();
|
|
1824
|
+
/** (instance, session, participant) triples this host has already leased. */
|
|
1825
|
+
instanceMemberships = new Set();
|
|
1826
|
+
instanceHeartbeat;
|
|
1827
|
+
static isLiveInstance(row, nowIso) {
|
|
1828
|
+
const expiresAt = row.expiresAt instanceof Date ? row.expiresAt.toISOString() : row.expiresAt;
|
|
1829
|
+
return !row.disconnectedAt && expiresAt != null && String(expiresAt) >= nowIso;
|
|
1830
|
+
}
|
|
1831
|
+
static randomHandle(base) {
|
|
1832
|
+
const alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
|
|
1833
|
+
const bytes = crypto.getRandomValues(new Uint8Array(4));
|
|
1834
|
+
const suffix = Array.from(bytes, (byte) => alphabet[byte % alphabet.length]).join('');
|
|
1835
|
+
return `${sanitizeSlugCandidate(base).slice(0, 48)}-${suffix}`;
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Register (or re-register) a running instance and give it a handle. A
|
|
1839
|
+
* chosen handle held by another LIVE instance is refused `CONFLICT
|
|
1840
|
+
* instance-handle-taken` naming the holder; one held by an expired or closed
|
|
1841
|
+
* instance is reclaimed. With no choice the instance keeps its live handle,
|
|
1842
|
+
* else gets a random `<agent>-<4>` handle. Uniqueness is the storage index
|
|
1843
|
+
* on `activeHandle`, not this check: a lost race is retried (random) or
|
|
1844
|
+
* refused (chosen).
|
|
1845
|
+
*/
|
|
1846
|
+
async registerInstance(input) {
|
|
1847
|
+
await this.ensureStorage();
|
|
1848
|
+
const storage = this.connectionsStorage;
|
|
1849
|
+
const now = Date.now();
|
|
1850
|
+
const nowAt = new Date(now);
|
|
1851
|
+
const nowIso = nowAt.toISOString();
|
|
1852
|
+
const expiresAt = new Date(now + SessionManager.INSTANCE_TTL_MS);
|
|
1853
|
+
const existing = await storage.get(input.instanceId);
|
|
1854
|
+
const chosen = input.handle?.trim() ? sanitizeSlugCandidate(input.handle) : undefined;
|
|
1855
|
+
// A live handle resolves ahead of participant slugs, so a chosen handle may
|
|
1856
|
+
// not be another participant's name: that would capture its messages.
|
|
1857
|
+
if (chosen) {
|
|
1858
|
+
const named = (await scanParticipantsBySlug(this, chosen)).match;
|
|
1859
|
+
const actsAs = input.participantId ?? existing?.participantId;
|
|
1860
|
+
if (named && named.participantId !== actsAs) {
|
|
1861
|
+
throw new CapabilityError(`The handle "${chosen}" is another participant's name.`, 'CONFLICT', {
|
|
1862
|
+
reason: 'instance-handle-names-participant',
|
|
1863
|
+
details: { handle: chosen, participantId: named.participantId },
|
|
1864
|
+
recoveryHint: 'Choose a handle that is not an existing participant\'s name.'
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
const kept = existing && SessionManager.isLiveInstance(existing, nowIso) ? existing.activeHandle ?? undefined : undefined;
|
|
1869
|
+
const base = input.agentId ?? input.harness ?? input.clientName;
|
|
1870
|
+
const fields = {
|
|
1871
|
+
participantId: input.participantId ?? existing?.participantId,
|
|
1872
|
+
clientName: input.clientName,
|
|
1873
|
+
clientVersion: input.clientVersion,
|
|
1874
|
+
hostname: input.hostname,
|
|
1875
|
+
platform: input.platform,
|
|
1876
|
+
workingDirectory: input.workingDirectory,
|
|
1877
|
+
transport: input.transport,
|
|
1878
|
+
agentId: input.agentId,
|
|
1879
|
+
harness: input.harness,
|
|
1880
|
+
harnessSessionId: input.harnessSessionId,
|
|
1881
|
+
lastHeartbeatAt: nowAt,
|
|
1882
|
+
expiresAt,
|
|
1883
|
+
disconnectedAt: null
|
|
1884
|
+
};
|
|
1885
|
+
for (let attempt = 0; attempt < SessionManager.INSTANCE_HANDLE_ATTEMPTS; attempt++) {
|
|
1886
|
+
const handle = chosen ?? (attempt === 0 && kept ? kept : SessionManager.randomHandle(base));
|
|
1887
|
+
if (handle !== existing?.activeHandle)
|
|
1888
|
+
await this.releaseExpiredHandle(handle, input.instanceId, nowIso);
|
|
1889
|
+
const row = { ...fields, handle, activeHandle: handle };
|
|
1890
|
+
const written = await settle(() => existing
|
|
1891
|
+
? storage.update(input.instanceId, row)
|
|
1892
|
+
: storage.create({ connectionId: input.instanceId, connectedAt: nowAt, ...row }));
|
|
1893
|
+
if (written.ok) {
|
|
1894
|
+
if (input.hosted)
|
|
1895
|
+
this.hostInstance(input.instanceId);
|
|
1896
|
+
this.instanceTouchedAt.set(input.instanceId, now);
|
|
1897
|
+
return { ...existing, connectionId: input.instanceId, connectedAt: existing?.connectedAt ?? nowAt, ...row };
|
|
1898
|
+
}
|
|
1899
|
+
if (!isUniqueConstraintError(written.cause))
|
|
1900
|
+
throw written.cause;
|
|
1901
|
+
if (chosen) {
|
|
1902
|
+
const holder = await this.liveInstanceByHandle(chosen);
|
|
1903
|
+
throw new CapabilityError(`The handle "${chosen}" is held by another live instance.`, 'CONFLICT', {
|
|
1904
|
+
reason: 'instance-handle-taken',
|
|
1905
|
+
details: { handle: chosen, holder: holder?.connectionId ?? null, agentId: holder?.agentId ?? null },
|
|
1906
|
+
recoveryHint: 'Choose another handle, or omit it to be given a random one.'
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
throw new CapabilityError('Could not allocate a unique instance handle.', 'UNAVAILABLE', {
|
|
1911
|
+
reason: 'instance-handle-exhausted'
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
/** Free `handle` when its holder is no longer live, so a new instance can take it. */
|
|
1915
|
+
async releaseExpiredHandle(handle, claimant, nowIso) {
|
|
1916
|
+
const held = await this.connectionsStorage.list({
|
|
1917
|
+
where: { activeHandle: handle },
|
|
1918
|
+
limit: 1
|
|
1919
|
+
});
|
|
1920
|
+
const holder = held.items[0];
|
|
1921
|
+
if (!holder || holder.connectionId === claimant)
|
|
1922
|
+
return;
|
|
1923
|
+
if (SessionManager.isLiveInstance(holder, nowIso))
|
|
1924
|
+
return;
|
|
1925
|
+
await this.connectionsStorage.update(holder.connectionId, { activeHandle: null });
|
|
1926
|
+
}
|
|
1927
|
+
/** The live instance holding `handle`, if any. */
|
|
1928
|
+
async liveInstanceByHandle(handle) {
|
|
1929
|
+
await this.ensureStorage();
|
|
1930
|
+
const held = await this.connectionsStorage.list({
|
|
1931
|
+
where: { activeHandle: sanitizeSlugCandidate(handle) },
|
|
1932
|
+
limit: 1
|
|
1933
|
+
});
|
|
1934
|
+
const holder = held.items[0];
|
|
1935
|
+
return holder && SessionManager.isLiveInstance(holder, new Date().toISOString()) ? holder : null;
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Mark an instance alive now and renew the membership leases it holds.
|
|
1939
|
+
* Writes at most once per touch window per instance.
|
|
1940
|
+
*/
|
|
1941
|
+
async touchInstance(instanceId, options = {}) {
|
|
1942
|
+
const now = Date.now();
|
|
1943
|
+
const last = this.instanceTouchedAt.get(instanceId);
|
|
1944
|
+
if (!options.force && last !== undefined && now - last < SessionManager.INSTANCE_TOUCH_MIN_MS)
|
|
1945
|
+
return true;
|
|
1946
|
+
await this.ensureStorage();
|
|
1947
|
+
const nowAt = new Date(now);
|
|
1948
|
+
const expiresAt = new Date(now + SessionManager.INSTANCE_TTL_MS);
|
|
1949
|
+
const row = await this.connectionsStorage.get(instanceId);
|
|
1950
|
+
if (!row || row.disconnectedAt) {
|
|
1951
|
+
this.forgetInstance(instanceId);
|
|
1952
|
+
return false;
|
|
1953
|
+
}
|
|
1954
|
+
await this.connectionsStorage.update(instanceId, { lastHeartbeatAt: nowAt, expiresAt });
|
|
1955
|
+
this.instanceTouchedAt.set(instanceId, now);
|
|
1956
|
+
for (const lease of await this.unreleasedLeasesOf(instanceId)) {
|
|
1957
|
+
await this.membershipLeasesStorage.update(lease.id, {
|
|
1958
|
+
lastHeartbeatAt: nowAt.toISOString(),
|
|
1959
|
+
expiresAt: expiresAt.toISOString()
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
return true;
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Keep an admitted caller's instance live: register it the first time this
|
|
1966
|
+
* host sees it (or after it was closed), otherwise touch it.
|
|
1967
|
+
*/
|
|
1968
|
+
async ensureInstance(input) {
|
|
1969
|
+
const known = this.instanceHandles.get(input.instanceId);
|
|
1970
|
+
if (known !== undefined && (await this.touchInstance(input.instanceId)))
|
|
1971
|
+
return { handle: known };
|
|
1972
|
+
const row = await this.registerInstance(input);
|
|
1973
|
+
this.instanceHandles.set(input.instanceId, row.activeHandle ?? row.handle ?? '');
|
|
1974
|
+
return { handle: row.activeHandle ?? row.handle ?? undefined };
|
|
1975
|
+
}
|
|
1976
|
+
/**
|
|
1977
|
+
* Make an instance a member of the session it calls with: one lease keyed
|
|
1978
|
+
* on the instance, taken the first time this host sees the pair and renewed
|
|
1979
|
+
* by every touch after. An archived or missing session takes no member.
|
|
1980
|
+
*/
|
|
1981
|
+
async ensureInstanceMembership(input) {
|
|
1982
|
+
const key = `${input.instanceId}|${input.sessionId}|${input.participantId}`;
|
|
1983
|
+
if (this.instanceMemberships.has(key))
|
|
1984
|
+
return;
|
|
1985
|
+
const session = await this.getSession(input.sessionId);
|
|
1986
|
+
if (!session || session.status === 'archived')
|
|
1987
|
+
return;
|
|
1988
|
+
await this.acquireMembershipLease({
|
|
1989
|
+
sessionId: input.sessionId,
|
|
1990
|
+
participantId: input.participantId,
|
|
1991
|
+
connectionKey: input.instanceId
|
|
1992
|
+
});
|
|
1993
|
+
this.instanceMemberships.add(key);
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Give a live instance the handle it chose. The registry's uniqueness rule
|
|
1997
|
+
* applies: a handle another live instance holds is refused naming its holder.
|
|
1998
|
+
*/
|
|
1999
|
+
async chooseInstanceHandle(instanceId, handle) {
|
|
2000
|
+
await this.ensureStorage();
|
|
2001
|
+
const row = await this.connectionsStorage.get(instanceId);
|
|
2002
|
+
if (!row) {
|
|
2003
|
+
throw new CapabilityError('This caller has no registered instance to name.', 'NOT_FOUND', {
|
|
2004
|
+
reason: 'instance-unregistered',
|
|
2005
|
+
details: { instanceId }
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
const named = await this.registerInstance({
|
|
2009
|
+
instanceId,
|
|
2010
|
+
handle,
|
|
2011
|
+
transport: row.transport ?? 'unknown',
|
|
2012
|
+
clientName: row.clientName,
|
|
2013
|
+
clientVersion: row.clientVersion,
|
|
2014
|
+
participantId: row.participantId,
|
|
2015
|
+
agentId: row.agentId,
|
|
2016
|
+
harness: row.harness,
|
|
2017
|
+
harnessSessionId: row.harnessSessionId,
|
|
2018
|
+
hosted: this.hostedInstances.has(instanceId)
|
|
2019
|
+
});
|
|
2020
|
+
this.instanceHandles.set(instanceId, named.activeHandle ?? named.handle ?? '');
|
|
2021
|
+
return named;
|
|
2022
|
+
}
|
|
2023
|
+
/** One instance's registry row. */
|
|
2024
|
+
async getInstance(instanceId) {
|
|
2025
|
+
await this.ensureStorage();
|
|
2026
|
+
return this.connectionsStorage.get(instanceId);
|
|
2027
|
+
}
|
|
2028
|
+
/** Record the session an instance is working in (its bind, or its first write). */
|
|
2029
|
+
async setInstanceSession(instanceId, sessionId) {
|
|
2030
|
+
await this.ensureStorage();
|
|
2031
|
+
const row = await this.connectionsStorage.get(instanceId);
|
|
2032
|
+
if (!row || row.sessionId === sessionId)
|
|
2033
|
+
return;
|
|
2034
|
+
await this.connectionsStorage.update(instanceId, { sessionId });
|
|
2035
|
+
}
|
|
2036
|
+
/** The session an instance is working in, while that session is still open. */
|
|
2037
|
+
async instanceSession(instanceId) {
|
|
2038
|
+
await this.ensureStorage();
|
|
2039
|
+
const sessionId = (await this.connectionsStorage.get(instanceId))?.sessionId;
|
|
2040
|
+
if (!sessionId)
|
|
2041
|
+
return undefined;
|
|
2042
|
+
const session = await this.getSession(sessionId);
|
|
2043
|
+
return session && session.status !== 'archived' ? session.id : undefined;
|
|
2044
|
+
}
|
|
2045
|
+
/** Close an instance: it stops being live, frees its handle and releases its leases. */
|
|
2046
|
+
async closeInstance(instanceId) {
|
|
2047
|
+
await this.ensureStorage();
|
|
2048
|
+
this.hostedInstances.delete(instanceId);
|
|
2049
|
+
this.forgetInstance(instanceId);
|
|
2050
|
+
const nowAt = new Date();
|
|
2051
|
+
const row = await this.connectionsStorage.get(instanceId);
|
|
2052
|
+
if (row && !row.disconnectedAt) {
|
|
2053
|
+
await this.connectionsStorage.update(instanceId, { disconnectedAt: nowAt, activeHandle: null });
|
|
2054
|
+
}
|
|
2055
|
+
for (const lease of await this.unreleasedLeasesOf(instanceId)) {
|
|
2056
|
+
await this.membershipLeasesStorage.update(lease.id, {
|
|
2057
|
+
releasedAt: nowAt.toISOString(),
|
|
2058
|
+
activeConnectionKey: null
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
if (this.hostedInstances.size === 0)
|
|
2062
|
+
this.stopInstanceHeartbeat();
|
|
2063
|
+
}
|
|
2064
|
+
/** Every live instance in this store: who is here, however they arrived. */
|
|
2065
|
+
async listLiveInstances() {
|
|
2066
|
+
await this.ensureStorage();
|
|
2067
|
+
const nowAt = new Date();
|
|
2068
|
+
const nowIso = nowAt.toISOString();
|
|
2069
|
+
const live = [];
|
|
2070
|
+
const pageSize = 500;
|
|
2071
|
+
for (let offset = 0;; offset += pageSize) {
|
|
2072
|
+
const page = await this.connectionsStorage.list({
|
|
2073
|
+
where: { expiresAt: { gte: nowAt } },
|
|
2074
|
+
orderBy: { connectionId: 'asc' },
|
|
2075
|
+
limit: pageSize,
|
|
2076
|
+
offset
|
|
2077
|
+
});
|
|
2078
|
+
live.push(...page.items.filter((row) => SessionManager.isLiveInstance(row, nowIso)));
|
|
2079
|
+
if (page.items.length < pageSize)
|
|
2080
|
+
return live;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
forgetInstance(instanceId) {
|
|
2084
|
+
this.instanceTouchedAt.delete(instanceId);
|
|
2085
|
+
this.instanceHandles.delete(instanceId);
|
|
2086
|
+
for (const key of Array.from(this.instanceMemberships)) {
|
|
2087
|
+
if (key.startsWith(`${instanceId}|`))
|
|
2088
|
+
this.instanceMemberships.delete(key);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
async unreleasedLeasesOf(instanceId) {
|
|
2092
|
+
const result = await this.membershipLeasesStorage.list({
|
|
2093
|
+
where: {
|
|
2094
|
+
activeConnectionKey: instanceId,
|
|
2095
|
+
releasedAt: { isNull: true }
|
|
2096
|
+
},
|
|
2097
|
+
limit: 500
|
|
2098
|
+
});
|
|
2099
|
+
return result.items;
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* Heartbeat hosted instances on this runtime's lifecycle interval, so the
|
|
2103
|
+
* kernel stops it at disposal. A finite host (a one-shot command) runs no
|
|
2104
|
+
* interval: its instance lives by activity and closes with the manager.
|
|
2105
|
+
*/
|
|
2106
|
+
hostInstance(instanceId) {
|
|
2107
|
+
this.hostedInstances.add(instanceId);
|
|
2108
|
+
if (this.instanceHeartbeat)
|
|
2109
|
+
return;
|
|
2110
|
+
const runtime = this.hostRuntime;
|
|
2111
|
+
if (runtime.declaredScope.lifecycle === 'finite' || runtime.lifecycle.signal.aborted)
|
|
2112
|
+
return;
|
|
2113
|
+
this.instanceHeartbeat = runtime.lifecycle.interval(() => {
|
|
2114
|
+
for (const id of this.hostedInstances) {
|
|
2115
|
+
void settle(() => this.touchInstance(id, { force: true })).then((touched) => {
|
|
2116
|
+
if (touched.ok)
|
|
2117
|
+
return;
|
|
2118
|
+
this.logger.warn('Instance heartbeat failed', { instanceId: id, error: valueFreeFailure(touched.cause) });
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
}, SessionManager.INSTANCE_HEARTBEAT_MS);
|
|
2122
|
+
}
|
|
2123
|
+
stopInstanceHeartbeat() {
|
|
2124
|
+
const stop = this.instanceHeartbeat;
|
|
2125
|
+
this.instanceHeartbeat = undefined;
|
|
2126
|
+
stop?.();
|
|
2127
|
+
}
|
|
2128
|
+
async getConnectionHistory(participantId, limit) {
|
|
2129
|
+
await this.ensureStorage();
|
|
2130
|
+
const where = {};
|
|
2131
|
+
if (participantId)
|
|
2132
|
+
where.participantId = participantId;
|
|
2133
|
+
const result = await this.connectionsStorage.list({
|
|
2134
|
+
where,
|
|
2135
|
+
orderBy: { connectedAt: 'desc' },
|
|
2136
|
+
limit: limit ?? 100
|
|
2137
|
+
});
|
|
2138
|
+
return result.items;
|
|
2139
|
+
}
|
|
2140
|
+
// KNOWLEDGE
|
|
2141
|
+
async addKnowledgeRef(sessionId, ref) {
|
|
2142
|
+
await this.ensureStorage();
|
|
2143
|
+
return this.knowledgeRefsStorage.create({
|
|
2144
|
+
...ref,
|
|
2145
|
+
metadata: ref.metadata ?? {},
|
|
2146
|
+
sessionId
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
async getKnowledgeRef(id) {
|
|
2150
|
+
await this.ensureStorage();
|
|
2151
|
+
return this.knowledgeRefsStorage.get(id);
|
|
2152
|
+
}
|
|
2153
|
+
async getKnowledgeRefs(sessionId, filters) {
|
|
2154
|
+
await this.ensureStorage();
|
|
2155
|
+
const where = { sessionId };
|
|
2156
|
+
if (filters?.reason)
|
|
2157
|
+
where.reason = filters.reason;
|
|
2158
|
+
const result = await this.knowledgeRefsStorage.list({
|
|
2159
|
+
where,
|
|
2160
|
+
orderBy: { createdAt: 'desc' },
|
|
2161
|
+
limit: filters?.limit ?? 100,
|
|
2162
|
+
offset: filters?.offset ?? 0
|
|
2163
|
+
});
|
|
2164
|
+
return result.items;
|
|
2165
|
+
}
|
|
2166
|
+
async deleteKnowledgeRef(id) {
|
|
2167
|
+
await this.ensureStorage();
|
|
2168
|
+
await this.knowledgeRefsStorage.delete(id);
|
|
2169
|
+
}
|
|
2170
|
+
async deleteSessionKnowledgeRefs(sessionId) {
|
|
2171
|
+
await this.ensureStorage();
|
|
2172
|
+
// Get all knowledge refs for session
|
|
2173
|
+
const result = await this.knowledgeRefsStorage.list({
|
|
2174
|
+
where: { sessionId },
|
|
2175
|
+
limit: 1000
|
|
2176
|
+
});
|
|
2177
|
+
// Delete them
|
|
2178
|
+
for (const ref of result.items) {
|
|
2179
|
+
await this.knowledgeRefsStorage.delete(ref.id);
|
|
2180
|
+
}
|
|
2181
|
+
return result.items.length;
|
|
2182
|
+
}
|
|
2183
|
+
// SESSION STATE (PERSISTED PER PARTICIPANT)
|
|
2184
|
+
async setParticipantActiveSession(participantId, sessionId) {
|
|
2185
|
+
await this.ensureStorage();
|
|
2186
|
+
if (!participantId)
|
|
2187
|
+
return;
|
|
2188
|
+
const previous = this.participantSessions.get(participantId);
|
|
2189
|
+
this.participantSessions.set(participantId, sessionId);
|
|
2190
|
+
try {
|
|
2191
|
+
await this.persistRoutingRow(`${SESSION_STATE_PARTICIPANT_PREFIX}${participantId}`, {
|
|
2192
|
+
sessionId
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
catch (error) {
|
|
2196
|
+
if (previous)
|
|
2197
|
+
this.participantSessions.set(participantId, previous);
|
|
2198
|
+
else
|
|
2199
|
+
this.participantSessions.delete(participantId);
|
|
2200
|
+
throw error;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
async getParticipantActiveSession(participantId) {
|
|
2204
|
+
await this.ensureStorage();
|
|
2205
|
+
if (!participantId)
|
|
2206
|
+
return null;
|
|
2207
|
+
return this.participantSessions.get(participantId) ?? null;
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Read one participant binding from durable authority for a custody decision.
|
|
2211
|
+
*
|
|
2212
|
+
* The ordinary getter intentionally serves the manager's hydrated read model.
|
|
2213
|
+
* MCP authorization cannot use that cache: another process may have rebound
|
|
2214
|
+
* the participant since this manager started. Read the body-scoped row on
|
|
2215
|
+
* every call, reconcile only this cache entry, and let storage/parse failures
|
|
2216
|
+
* propagate so the caller fails closed.
|
|
2217
|
+
*/
|
|
2218
|
+
async getAuthoritativeParticipantActiveSession(participantId) {
|
|
2219
|
+
await this.ensureStorage();
|
|
2220
|
+
if (!participantId)
|
|
2221
|
+
return null;
|
|
2222
|
+
const routing = await this.readParticipantRouting(participantId);
|
|
2223
|
+
if (routing.state === 'malformed') {
|
|
2224
|
+
throw new CapabilityError(routing.problem === 'invalid-json'
|
|
2225
|
+
? `Participant ${participantId} routing row contains invalid JSON.`
|
|
2226
|
+
: `Participant ${participantId} routing row has no valid sessionId.`, 'INVALID_INPUT', { reason: 'routing-row-malformed', details: { participantId, problem: routing.problem } });
|
|
2227
|
+
}
|
|
2228
|
+
return routing.state === 'bound' ? routing.sessionId : null;
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Read one participant's body-scoped routing row from durable storage and
|
|
2232
|
+
* reconcile this manager's cache entry with it. A storage failure
|
|
2233
|
+
* propagates; a row that cannot be decoded is reported as `malformed`
|
|
2234
|
+
* (never as unbound) so each caller decides whether to fail closed.
|
|
2235
|
+
*/
|
|
2236
|
+
async readParticipantRouting(participantId) {
|
|
2237
|
+
await this.persistMappingsTail;
|
|
2238
|
+
const stateStorage = await this.resolveStorageRecord('session.state');
|
|
2239
|
+
const row = await stateStorage.get(`${SESSION_STATE_PARTICIPANT_PREFIX}${participantId}`);
|
|
2240
|
+
if (!row) {
|
|
2241
|
+
this.participantSessions.delete(participantId);
|
|
2242
|
+
return { state: 'unbound' };
|
|
2243
|
+
}
|
|
2244
|
+
const decoded = decodeJson(row.value);
|
|
2245
|
+
if (!decoded.ok)
|
|
2246
|
+
return { state: 'malformed', problem: 'invalid-json' };
|
|
2247
|
+
const parsed = RoutingValueSchema.safeParse(decoded.value);
|
|
2248
|
+
if (!parsed.success)
|
|
2249
|
+
return { state: 'malformed', problem: 'not-an-object' };
|
|
2250
|
+
if (parsed.data.sessionId === null) {
|
|
2251
|
+
this.participantSessions.delete(participantId);
|
|
2252
|
+
return { state: 'unbound' };
|
|
2253
|
+
}
|
|
2254
|
+
const sessionId = parsed.data.sessionId;
|
|
2255
|
+
if (!sessionId || sessionId.trim().length === 0)
|
|
2256
|
+
return { state: 'malformed', problem: 'no-session-id' };
|
|
2257
|
+
this.participantSessions.set(participantId, sessionId);
|
|
2258
|
+
return { state: 'bound', sessionId };
|
|
2259
|
+
}
|
|
2260
|
+
async clearParticipantActiveSession(participantId) {
|
|
2261
|
+
await this.ensureStorage();
|
|
2262
|
+
if (!participantId)
|
|
2263
|
+
return;
|
|
2264
|
+
const previous = this.participantSessions.get(participantId);
|
|
2265
|
+
if (!previous)
|
|
2266
|
+
return;
|
|
2267
|
+
this.participantSessions.delete(participantId);
|
|
2268
|
+
try {
|
|
2269
|
+
await this.deleteRoutingRow(`${SESSION_STATE_PARTICIPANT_PREFIX}${participantId}`);
|
|
2270
|
+
}
|
|
2271
|
+
catch (error) {
|
|
2272
|
+
this.participantSessions.set(participantId, previous);
|
|
2273
|
+
throw error;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
async ensureParticipantSession(participantId) {
|
|
2277
|
+
await this.ensureStorage();
|
|
2278
|
+
if (!participantId)
|
|
2279
|
+
return null;
|
|
2280
|
+
// Participant resolution is part of MCP custody stamping. A manager can
|
|
2281
|
+
// outlive another process rebinding this participant, so its hydrated map
|
|
2282
|
+
// is not authority here. Read the exact durable routing row before any
|
|
2283
|
+
// session-open side effect; failures must propagate rather than falling
|
|
2284
|
+
// through to a stale/default session.
|
|
2285
|
+
const routing = await this.readParticipantRouting(participantId);
|
|
2286
|
+
if (routing.state === 'malformed') {
|
|
2287
|
+
// A routing row that cannot even be decoded names nothing bindable;
|
|
2288
|
+
// every reconciliation would refuse on it forever. Tombstone it and
|
|
2289
|
+
// treat the body as unbound rather than wedged. A storage failure
|
|
2290
|
+
// (the read above, or this tombstone write) propagates instead.
|
|
2291
|
+
this.logger.warn('Participant routing row malformed; clearing it', {
|
|
2292
|
+
participantId,
|
|
2293
|
+
problem: routing.problem
|
|
2294
|
+
});
|
|
2295
|
+
await this.deleteRoutingRow(`${SESSION_STATE_PARTICIPANT_PREFIX}${participantId}`);
|
|
2296
|
+
this.participantSessions.delete(participantId);
|
|
2297
|
+
return null;
|
|
2298
|
+
}
|
|
2299
|
+
const sessionId = routing.state === 'bound' ? routing.sessionId : null;
|
|
2300
|
+
if (!sessionId) {
|
|
2301
|
+
return null;
|
|
2302
|
+
}
|
|
2303
|
+
// This resolver reconciles identity; it does not own participant routing.
|
|
2304
|
+
// Repersisting or clearing the row after the read creates a TOCTOU window
|
|
2305
|
+
// where another process' newer binding can be overwritten or tombstoned —
|
|
2306
|
+
// so the heal below re-reads the row and tombstones it only when it still
|
|
2307
|
+
// names the SAME dead session. The mcp: transport mapping already self-
|
|
2308
|
+
// heals this way in getSessionForMcpConnection; a participant row whose
|
|
2309
|
+
// session row is gone used to be the one mapping that could never shed it,
|
|
2310
|
+
// wedging every host reconciliation (and the relay's bind replay) behind a
|
|
2311
|
+
// fail-closed error until storage was hand-edited.
|
|
2312
|
+
const session = await this.getSession(sessionId);
|
|
2313
|
+
if (!session) {
|
|
2314
|
+
// The mapped session row is GONE — not archived (that stays fail-closed
|
|
2315
|
+
// below, because reopening is an explicit lifecycle action) but deleted
|
|
2316
|
+
// out from under the mapping, possibly by another process. Heal it.
|
|
2317
|
+
const stillStale = await this.getAuthoritativeParticipantActiveSession(participantId);
|
|
2318
|
+
if (stillStale === sessionId) {
|
|
2319
|
+
await this.deleteRoutingRow(`${SESSION_STATE_PARTICIPANT_PREFIX}${participantId}`);
|
|
2320
|
+
this.participantSessions.delete(participantId);
|
|
2321
|
+
this.logger.warn('Cleared orphaned participant→session mapping (session row missing)', {
|
|
2322
|
+
participantId,
|
|
2323
|
+
sessionId
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
return null;
|
|
2327
|
+
}
|
|
2328
|
+
if (session.status === 'archived')
|
|
2329
|
+
return null;
|
|
2330
|
+
if (session.status !== 'paused')
|
|
2331
|
+
return session;
|
|
2332
|
+
// Preserve the historical "open paused work" behavior without calling
|
|
2333
|
+
// resumeSession(), whose broader default/participant writes are forbidden
|
|
2334
|
+
// on this custody-resolution path.
|
|
2335
|
+
await this.updateSession(sessionId, { status: 'active' });
|
|
2336
|
+
return this.getSession(sessionId);
|
|
2337
|
+
}
|
|
2338
|
+
async setDefaultActiveSession(sessionId) {
|
|
2339
|
+
await this.ensureStorage();
|
|
2340
|
+
this.defaultSessionId = sessionId;
|
|
2341
|
+
if (!sessionId ||
|
|
2342
|
+
(this.defaultParticipantId && this.participantSessions.get(this.defaultParticipantId) !== sessionId)) {
|
|
2343
|
+
this.defaultParticipantId = null;
|
|
2344
|
+
}
|
|
2345
|
+
await this.persistRoutingRow(SESSION_STATE_DEFAULT_KEY, {
|
|
2346
|
+
sessionId,
|
|
2347
|
+
participantId: this.defaultParticipantId
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
async getDefaultActiveSession() {
|
|
2351
|
+
await this.ensureStorage();
|
|
2352
|
+
return this.defaultSessionId;
|
|
2353
|
+
}
|
|
2354
|
+
/** Persist the default session together with the CLI participant that selected it. */
|
|
2355
|
+
async setDefaultActiveBinding(sessionId, participantId) {
|
|
2356
|
+
await this.ensureStorage();
|
|
2357
|
+
this.defaultSessionId = sessionId;
|
|
2358
|
+
this.defaultParticipantId = participantId;
|
|
2359
|
+
this.participantSessions.set(participantId, sessionId);
|
|
2360
|
+
// Participant binding is the authority. Publish the global/default pointer
|
|
2361
|
+
// only after that exact body row is durable.
|
|
2362
|
+
await this.persistRoutingRow(`${SESSION_STATE_PARTICIPANT_PREFIX}${participantId}`, {
|
|
2363
|
+
sessionId
|
|
2364
|
+
});
|
|
2365
|
+
await this.persistRoutingRow(SESSION_STATE_DEFAULT_KEY, { sessionId, participantId });
|
|
2366
|
+
}
|
|
2367
|
+
/** Resolve the durable CLI caller without falling back to a participant-null session. */
|
|
2368
|
+
async getDefaultActiveBinding() {
|
|
2369
|
+
await this.ensureStorage();
|
|
2370
|
+
if (!this.defaultSessionId || !this.defaultParticipantId)
|
|
2371
|
+
return null;
|
|
2372
|
+
if (this.participantSessions.get(this.defaultParticipantId) !== this.defaultSessionId)
|
|
2373
|
+
return null;
|
|
2374
|
+
return {
|
|
2375
|
+
sessionId: this.defaultSessionId,
|
|
2376
|
+
participantId: this.defaultParticipantId
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
// UTILITIES
|
|
2380
|
+
/**
|
|
2381
|
+
* Get or create active session for a participant
|
|
2382
|
+
*/
|
|
2383
|
+
async getOrCreateActiveSession(participantId, title) {
|
|
2384
|
+
await this.ensureStorage();
|
|
2385
|
+
// Check for existing active session
|
|
2386
|
+
const active = await this.getActiveSession();
|
|
2387
|
+
if (active)
|
|
2388
|
+
return active;
|
|
2389
|
+
// Create new session
|
|
2390
|
+
return this.createSession({
|
|
2391
|
+
title: title || `Session ${new Date().toISOString()}`,
|
|
2392
|
+
status: 'active',
|
|
2393
|
+
ownerParticipant: participantId
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
/**
|
|
2397
|
+
* Get session statistics.
|
|
2398
|
+
*
|
|
2399
|
+
* Every figure is a TRUE count — countRecords over the exact where-filters
|
|
2400
|
+
* the corresponding lists use. These totals previously came from capped
|
|
2401
|
+
* list pages (todos/scratchpad/messages at 10000 rows, jobs at the 100-row
|
|
2402
|
+
* default page) and silently froze at the ceiling on busy sessions.
|
|
2403
|
+
*/
|
|
2404
|
+
async getSessionStats(sessionId, participantId) {
|
|
2405
|
+
await this.ensureStorage();
|
|
2406
|
+
const session = await this.getSession(sessionId);
|
|
2407
|
+
if (!session)
|
|
2408
|
+
return null;
|
|
2409
|
+
// When the caller names a participant, scope the message view to THAT
|
|
2410
|
+
// seat's inbox so `status {participantId}` reports the seat's own mail —
|
|
2411
|
+
// not a session-wide count that no per-seat mark-read can ever drain.
|
|
2412
|
+
// No participant → preserve the original ambient session-wide view.
|
|
2413
|
+
const messageScope = participantId ? { toParticipant: participantId } : {};
|
|
2414
|
+
const [todosTotal, todosCompleted, todosActive,
|
|
2415
|
+
// blocked = the todo's OWN explicit status ('blocked'). A todo waiting
|
|
2416
|
+
// on incomplete dependencies is a different state, reported separately
|
|
2417
|
+
// as awaitingDependencies below.
|
|
2418
|
+
todosBlocked, todosPending, awaitingDependencies, scratchpadTotal, scratchpadByType, jobsTotal, jobsCompleted, jobsPending, jobsProcessing, jobsFailed, messagesTotal, messagesUnread] = await Promise.all([
|
|
2419
|
+
this.countTodos(sessionId),
|
|
2420
|
+
this.countTodos(sessionId, { status: 'completed' }),
|
|
2421
|
+
this.countTodos(sessionId, { status: 'in_progress' }),
|
|
2422
|
+
this.countTodos(sessionId, { status: 'blocked' }),
|
|
2423
|
+
this.countTodos(sessionId, { status: 'pending' }),
|
|
2424
|
+
this.countTodosAwaitingDependencies(sessionId),
|
|
2425
|
+
this.countScratchpad(sessionId),
|
|
2426
|
+
this.countScratchpadByType(sessionId),
|
|
2427
|
+
this.countJobRefs(sessionId),
|
|
2428
|
+
this.countJobRefs(sessionId, { status: 'completed' }),
|
|
2429
|
+
this.countJobRefs(sessionId, { status: 'pending' }),
|
|
2430
|
+
this.countJobRefs(sessionId, { status: 'processing' }),
|
|
2431
|
+
this.countJobRefs(sessionId, { status: 'failed' }),
|
|
2432
|
+
this.countMessages(sessionId, messageScope),
|
|
2433
|
+
this.countMessages(sessionId, { ...messageScope, isRead: false })
|
|
2434
|
+
]);
|
|
2435
|
+
return {
|
|
2436
|
+
session,
|
|
2437
|
+
todos: {
|
|
2438
|
+
total: todosTotal,
|
|
2439
|
+
completed: todosCompleted,
|
|
2440
|
+
active: todosActive,
|
|
2441
|
+
blocked: todosBlocked,
|
|
2442
|
+
awaitingDependencies,
|
|
2443
|
+
pending: todosPending
|
|
2444
|
+
},
|
|
2445
|
+
scratchpad: {
|
|
2446
|
+
total: scratchpadTotal,
|
|
2447
|
+
byType: scratchpadByType
|
|
2448
|
+
},
|
|
2449
|
+
jobs: {
|
|
2450
|
+
total: jobsTotal,
|
|
2451
|
+
completed: jobsCompleted,
|
|
2452
|
+
pending: jobsPending,
|
|
2453
|
+
processing: jobsProcessing,
|
|
2454
|
+
failed: jobsFailed
|
|
2455
|
+
},
|
|
2456
|
+
messages: {
|
|
2457
|
+
total: messagesTotal,
|
|
2458
|
+
unread: messagesUnread,
|
|
2459
|
+
// Label WHOSE mailbox these figures describe: seat-scoped when a
|
|
2460
|
+
// participant was named, session-wide otherwise. An unlabeled
|
|
2461
|
+
// ambient unread count reads as personal backlog when it is the
|
|
2462
|
+
// whole bus (ledger L-11).
|
|
2463
|
+
scope: participantId ? 'participant' : 'session',
|
|
2464
|
+
participantId
|
|
2465
|
+
}
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
/**
|
|
2469
|
+
* True counts per scratchpad content type. Enumerates the closed content
|
|
2470
|
+
* type set and counts each — never derived from a capped list page. Types
|
|
2471
|
+
* with zero entries are omitted, matching the historical byType shape.
|
|
2472
|
+
*/
|
|
2473
|
+
async countScratchpadByType(sessionId) {
|
|
2474
|
+
const byType = {};
|
|
2475
|
+
await Promise.all(ScratchpadContentTypeSchema.options.map(async (contentType) => {
|
|
2476
|
+
const count = await this.countScratchpad(sessionId, { contentType });
|
|
2477
|
+
if (count > 0)
|
|
2478
|
+
byType[contentType] = count;
|
|
2479
|
+
}));
|
|
2480
|
+
return byType;
|
|
2481
|
+
}
|
|
2482
|
+
/**
|
|
2483
|
+
* Count non-terminal todos waiting on at least one incomplete dependency.
|
|
2484
|
+
* Exhaustive paged scan — no row ceiling. Distinct from the `blocked`
|
|
2485
|
+
* bucket, which is the todo's own explicit status; a completed or
|
|
2486
|
+
* cancelled todo is not awaiting anything, so terminal rows never count.
|
|
2487
|
+
*/
|
|
2488
|
+
async countTodosAwaitingDependencies(sessionId) {
|
|
2489
|
+
const blocked = await this.scanTodosByDependency(sessionId, { isBlocked: true });
|
|
2490
|
+
return blocked.filter((todo) => todo.status !== 'completed' && todo.status !== 'cancelled').length;
|
|
2491
|
+
}
|
|
2492
|
+
// PRIVATE
|
|
2493
|
+
ensureInitialized() {
|
|
2494
|
+
if (!this.initialized) {
|
|
2495
|
+
throw new Error('SessionManager not initialized. Call initialize() first.');
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
async loadSessionMappings() {
|
|
2499
|
+
this.participantSessions.clear();
|
|
2500
|
+
this.mcpSessions.clear();
|
|
2501
|
+
this.defaultSessionId = null;
|
|
2502
|
+
this.defaultParticipantId = null;
|
|
2503
|
+
const stateStorage = await this.resolveStorageRecord('session.state');
|
|
2504
|
+
const items = [];
|
|
2505
|
+
const pageSize = 1000;
|
|
2506
|
+
let offset = 0;
|
|
2507
|
+
for (;;) {
|
|
2508
|
+
const page = await stateStorage.list({
|
|
2509
|
+
includeTotal: true,
|
|
2510
|
+
limit: pageSize,
|
|
2511
|
+
offset,
|
|
2512
|
+
orderBy: { key: 'asc' }
|
|
2513
|
+
});
|
|
2514
|
+
items.push(...page.items);
|
|
2515
|
+
if (page.items.length === 0) {
|
|
2516
|
+
if (page.hasMore === true || (page.total !== undefined && offset < page.total)) {
|
|
2517
|
+
throw new CapabilityError('session.state pagination made no progress while rows remain.', 'INTERNAL', { reason: 'routing-pagination-stalled' });
|
|
2518
|
+
}
|
|
2519
|
+
break;
|
|
2520
|
+
}
|
|
2521
|
+
offset += page.items.length;
|
|
2522
|
+
const hasMore = page.hasMore ??
|
|
2523
|
+
(page.total !== undefined ? offset < page.total : page.items.length === pageSize);
|
|
2524
|
+
if (!hasMore)
|
|
2525
|
+
break;
|
|
2526
|
+
}
|
|
2527
|
+
const legacy = items.find((item) => item.key === SESSION_STATE_LEGACY_KEY);
|
|
2528
|
+
if (legacy) {
|
|
2529
|
+
const decoded = decodeJson(legacy.value);
|
|
2530
|
+
if (!decoded.ok) {
|
|
2531
|
+
throw new CapabilityError('Persisted session.state record contains invalid JSON.', 'INVALID_INPUT', {
|
|
2532
|
+
reason: 'session-state-malformed',
|
|
2533
|
+
cause: decoded.cause
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
this.applySessionState(LegacySessionStateSchema.parse(isJsonObject(decoded.value) ? decoded.value : {}));
|
|
2537
|
+
}
|
|
2538
|
+
// Body-scoped rows override the legacy aggregate. One participant write
|
|
2539
|
+
// cannot overwrite another participant simply because its manager is
|
|
2540
|
+
// stale. One undecodable row is skipped and reported; it never fails the
|
|
2541
|
+
// rest of the routing state.
|
|
2542
|
+
for (const item of items) {
|
|
2543
|
+
if (item.key === SESSION_STATE_LEGACY_KEY)
|
|
2544
|
+
continue;
|
|
2545
|
+
const decoded = decodeJson(item.value);
|
|
2546
|
+
if (!decoded.ok) {
|
|
2547
|
+
this.logger.warn('Skipping persisted session.state routing row with invalid JSON', {
|
|
2548
|
+
key: item.key,
|
|
2549
|
+
error: valueFreeFailure(decoded.cause)
|
|
2550
|
+
});
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
const parsed = RoutingValueSchema.safeParse(decoded.value);
|
|
2554
|
+
if (!parsed.success) {
|
|
2555
|
+
this.logger.warn('Skipping persisted session.state routing row that is not a JSON object', {
|
|
2556
|
+
key: item.key
|
|
2557
|
+
});
|
|
2558
|
+
continue;
|
|
2559
|
+
}
|
|
2560
|
+
const value = parsed.data;
|
|
2561
|
+
const sessionId = value.sessionId ?? null;
|
|
2562
|
+
if (item.key.startsWith(SESSION_STATE_PARTICIPANT_PREFIX)) {
|
|
2563
|
+
const participantId = item.key.slice(SESSION_STATE_PARTICIPANT_PREFIX.length);
|
|
2564
|
+
if (participantId && sessionId)
|
|
2565
|
+
this.participantSessions.set(participantId, sessionId);
|
|
2566
|
+
else if (participantId)
|
|
2567
|
+
this.participantSessions.delete(participantId);
|
|
2568
|
+
}
|
|
2569
|
+
else if (item.key.startsWith(SESSION_STATE_MCP_PREFIX)) {
|
|
2570
|
+
const mcpSessionId = item.key.slice(SESSION_STATE_MCP_PREFIX.length);
|
|
2571
|
+
if (mcpSessionId && sessionId)
|
|
2572
|
+
this.mcpSessions.set(mcpSessionId, sessionId);
|
|
2573
|
+
else if (mcpSessionId)
|
|
2574
|
+
this.mcpSessions.delete(mcpSessionId);
|
|
2575
|
+
}
|
|
2576
|
+
else if (item.key === SESSION_STATE_DEFAULT_KEY) {
|
|
2577
|
+
this.defaultSessionId = sessionId;
|
|
2578
|
+
this.defaultParticipantId = value.participantId ?? null;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
/**
|
|
2583
|
+
* Serialize one routing write behind every earlier one. Its failure is
|
|
2584
|
+
* returned to the caller that queued it (the returned promise rejects);
|
|
2585
|
+
* the tail only orders writes, so one failed write never blocks the next.
|
|
2586
|
+
*/
|
|
2587
|
+
queueRoutingMutation(operation) {
|
|
2588
|
+
const queued = this.persistMappingsTail.then(operation);
|
|
2589
|
+
this.persistMappingsTail = Promise.allSettled([queued]).then(() => undefined);
|
|
2590
|
+
return queued;
|
|
2591
|
+
}
|
|
2592
|
+
persistRoutingRow(key, value) {
|
|
2593
|
+
return this.queueRoutingMutation(async () => {
|
|
2594
|
+
const stateStorage = await this.resolveStorageRecord('session.state');
|
|
2595
|
+
const encoded = JSON.stringify(value);
|
|
2596
|
+
const existing = await stateStorage.get(key);
|
|
2597
|
+
if (existing) {
|
|
2598
|
+
await stateStorage.update(key, { value: encoded });
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2601
|
+
await writeOrRecover(() => stateStorage.create({ key, value: encoded }), () => stateStorage.update(key, { value: encoded }), isUniqueConstraintError);
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
deleteRoutingRow(key) {
|
|
2605
|
+
// A null tombstone wins over an old aggregate snapshot written by a stale
|
|
2606
|
+
// process. Physical deletion would let that legacy row resurrect custody.
|
|
2607
|
+
return this.persistRoutingRow(key, { sessionId: null });
|
|
2608
|
+
}
|
|
2609
|
+
async clearMappingsForSession(sessionId) {
|
|
2610
|
+
const participantIds = [];
|
|
2611
|
+
const mcpIds = [];
|
|
2612
|
+
for (const [participantId, mappedSessionId] of this.participantSessions.entries()) {
|
|
2613
|
+
if (mappedSessionId === sessionId) {
|
|
2614
|
+
this.participantSessions.delete(participantId);
|
|
2615
|
+
participantIds.push(participantId);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
for (const [mcpSessionId, mappedSessionId] of this.mcpSessions.entries()) {
|
|
2619
|
+
if (mappedSessionId === sessionId) {
|
|
2620
|
+
this.mcpSessions.delete(mcpSessionId);
|
|
2621
|
+
mcpIds.push(mcpSessionId);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
if (participantIds.length > 0 || mcpIds.length > 0) {
|
|
2625
|
+
await Promise.all([
|
|
2626
|
+
...participantIds.map((id) => this.deleteRoutingRow(`${SESSION_STATE_PARTICIPANT_PREFIX}${id}`)),
|
|
2627
|
+
...mcpIds.map((id) => this.deleteRoutingRow(`${SESSION_STATE_MCP_PREFIX}${id}`))
|
|
2628
|
+
]);
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
applySessionState(state) {
|
|
2632
|
+
this.participantSessions = new Map(state.participantSessions);
|
|
2633
|
+
this.mcpSessions = new Map(state.mcpSessions);
|
|
2634
|
+
this.defaultSessionId = state.defaultSessionId ?? null;
|
|
2635
|
+
this.defaultParticipantId = state.defaultParticipantId ?? null;
|
|
2636
|
+
}
|
|
2637
|
+
async recoverActiveSession() {
|
|
2638
|
+
await this.loadSessionMappings();
|
|
2639
|
+
if (!this.defaultSessionId)
|
|
2640
|
+
return;
|
|
2641
|
+
const existing = await this.getSession(this.defaultSessionId);
|
|
2642
|
+
if (!existing || existing.status !== 'active')
|
|
2643
|
+
await this.setDefaultActiveSession(null);
|
|
2644
|
+
}
|
|
2645
|
+
async ensureSessionActive(sessionId) {
|
|
2646
|
+
await this.ensureStorage();
|
|
2647
|
+
const session = await this.getSession(sessionId);
|
|
2648
|
+
if (!session) {
|
|
2649
|
+
throw sessionNotFound(sessionId);
|
|
2650
|
+
}
|
|
2651
|
+
if (session.status !== 'active') {
|
|
2652
|
+
throw sessionNotWritable(sessionId, session.status);
|
|
2653
|
+
}
|
|
2654
|
+
return session;
|
|
2655
|
+
}
|
|
2656
|
+
// MCP SESSION MAPPING
|
|
2657
|
+
/**
|
|
2658
|
+
* Link MCP HTTP session to Vibes session
|
|
2659
|
+
*
|
|
2660
|
+
* MCP HTTP transport maintains connection-based sessions.
|
|
2661
|
+
* This maps those to active Vibes sessions for tool execution context.
|
|
2662
|
+
*
|
|
2663
|
+
* @param mcpSessionId - MCP HTTP session identifier
|
|
2664
|
+
* @param vibesSessionId - Vibes session UUID
|
|
2665
|
+
*/
|
|
2666
|
+
async linkMcpSession(mcpSessionId, vibesSessionId) {
|
|
2667
|
+
await this.ensureStorage();
|
|
2668
|
+
const session = await this.getSession(vibesSessionId);
|
|
2669
|
+
if (!session || session.status !== 'active') {
|
|
2670
|
+
throw new CapabilityError(`Cannot bind MCP connection to inactive session ${vibesSessionId}`, session ? 'CONFLICT' : 'NOT_FOUND', { reason: session ? 'session-not-writable' : 'session-not-found', details: { sessionId: vibesSessionId } });
|
|
2671
|
+
}
|
|
2672
|
+
this.mcpSessions.set(mcpSessionId, vibesSessionId);
|
|
2673
|
+
await this.persistRoutingRow(`${SESSION_STATE_MCP_PREFIX}${mcpSessionId}`, {
|
|
2674
|
+
sessionId: vibesSessionId
|
|
2675
|
+
});
|
|
2676
|
+
}
|
|
2677
|
+
/**
|
|
2678
|
+
* Get Vibes session for MCP connection
|
|
2679
|
+
*
|
|
2680
|
+
* @param mcpSessionId - MCP HTTP session identifier
|
|
2681
|
+
* @returns Session if mapped and exists, null otherwise
|
|
2682
|
+
*/
|
|
2683
|
+
async getSessionForMcpConnection(mcpSessionId) {
|
|
2684
|
+
await this.ensureStorage();
|
|
2685
|
+
const vibesSessionId = this.mcpSessions.get(mcpSessionId);
|
|
2686
|
+
if (!vibesSessionId) {
|
|
2687
|
+
return null;
|
|
2688
|
+
}
|
|
2689
|
+
const session = await this.getSession(vibesSessionId);
|
|
2690
|
+
if (!session || session.status === 'archived') {
|
|
2691
|
+
this.mcpSessions.delete(mcpSessionId);
|
|
2692
|
+
await this.deleteRoutingRow(`${SESSION_STATE_MCP_PREFIX}${mcpSessionId}`);
|
|
2693
|
+
return null;
|
|
2694
|
+
}
|
|
2695
|
+
return session;
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Unlink MCP HTTP session from Vibes session
|
|
2699
|
+
*
|
|
2700
|
+
* Called when MCP session closes or Vibes session is archived.
|
|
2701
|
+
*
|
|
2702
|
+
* @param mcpSessionId - MCP HTTP session identifier
|
|
2703
|
+
*/
|
|
2704
|
+
async unlinkMcpSession(mcpSessionId) {
|
|
2705
|
+
await this.ensureStorage();
|
|
2706
|
+
if (this.mcpSessions.delete(mcpSessionId)) {
|
|
2707
|
+
await this.deleteRoutingRow(`${SESSION_STATE_MCP_PREFIX}${mcpSessionId}`);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
/**
|
|
2711
|
+
* Clear all MCP session mappings for a given Vibes session
|
|
2712
|
+
*
|
|
2713
|
+
* Called when a session is archived or deleted.
|
|
2714
|
+
*
|
|
2715
|
+
* @param vibesSessionId - Vibes session UUID
|
|
2716
|
+
*/
|
|
2717
|
+
async clearMcpSessionsForVibesSession(vibesSessionId) {
|
|
2718
|
+
await this.ensureStorage();
|
|
2719
|
+
const removed = [];
|
|
2720
|
+
for (const [mcpId, vibesId] of this.mcpSessions.entries()) {
|
|
2721
|
+
if (vibesId === vibesSessionId) {
|
|
2722
|
+
this.mcpSessions.delete(mcpId);
|
|
2723
|
+
removed.push(mcpId);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
await Promise.all(removed.map((mcpId) => this.deleteRoutingRow(`${SESSION_STATE_MCP_PREFIX}${mcpId}`)));
|
|
2727
|
+
}
|
|
2728
|
+
// PRESENCE TRACKING
|
|
2729
|
+
/**
|
|
2730
|
+
* Record presence for a connection to a session
|
|
2731
|
+
*/
|
|
2732
|
+
async recordPresence(input) {
|
|
2733
|
+
await this.ensureStorage();
|
|
2734
|
+
const now = new Date();
|
|
2735
|
+
const presence = await this.presenceStorage.create({
|
|
2736
|
+
contextType: 'session',
|
|
2737
|
+
contextId: input.sessionId,
|
|
2738
|
+
participantId: input.participantId,
|
|
2739
|
+
connectionId: input.connectionId,
|
|
2740
|
+
metadata: input.metadata,
|
|
2741
|
+
lastHeartbeat: now.toISOString(),
|
|
2742
|
+
status: 'active',
|
|
2743
|
+
connectedAt: now.toISOString()
|
|
2744
|
+
});
|
|
2745
|
+
if (input.participantId) {
|
|
2746
|
+
// Lease authority must not be swallowed: presence without a lease
|
|
2747
|
+
// creates a ghost connection absent from the membership roster.
|
|
2748
|
+
try {
|
|
2749
|
+
await this.acquireMembershipLease({
|
|
2750
|
+
sessionId: input.sessionId,
|
|
2751
|
+
participantId: input.participantId,
|
|
2752
|
+
connectionKey: input.connectionId,
|
|
2753
|
+
role: 'member',
|
|
2754
|
+
metadata: input.metadata
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
catch (error) {
|
|
2758
|
+
// Presence without a lease is a ghost roster entry — roll back. The
|
|
2759
|
+
// lease failure is what surfaces; a failed rollback is reported too.
|
|
2760
|
+
const [rollback] = await Promise.allSettled([this.presenceStorage.delete(presence.id)]);
|
|
2761
|
+
if (rollback.status === 'rejected') {
|
|
2762
|
+
this.logger.warn('Failed to roll back presence after lease failure', {
|
|
2763
|
+
presenceId: presence.id,
|
|
2764
|
+
error: valueFreeFailure(CaughtFailureSchema.parse(rollback.reason))
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
throw error;
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
return presence;
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* Update heartbeat for a connection
|
|
2774
|
+
*/
|
|
2775
|
+
async updateHeartbeat(connectionId) {
|
|
2776
|
+
await this.ensureStorage();
|
|
2777
|
+
const now = new Date();
|
|
2778
|
+
// Find presence by connectionId
|
|
2779
|
+
const result = await this.presenceStorage.list({
|
|
2780
|
+
where: { connectionId },
|
|
2781
|
+
limit: 1
|
|
2782
|
+
});
|
|
2783
|
+
const presence = result.items[0];
|
|
2784
|
+
if (!presence)
|
|
2785
|
+
return;
|
|
2786
|
+
await this.presenceStorage.update(presence.id, {
|
|
2787
|
+
lastHeartbeat: now.toISOString()
|
|
2788
|
+
});
|
|
2789
|
+
// Renew the membership lease on the same cadence as presence heartbeat
|
|
2790
|
+
// so the roster cannot expire while the connection is still live.
|
|
2791
|
+
if (presence.participantId) {
|
|
2792
|
+
await this.acquireMembershipLease({
|
|
2793
|
+
sessionId: presence.contextId,
|
|
2794
|
+
participantId: presence.participantId,
|
|
2795
|
+
connectionKey: presence.connectionId ?? connectionId
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
/**
|
|
2800
|
+
* Get active presence records for a session
|
|
2801
|
+
*/
|
|
2802
|
+
async getActivePresence(sessionId) {
|
|
2803
|
+
await this.ensureStorage();
|
|
2804
|
+
// Get recent presence records (last 5 minutes)
|
|
2805
|
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
|
2806
|
+
const result = await this.presenceStorage.list({
|
|
2807
|
+
where: { contextId: sessionId, status: 'active' },
|
|
2808
|
+
limit: 100
|
|
2809
|
+
});
|
|
2810
|
+
// Filter by heartbeat
|
|
2811
|
+
return result.items.filter((p) => p.lastHeartbeat >= fiveMinutesAgo);
|
|
2812
|
+
}
|
|
2813
|
+
/**
|
|
2814
|
+
* Get presence summary for a session
|
|
2815
|
+
*/
|
|
2816
|
+
async getPresenceSummary(sessionId) {
|
|
2817
|
+
await this.ensureStorage();
|
|
2818
|
+
const active = await this.listActiveMembershipLeases(sessionId);
|
|
2819
|
+
const asIso = (value) => value instanceof Date ? value.toISOString() : value;
|
|
2820
|
+
return {
|
|
2821
|
+
sessionId,
|
|
2822
|
+
activeCount: active.length,
|
|
2823
|
+
idleCount: 0,
|
|
2824
|
+
staleCount: 0,
|
|
2825
|
+
totalCount: active.length,
|
|
2826
|
+
isOccupied: active.length > 0,
|
|
2827
|
+
connections: active.map((lease) => ({
|
|
2828
|
+
id: lease.id,
|
|
2829
|
+
status: 'active',
|
|
2830
|
+
lastHeartbeat: asIso(lease.lastHeartbeatAt),
|
|
2831
|
+
connectionId: lease.connectionKey,
|
|
2832
|
+
participantId: lease.participantId
|
|
2833
|
+
}))
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
/**
|
|
2837
|
+
* Check if session is occupied (has active/idle connections)
|
|
2838
|
+
*/
|
|
2839
|
+
async isSessionOccupied(sessionId) {
|
|
2840
|
+
await this.ensureStorage();
|
|
2841
|
+
const active = await this.listActiveMembershipLeases(sessionId);
|
|
2842
|
+
return active.length > 0;
|
|
2843
|
+
}
|
|
2844
|
+
// MEMBERSHIP LEASES
|
|
2845
|
+
static MEMBERSHIP_LEASE_TTL_MS = 10 * 60 * 1000;
|
|
2846
|
+
normalizeConnectionKey(connectionKey) {
|
|
2847
|
+
const key = connectionKey?.trim() ?? '';
|
|
2848
|
+
return key.length > 0 ? key : 'local';
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Acquire or renew a membership lease. Idempotent on
|
|
2852
|
+
* (sessionId, participantId, connectionKey).
|
|
2853
|
+
*/
|
|
2854
|
+
async acquireMembershipLease(input) {
|
|
2855
|
+
await this.ensureStorage();
|
|
2856
|
+
const connectionKey = this.normalizeConnectionKey(input.connectionKey);
|
|
2857
|
+
const now = Date.now();
|
|
2858
|
+
const ttl = Math.max(30_000, input.ttlMs ?? SessionManager.MEMBERSHIP_LEASE_TTL_MS);
|
|
2859
|
+
const nowIso = new Date(now).toISOString();
|
|
2860
|
+
const expiresAt = new Date(now + ttl).toISOString();
|
|
2861
|
+
const existing = await this.membershipLeasesStorage.list({
|
|
2862
|
+
where: {
|
|
2863
|
+
sessionId: input.sessionId,
|
|
2864
|
+
participantId: input.participantId,
|
|
2865
|
+
activeConnectionKey: connectionKey
|
|
2866
|
+
},
|
|
2867
|
+
limit: 1
|
|
2868
|
+
});
|
|
2869
|
+
const row = existing.items[0];
|
|
2870
|
+
if (row) {
|
|
2871
|
+
return this.membershipLeasesStorage.update(row.id, {
|
|
2872
|
+
role: input.role ?? row.role ?? 'member',
|
|
2873
|
+
lastHeartbeatAt: nowIso,
|
|
2874
|
+
expiresAt,
|
|
2875
|
+
releasedAt: null,
|
|
2876
|
+
activeConnectionKey: connectionKey,
|
|
2877
|
+
claimedTodoIds: input.claimedTodoIds,
|
|
2878
|
+
metadata: input.metadata ? { ...row.metadata, ...input.metadata } : undefined
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
return writeOrRecover(() => this.membershipLeasesStorage.create({
|
|
2882
|
+
sessionId: input.sessionId,
|
|
2883
|
+
participantId: input.participantId,
|
|
2884
|
+
connectionKey,
|
|
2885
|
+
activeConnectionKey: connectionKey,
|
|
2886
|
+
role: input.role ?? 'member',
|
|
2887
|
+
joinedAt: nowIso,
|
|
2888
|
+
lastHeartbeatAt: nowIso,
|
|
2889
|
+
expiresAt,
|
|
2890
|
+
releasedAt: null,
|
|
2891
|
+
claimedTodoIds: input.claimedTodoIds ?? [],
|
|
2892
|
+
metadata: input.metadata ?? {}
|
|
2893
|
+
}), async () => {
|
|
2894
|
+
const raced = await this.membershipLeasesStorage.list({
|
|
2895
|
+
where: {
|
|
2896
|
+
sessionId: input.sessionId,
|
|
2897
|
+
participantId: input.participantId,
|
|
2898
|
+
activeConnectionKey: connectionKey
|
|
2899
|
+
},
|
|
2900
|
+
limit: 1
|
|
2901
|
+
});
|
|
2902
|
+
const racedRow = raced.items[0];
|
|
2903
|
+
if (!racedRow)
|
|
2904
|
+
return null;
|
|
2905
|
+
return this.membershipLeasesStorage.update(racedRow.id, {
|
|
2906
|
+
lastHeartbeatAt: nowIso,
|
|
2907
|
+
expiresAt,
|
|
2908
|
+
releasedAt: null,
|
|
2909
|
+
activeConnectionKey: connectionKey
|
|
2910
|
+
});
|
|
2911
|
+
}, isUniqueConstraintError);
|
|
2912
|
+
}
|
|
2913
|
+
async releaseMembershipLease(input) {
|
|
2914
|
+
await this.ensureStorage();
|
|
2915
|
+
const connectionKey = this.normalizeConnectionKey(input.connectionKey);
|
|
2916
|
+
const existing = await this.membershipLeasesStorage.list({
|
|
2917
|
+
where: {
|
|
2918
|
+
sessionId: input.sessionId,
|
|
2919
|
+
participantId: input.participantId,
|
|
2920
|
+
activeConnectionKey: connectionKey
|
|
2921
|
+
},
|
|
2922
|
+
limit: 1
|
|
2923
|
+
});
|
|
2924
|
+
const row = existing.items[0];
|
|
2925
|
+
if (!row || row.releasedAt)
|
|
2926
|
+
return false;
|
|
2927
|
+
await this.membershipLeasesStorage.update(row.id, {
|
|
2928
|
+
releasedAt: new Date().toISOString(),
|
|
2929
|
+
activeConnectionKey: null
|
|
2930
|
+
});
|
|
2931
|
+
return true;
|
|
2932
|
+
}
|
|
2933
|
+
/** Live roster: unreleased leases whose expiry is still in the future. */
|
|
2934
|
+
async listActiveMembershipLeases(sessionId) {
|
|
2935
|
+
await this.ensureStorage();
|
|
2936
|
+
const nowIso = new Date().toISOString();
|
|
2937
|
+
const leases = [];
|
|
2938
|
+
const pageSize = 500;
|
|
2939
|
+
const where = {
|
|
2940
|
+
sessionId,
|
|
2941
|
+
releasedAt: { isNull: true }
|
|
2942
|
+
};
|
|
2943
|
+
for (let offset = 0;; offset += pageSize) {
|
|
2944
|
+
const result = await this.membershipLeasesStorage.list({
|
|
2945
|
+
where,
|
|
2946
|
+
limit: pageSize,
|
|
2947
|
+
offset
|
|
2948
|
+
});
|
|
2949
|
+
leases.push(...result.items);
|
|
2950
|
+
if (result.items.length < pageSize)
|
|
2951
|
+
break;
|
|
2952
|
+
}
|
|
2953
|
+
return leases
|
|
2954
|
+
.filter((lease) => !lease.releasedAt && String(lease.expiresAt) >= nowIso)
|
|
2955
|
+
.sort((a, b) => {
|
|
2956
|
+
const roleRank = (role) => (role === 'owner' ? 0 : 1);
|
|
2957
|
+
const byRole = roleRank(a.role) - roleRank(b.role);
|
|
2958
|
+
if (byRole !== 0)
|
|
2959
|
+
return byRole;
|
|
2960
|
+
return String(a.participantId).localeCompare(String(b.participantId));
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
/**
|
|
2964
|
+
* Remove presence record (on disconnect) and release its membership lease.
|
|
2965
|
+
*/
|
|
2966
|
+
async removePresence(connectionId) {
|
|
2967
|
+
await this.ensureStorage();
|
|
2968
|
+
// Find presence by connectionId
|
|
2969
|
+
const result = await this.presenceStorage.list({
|
|
2970
|
+
where: { connectionId },
|
|
2971
|
+
limit: 1
|
|
2972
|
+
});
|
|
2973
|
+
const presence = result.items[0];
|
|
2974
|
+
if (!presence)
|
|
2975
|
+
return;
|
|
2976
|
+
// Release before delete so a lease-authority failure cannot leave a
|
|
2977
|
+
// ghost roster entry after presence is gone.
|
|
2978
|
+
if (presence.participantId) {
|
|
2979
|
+
await this.releaseMembershipLease({
|
|
2980
|
+
sessionId: presence.contextId,
|
|
2981
|
+
participantId: presence.participantId,
|
|
2982
|
+
connectionKey: presence.connectionId ?? connectionId
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
await this.presenceStorage.delete(presence.id);
|
|
2986
|
+
}
|
|
2987
|
+
/**
|
|
2988
|
+
* Cleanup stale presence records and release any associated leases.
|
|
2989
|
+
*/
|
|
2990
|
+
async cleanupStalePresence(thresholdMs) {
|
|
2991
|
+
await this.ensureStorage();
|
|
2992
|
+
const threshold = new Date(Date.now() - (thresholdMs ?? 5 * 60 * 1000)).toISOString();
|
|
2993
|
+
// Get stale records
|
|
2994
|
+
const result = await this.presenceStorage.list({
|
|
2995
|
+
limit: 1000
|
|
2996
|
+
});
|
|
2997
|
+
const stale = result.items.filter((p) => p.lastHeartbeat < threshold);
|
|
2998
|
+
// Delete them (and release leases — same contract as removePresence)
|
|
2999
|
+
for (const presence of stale) {
|
|
3000
|
+
if (presence.participantId) {
|
|
3001
|
+
await this.releaseMembershipLease({
|
|
3002
|
+
sessionId: presence.contextId,
|
|
3003
|
+
participantId: presence.participantId,
|
|
3004
|
+
connectionKey: presence.connectionId ?? undefined
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
await this.presenceStorage.delete(presence.id);
|
|
3008
|
+
}
|
|
3009
|
+
return stale.length;
|
|
3010
|
+
}
|
|
3011
|
+
/**
|
|
3012
|
+
* Resolve session with occupation awareness
|
|
3013
|
+
* Returns the first unoccupied session from candidates, or null if all occupied
|
|
3014
|
+
*/
|
|
3015
|
+
async resolveUnoccupiedSession(candidateIds) {
|
|
3016
|
+
await this.ensureStorage();
|
|
3017
|
+
for (const sessionId of candidateIds) {
|
|
3018
|
+
const session = await this.getSession(sessionId);
|
|
3019
|
+
if (!session || session.status === 'archived')
|
|
3020
|
+
continue;
|
|
3021
|
+
const occupied = await this.isSessionOccupied(sessionId);
|
|
3022
|
+
if (!occupied) {
|
|
3023
|
+
return session;
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
return null;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
//# sourceMappingURL=manager.js.map
|