@roj-ai/sdk 0.1.36 → 0.2.1
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/dist/bootstrap.d.ts +420 -26
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +55 -24
- package/dist/bootstrap.js.map +1 -1
- package/dist/bun-platform/fs.d.ts.map +1 -1
- package/dist/bun-platform/fs.js +1 -0
- package/dist/bun-platform/fs.js.map +1 -1
- package/dist/bun-platform/index.d.ts +4 -1
- package/dist/bun-platform/index.d.ts.map +1 -1
- package/dist/bun-platform/index.js +7 -1
- package/dist/bun-platform/index.js.map +1 -1
- package/dist/bun-platform/server-lifecycle.d.ts +30 -0
- package/dist/bun-platform/server-lifecycle.d.ts.map +1 -0
- package/dist/bun-platform/server-lifecycle.js +182 -0
- package/dist/bun-platform/server-lifecycle.js.map +1 -0
- package/dist/bun-platform/shell.d.ts +33 -0
- package/dist/bun-platform/shell.d.ts.map +1 -0
- package/dist/bun-platform/shell.js +322 -0
- package/dist/bun-platform/shell.js.map +1 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +97 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agents/agent.d.ts +42 -7
- package/dist/core/agents/agent.d.ts.map +1 -1
- package/dist/core/agents/agent.js +202 -87
- package/dist/core/agents/agent.js.map +1 -1
- package/dist/core/agents/config.d.ts +8 -1
- package/dist/core/agents/config.d.ts.map +1 -1
- package/dist/core/agents/config.js +5 -1
- package/dist/core/agents/config.js.map +1 -1
- package/dist/core/agents/retry.d.ts +1 -1
- package/dist/core/agents/retry.d.ts.map +1 -1
- package/dist/core/agents/retry.js +14 -16
- package/dist/core/agents/retry.js.map +1 -1
- package/dist/core/agents/state.d.ts +3 -1
- package/dist/core/agents/state.d.ts.map +1 -1
- package/dist/core/agents/state.js +1 -1
- package/dist/core/agents/state.js.map +1 -1
- package/dist/core/context/state.d.ts +3 -3
- package/dist/core/context/state.js +2 -2
- package/dist/core/events/base-event-store.d.ts.map +1 -1
- package/dist/core/events/base-event-store.js +22 -14
- package/dist/core/events/base-event-store.js.map +1 -1
- package/dist/core/events/file.d.ts +39 -1
- package/dist/core/events/file.d.ts.map +1 -1
- package/dist/core/events/file.js +159 -20
- package/dist/core/events/file.js.map +1 -1
- package/dist/core/events/types.d.ts +1 -1
- package/dist/core/file-store/containment.d.ts +19 -0
- package/dist/core/file-store/containment.d.ts.map +1 -0
- package/dist/core/file-store/containment.js +62 -0
- package/dist/core/file-store/containment.js.map +1 -0
- package/dist/core/file-store/file-store.d.ts +22 -2
- package/dist/core/file-store/file-store.d.ts.map +1 -1
- package/dist/core/file-store/file-store.js +88 -20
- package/dist/core/file-store/file-store.js.map +1 -1
- package/dist/core/file-store/types.d.ts +13 -1
- package/dist/core/file-store/types.d.ts.map +1 -1
- package/dist/core/image/image-processor.d.ts.map +1 -1
- package/dist/core/image/image-processor.js +3 -1
- package/dist/core/image/image-processor.js.map +1 -1
- package/dist/core/image/types.d.ts +2 -0
- package/dist/core/image/types.d.ts.map +1 -1
- package/dist/core/image/vips-resizer.d.ts.map +1 -1
- package/dist/core/image/vips-resizer.js +81 -33
- package/dist/core/image/vips-resizer.js.map +1 -1
- package/dist/core/llm/anthropic.d.ts +0 -1
- package/dist/core/llm/anthropic.d.ts.map +1 -1
- package/dist/core/llm/anthropic.js +58 -72
- package/dist/core/llm/anthropic.js.map +1 -1
- package/dist/core/llm/logger.d.ts +14 -2
- package/dist/core/llm/logger.d.ts.map +1 -1
- package/dist/core/llm/logger.js +161 -36
- package/dist/core/llm/logger.js.map +1 -1
- package/dist/core/llm/openrouter.d.ts +0 -1
- package/dist/core/llm/openrouter.d.ts.map +1 -1
- package/dist/core/llm/openrouter.js +53 -66
- package/dist/core/llm/openrouter.js.map +1 -1
- package/dist/core/llm/provider-request.d.ts +10 -0
- package/dist/core/llm/provider-request.d.ts.map +1 -0
- package/dist/core/llm/provider-request.js +41 -0
- package/dist/core/llm/provider-request.js.map +1 -0
- package/dist/core/llm/provider.d.ts +11 -0
- package/dist/core/llm/provider.d.ts.map +1 -1
- package/dist/core/llm/provider.js +26 -0
- package/dist/core/llm/provider.js.map +1 -1
- package/dist/core/llm/state.d.ts +2 -2
- package/dist/core/llm/state.js +4 -4
- package/dist/core/plugins/plugin-builder.d.ts +57 -3
- package/dist/core/plugins/plugin-builder.d.ts.map +1 -1
- package/dist/core/plugins/plugin-builder.js +68 -14
- package/dist/core/plugins/plugin-builder.js.map +1 -1
- package/dist/core/preset/preset-builder.d.ts +3 -2
- package/dist/core/preset/preset-builder.d.ts.map +1 -1
- package/dist/core/preset/preset-builder.js +7 -16
- package/dist/core/preset/preset-builder.js.map +1 -1
- package/dist/core/sessions/context.d.ts +23 -5
- package/dist/core/sessions/context.d.ts.map +1 -1
- package/dist/core/sessions/runtime-activity.d.ts +33 -0
- package/dist/core/sessions/runtime-activity.d.ts.map +1 -0
- package/dist/core/sessions/runtime-activity.js +55 -0
- package/dist/core/sessions/runtime-activity.js.map +1 -0
- package/dist/core/sessions/schema.d.ts +23 -2
- package/dist/core/sessions/schema.d.ts.map +1 -1
- package/dist/core/sessions/schema.js +24 -3
- package/dist/core/sessions/schema.js.map +1 -1
- package/dist/core/sessions/session-manager.d.ts +83 -6
- package/dist/core/sessions/session-manager.d.ts.map +1 -1
- package/dist/core/sessions/session-manager.js +517 -118
- package/dist/core/sessions/session-manager.js.map +1 -1
- package/dist/core/sessions/session-store.d.ts +18 -0
- package/dist/core/sessions/session-store.d.ts.map +1 -1
- package/dist/core/sessions/session-store.js +33 -0
- package/dist/core/sessions/session-store.js.map +1 -1
- package/dist/core/sessions/session.d.ts +42 -6
- package/dist/core/sessions/session.d.ts.map +1 -1
- package/dist/core/sessions/session.js +176 -44
- package/dist/core/sessions/session.js.map +1 -1
- package/dist/core/sessions/state.d.ts +10 -1
- package/dist/core/sessions/state.d.ts.map +1 -1
- package/dist/core/sessions/state.js +11 -1
- package/dist/core/sessions/state.js.map +1 -1
- package/dist/core/system.d.ts +9 -0
- package/dist/core/system.d.ts.map +1 -1
- package/dist/core/system.js +4 -1
- package/dist/core/system.js.map +1 -1
- package/dist/core/tools/executor.d.ts +1 -1
- package/dist/core/wake-key.d.ts +34 -0
- package/dist/core/wake-key.d.ts.map +1 -0
- package/dist/core/wake-key.js +51 -0
- package/dist/core/wake-key.js.map +1 -0
- package/dist/index.d.ts +23 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -8
- package/dist/index.js.map +1 -1
- package/dist/lib/archive/archive-inspection.d.ts +48 -0
- package/dist/lib/archive/archive-inspection.d.ts.map +1 -0
- package/dist/lib/archive/archive-inspection.fixtures.d.ts +7 -0
- package/dist/lib/archive/archive-inspection.fixtures.d.ts.map +1 -0
- package/dist/lib/archive/archive-inspection.fixtures.js +134 -0
- package/dist/lib/archive/archive-inspection.fixtures.js.map +1 -0
- package/dist/lib/archive/archive-inspection.js +253 -0
- package/dist/lib/archive/archive-inspection.js.map +1 -0
- package/dist/lib/archive/index.d.ts +3 -0
- package/dist/lib/archive/index.d.ts.map +1 -0
- package/dist/lib/archive/index.js +2 -0
- package/dist/lib/archive/index.js.map +1 -0
- package/dist/lib/logger/console.d.ts +5 -12
- package/dist/lib/logger/console.d.ts.map +1 -1
- package/dist/lib/logger/console.js +5 -47
- package/dist/lib/logger/console.js.map +1 -1
- package/dist/lib/logger/file.d.ts +8 -11
- package/dist/lib/logger/file.d.ts.map +1 -1
- package/dist/lib/logger/file.js +26 -37
- package/dist/lib/logger/file.js.map +1 -1
- package/dist/lib/logger/index.d.ts +3 -1
- package/dist/lib/logger/index.d.ts.map +1 -1
- package/dist/lib/logger/index.js +3 -1
- package/dist/lib/logger/index.js.map +1 -1
- package/dist/lib/logger/jsonl.d.ts +21 -0
- package/dist/lib/logger/jsonl.d.ts.map +1 -0
- package/dist/lib/logger/jsonl.js +29 -0
- package/dist/lib/logger/jsonl.js.map +1 -0
- package/dist/lib/logger/logger.d.ts +22 -0
- package/dist/lib/logger/logger.d.ts.map +1 -1
- package/dist/lib/logger/logger.js +37 -0
- package/dist/lib/logger/logger.js.map +1 -1
- package/dist/lib/logger/session-log.d.ts +21 -0
- package/dist/lib/logger/session-log.d.ts.map +1 -0
- package/dist/lib/logger/session-log.js +37 -0
- package/dist/lib/logger/session-log.js.map +1 -0
- package/dist/lib/utils/concurrency.d.ts +1 -1
- package/dist/lib/utils/concurrency.d.ts.map +1 -1
- package/dist/lib/utils/concurrency.js +30 -7
- package/dist/lib/utils/concurrency.js.map +1 -1
- package/dist/lib/utils/fs-batch.d.ts +17 -0
- package/dist/lib/utils/fs-batch.d.ts.map +1 -0
- package/dist/lib/utils/fs-batch.js +38 -0
- package/dist/lib/utils/fs-batch.js.map +1 -0
- package/dist/lib/utils/result.d.ts +7 -22
- package/dist/lib/utils/result.d.ts.map +1 -1
- package/dist/lib/utils/result.js +6 -17
- package/dist/lib/utils/result.js.map +1 -1
- package/dist/lib/utils/sleep.d.ts +10 -0
- package/dist/lib/utils/sleep.d.ts.map +1 -0
- package/dist/lib/utils/sleep.js +26 -0
- package/dist/lib/utils/sleep.js.map +1 -0
- package/dist/platform/fs-revision.d.ts +21 -0
- package/dist/platform/fs-revision.d.ts.map +1 -0
- package/dist/platform/fs-revision.js +9 -0
- package/dist/platform/fs-revision.js.map +1 -0
- package/dist/platform/fs.d.ts +104 -1
- package/dist/platform/fs.d.ts.map +1 -1
- package/dist/platform/git.d.ts +59 -0
- package/dist/platform/git.d.ts.map +1 -0
- package/dist/platform/git.js +8 -0
- package/dist/platform/git.js.map +1 -0
- package/dist/platform/index.d.ts +40 -1
- package/dist/platform/index.d.ts.map +1 -1
- package/dist/platform/index.js +1 -1
- package/dist/platform/index.js.map +1 -1
- package/dist/platform/llm-call-log.d.ts +80 -0
- package/dist/platform/llm-call-log.d.ts.map +1 -0
- package/dist/platform/llm-call-log.js +12 -0
- package/dist/platform/llm-call-log.js.map +1 -0
- package/dist/platform/scheduler.d.ts +32 -0
- package/dist/platform/scheduler.d.ts.map +1 -0
- package/dist/platform/scheduler.js +46 -0
- package/dist/platform/scheduler.js.map +1 -0
- package/dist/platform/session-log.d.ts +36 -0
- package/dist/platform/session-log.d.ts.map +1 -0
- package/dist/platform/session-log.js +12 -0
- package/dist/platform/session-log.js.map +1 -0
- package/dist/platform/shell.d.ts +83 -0
- package/dist/platform/shell.d.ts.map +1 -0
- package/dist/platform/shell.js +13 -0
- package/dist/platform/shell.js.map +1 -0
- package/dist/plugins/agent-status/plugin.d.ts.map +1 -1
- package/dist/plugins/agent-status/plugin.js +1 -0
- package/dist/plugins/agent-status/plugin.js.map +1 -1
- package/dist/plugins/agents/plugin.d.ts.map +1 -1
- package/dist/plugins/agents/plugin.js +129 -62
- package/dist/plugins/agents/plugin.js.map +1 -1
- package/dist/plugins/filesystem/listing.d.ts +3 -0
- package/dist/plugins/filesystem/listing.d.ts.map +1 -1
- package/dist/plugins/filesystem/listing.js +66 -10
- package/dist/plugins/filesystem/listing.js.map +1 -1
- package/dist/plugins/filesystem/plugin.d.ts.map +1 -1
- package/dist/plugins/filesystem/plugin.js +8 -6
- package/dist/plugins/filesystem/plugin.js.map +1 -1
- package/dist/plugins/git-status/plugin.d.ts +34 -7
- package/dist/plugins/git-status/plugin.d.ts.map +1 -1
- package/dist/plugins/git-status/plugin.js +168 -64
- package/dist/plugins/git-status/plugin.js.map +1 -1
- package/dist/plugins/llm-debug/plugin.d.ts.map +1 -1
- package/dist/plugins/llm-debug/plugin.js +1 -0
- package/dist/plugins/llm-debug/plugin.js.map +1 -1
- package/dist/plugins/logs/plugin.d.ts +1 -4
- package/dist/plugins/logs/plugin.d.ts.map +1 -1
- package/dist/plugins/logs/plugin.js +7 -0
- package/dist/plugins/logs/plugin.js.map +1 -1
- package/dist/plugins/mailbox/helpers.d.ts +2 -2
- package/dist/plugins/mailbox/helpers.js +1 -1
- package/dist/plugins/mailbox/plugin.d.ts.map +1 -1
- package/dist/plugins/mailbox/plugin.js +38 -43
- package/dist/plugins/mailbox/plugin.js.map +1 -1
- package/dist/plugins/mailbox/query.d.ts +7 -2
- package/dist/plugins/mailbox/query.d.ts.map +1 -1
- package/dist/plugins/mailbox/query.js +14 -7
- package/dist/plugins/mailbox/query.js.map +1 -1
- package/dist/plugins/mailbox/state.d.ts +5 -4
- package/dist/plugins/mailbox/state.d.ts.map +1 -1
- package/dist/plugins/mailbox/state.js +6 -5
- package/dist/plugins/mailbox/state.js.map +1 -1
- package/dist/plugins/resources/filename.d.ts +4 -0
- package/dist/plugins/resources/filename.d.ts.map +1 -0
- package/dist/plugins/resources/filename.js +18 -0
- package/dist/plugins/resources/filename.js.map +1 -0
- package/dist/plugins/resources/plugin.d.ts +3 -0
- package/dist/plugins/resources/plugin.d.ts.map +1 -1
- package/dist/plugins/resources/plugin.js +97 -60
- package/dist/plugins/resources/plugin.js.map +1 -1
- package/dist/plugins/resources/state.d.ts +1 -1
- package/dist/plugins/resources/state.js +1 -1
- package/dist/plugins/result-eviction/plugin.d.ts.map +1 -1
- package/dist/plugins/result-eviction/plugin.js +6 -1
- package/dist/plugins/result-eviction/plugin.js.map +1 -1
- package/dist/plugins/services/plugin.d.ts +4 -1
- package/dist/plugins/services/plugin.d.ts.map +1 -1
- package/dist/plugins/services/plugin.js +251 -100
- package/dist/plugins/services/plugin.js.map +1 -1
- package/dist/plugins/services/prompt.d.ts.map +1 -1
- package/dist/plugins/services/prompt.js +1 -4
- package/dist/plugins/services/prompt.js.map +1 -1
- package/dist/plugins/services/schema.d.ts +15 -5
- package/dist/plugins/services/schema.d.ts.map +1 -1
- package/dist/plugins/services/service.d.ts +100 -18
- package/dist/plugins/services/service.d.ts.map +1 -1
- package/dist/plugins/services/service.js +680 -230
- package/dist/plugins/services/service.js.map +1 -1
- package/dist/plugins/session-lifecycle/plugin.d.ts +21 -1
- package/dist/plugins/session-lifecycle/plugin.d.ts.map +1 -1
- package/dist/plugins/session-lifecycle/plugin.js +53 -40
- package/dist/plugins/session-lifecycle/plugin.js.map +1 -1
- package/dist/plugins/session-state/plugin.d.ts +21 -13
- package/dist/plugins/session-state/plugin.d.ts.map +1 -1
- package/dist/plugins/session-state/plugin.js +214 -79
- package/dist/plugins/session-state/plugin.js.map +1 -1
- package/dist/plugins/session-stats/plugin.d.ts.map +1 -1
- package/dist/plugins/session-stats/plugin.js +1 -0
- package/dist/plugins/session-stats/plugin.js.map +1 -1
- package/dist/plugins/shell/executor.d.ts +17 -15
- package/dist/plugins/shell/executor.d.ts.map +1 -1
- package/dist/plugins/shell/executor.js +130 -195
- package/dist/plugins/shell/executor.js.map +1 -1
- package/dist/plugins/shell/index.d.ts +2 -2
- package/dist/plugins/shell/index.d.ts.map +1 -1
- package/dist/plugins/shell/index.js +2 -2
- package/dist/plugins/shell/index.js.map +1 -1
- package/dist/plugins/shell/plugin.d.ts +5 -4
- package/dist/plugins/shell/plugin.d.ts.map +1 -1
- package/dist/plugins/shell/plugin.js +4 -8
- package/dist/plugins/shell/plugin.js.map +1 -1
- package/dist/plugins/skills/discovery.d.ts.map +1 -1
- package/dist/plugins/skills/discovery.js +10 -8
- package/dist/plugins/skills/discovery.js.map +1 -1
- package/dist/plugins/skills/index.d.ts +1 -1
- package/dist/plugins/skills/index.d.ts.map +1 -1
- package/dist/plugins/skills/plugin.d.ts +1 -1
- package/dist/plugins/skills/plugin.d.ts.map +1 -1
- package/dist/plugins/skills/plugin.js +23 -30
- package/dist/plugins/skills/plugin.js.map +1 -1
- package/dist/plugins/skills/prompts.d.ts +3 -3
- package/dist/plugins/skills/prompts.d.ts.map +1 -1
- package/dist/plugins/skills/prompts.js.map +1 -1
- package/dist/plugins/skills/schema.d.ts +4 -2
- package/dist/plugins/skills/schema.d.ts.map +1 -1
- package/dist/plugins/todo/schema.d.ts +1 -1
- package/dist/plugins/uploads/plugin.d.ts +11 -4
- package/dist/plugins/uploads/plugin.d.ts.map +1 -1
- package/dist/plugins/uploads/plugin.js +891 -225
- package/dist/plugins/uploads/plugin.js.map +1 -1
- package/dist/plugins/uploads/preprocessor.d.ts +14 -0
- package/dist/plugins/uploads/preprocessor.d.ts.map +1 -1
- package/dist/plugins/uploads/preprocessor.js +11 -0
- package/dist/plugins/uploads/preprocessor.js.map +1 -1
- package/dist/plugins/uploads/preprocessors/image-classifier.d.ts +1 -1
- package/dist/plugins/uploads/preprocessors/image-classifier.d.ts.map +1 -1
- package/dist/plugins/uploads/preprocessors/image-classifier.js +51 -26
- package/dist/plugins/uploads/preprocessors/image-classifier.js.map +1 -1
- package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.d.ts +2 -2
- package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.d.ts.map +1 -1
- package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.js +41 -16
- package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.js.map +1 -1
- package/dist/plugins/uploads/preprocessors/pdf-preprocessor.d.ts +1 -1
- package/dist/plugins/uploads/preprocessors/pdf-preprocessor.d.ts.map +1 -1
- package/dist/plugins/uploads/preprocessors/pdf-preprocessor.js +29 -14
- package/dist/plugins/uploads/preprocessors/pdf-preprocessor.js.map +1 -1
- package/dist/plugins/uploads/preprocessors/zip-preprocessor.d.ts +1 -1
- package/dist/plugins/uploads/preprocessors/zip-preprocessor.d.ts.map +1 -1
- package/dist/plugins/uploads/preprocessors/zip-preprocessor.js +62 -34
- package/dist/plugins/uploads/preprocessors/zip-preprocessor.js.map +1 -1
- package/dist/plugins/uploads/schema.d.ts +25 -2
- package/dist/plugins/uploads/schema.d.ts.map +1 -1
- package/dist/plugins/uploads/schema.js +24 -3
- package/dist/plugins/uploads/schema.js.map +1 -1
- package/dist/plugins/uploads/state.d.ts +13 -1
- package/dist/plugins/uploads/state.d.ts.map +1 -1
- package/dist/plugins/uploads/state.js +4 -1
- package/dist/plugins/uploads/state.js.map +1 -1
- package/dist/plugins/user-chat/plugin.d.ts +12 -5
- package/dist/plugins/user-chat/plugin.d.ts.map +1 -1
- package/dist/plugins/user-chat/plugin.js +141 -32
- package/dist/plugins/user-chat/plugin.js.map +1 -1
- package/dist/plugins/workers/context.d.ts +35 -13
- package/dist/plugins/workers/context.d.ts.map +1 -1
- package/dist/plugins/workers/context.js +82 -32
- package/dist/plugins/workers/context.js.map +1 -1
- package/dist/plugins/workers/definition.d.ts +7 -0
- package/dist/plugins/workers/definition.d.ts.map +1 -1
- package/dist/plugins/workers/definition.js.map +1 -1
- package/dist/plugins/workers/index.d.ts +4 -4
- package/dist/plugins/workers/index.d.ts.map +1 -1
- package/dist/plugins/workers/index.js +2 -2
- package/dist/plugins/workers/index.js.map +1 -1
- package/dist/plugins/workers/plugin.d.ts +10 -57
- package/dist/plugins/workers/plugin.d.ts.map +1 -1
- package/dist/plugins/workers/plugin.js +385 -123
- package/dist/plugins/workers/plugin.js.map +1 -1
- package/dist/plugins/workers/state.d.ts +69 -0
- package/dist/plugins/workers/state.d.ts.map +1 -0
- package/dist/plugins/workers/state.js +52 -0
- package/dist/plugins/workers/state.js.map +1 -0
- package/dist/plugins/workers/worker.d.ts +15 -4
- package/dist/plugins/workers/worker.d.ts.map +1 -1
- package/dist/testing/conformance.d.ts +106 -0
- package/dist/testing/conformance.d.ts.map +1 -0
- package/dist/testing/conformance.js +1463 -0
- package/dist/testing/conformance.js.map +1 -0
- package/dist/testing/node-platform.d.ts.map +1 -1
- package/dist/testing/node-platform.js +5 -0
- package/dist/testing/node-platform.js.map +1 -1
- package/dist/testing/test-harness.d.ts +11 -1
- package/dist/testing/test-harness.d.ts.map +1 -1
- package/dist/testing/test-harness.js +17 -37
- package/dist/testing/test-harness.js.map +1 -1
- package/dist/testing/wait-helpers.d.ts.map +1 -1
- package/dist/testing/wait-helpers.js +1 -3
- package/dist/testing/wait-helpers.js.map +1 -1
- package/dist/transport/adapter/client-adapter.d.ts +6 -0
- package/dist/transport/adapter/client-adapter.d.ts.map +1 -1
- package/dist/transport/adapter/client-adapter.js +50 -3
- package/dist/transport/adapter/client-adapter.js.map +1 -1
- package/dist/transport/adapter/heartbeat.d.ts +16 -0
- package/dist/transport/adapter/heartbeat.d.ts.map +1 -0
- package/dist/transport/adapter/heartbeat.js +41 -0
- package/dist/transport/adapter/heartbeat.js.map +1 -0
- package/dist/transport/adapter/server-adapter.d.ts.map +1 -1
- package/dist/transport/adapter/server-adapter.js +6 -0
- package/dist/transport/adapter/server-adapter.js.map +1 -1
- package/dist/transport/http/app.d.ts +3 -30
- package/dist/transport/http/app.d.ts.map +1 -1
- package/dist/transport/http/app.js +3 -7
- package/dist/transport/http/app.js.map +1 -1
- package/dist/transport/http/context.d.ts +40 -0
- package/dist/transport/http/context.d.ts.map +1 -0
- package/dist/transport/http/context.js +17 -0
- package/dist/transport/http/context.js.map +1 -0
- package/dist/transport/http/fetch-guard.d.ts +49 -0
- package/dist/transport/http/fetch-guard.d.ts.map +1 -0
- package/dist/transport/http/fetch-guard.js +312 -0
- package/dist/transport/http/fetch-guard.js.map +1 -0
- package/dist/transport/http/index.d.ts +1 -1
- package/dist/transport/http/index.d.ts.map +1 -1
- package/dist/transport/http/middleware/bearer-auth.d.ts +1 -1
- package/dist/transport/http/middleware/bearer-auth.d.ts.map +1 -1
- package/dist/transport/http/middleware/error-handler.d.ts +2 -2
- package/dist/transport/http/middleware/error-handler.d.ts.map +1 -1
- package/dist/transport/http/path-containment.d.ts +20 -0
- package/dist/transport/http/path-containment.d.ts.map +1 -0
- package/dist/transport/http/path-containment.js +31 -0
- package/dist/transport/http/path-containment.js.map +1 -0
- package/dist/transport/http/responses.d.ts +30 -0
- package/dist/transport/http/responses.d.ts.map +1 -0
- package/dist/transport/http/responses.js +5 -0
- package/dist/transport/http/responses.js.map +1 -0
- package/dist/transport/http/routes/files.d.ts +1 -1
- package/dist/transport/http/routes/files.d.ts.map +1 -1
- package/dist/transport/http/routes/files.js +54 -155
- package/dist/transport/http/routes/files.js.map +1 -1
- package/dist/transport/http/routes/resources.d.ts +1 -1
- package/dist/transport/http/routes/resources.d.ts.map +1 -1
- package/dist/transport/http/routes/resources.js +92 -57
- package/dist/transport/http/routes/resources.js.map +1 -1
- package/dist/transport/http/routes/rpc.d.ts +6 -1
- package/dist/transport/http/routes/rpc.d.ts.map +1 -1
- package/dist/transport/http/routes/rpc.js +51 -21
- package/dist/transport/http/routes/rpc.js.map +1 -1
- package/dist/transport/http/routes/upload.d.ts +1 -1
- package/dist/transport/http/routes/upload.d.ts.map +1 -1
- package/dist/transport/http/routes/upload.js +256 -169
- package/dist/transport/http/routes/upload.js.map +1 -1
- package/dist/user-config.d.ts +34 -7
- package/dist/user-config.d.ts.map +1 -1
- package/dist/user-config.js +71 -0
- package/dist/user-config.js.map +1 -1
- package/package.json +17 -7
- package/src/bootstrap.ts +119 -35
- package/src/bun-platform/fs.ts +1 -0
- package/src/bun-platform/index.ts +18 -1
- package/src/bun-platform/server-lifecycle.ts +218 -0
- package/src/bun-platform/shell.ts +362 -0
- package/src/config.ts +130 -0
- package/src/core/agents/agent.ts +210 -92
- package/src/core/agents/config.ts +8 -0
- package/src/core/agents/retry.ts +16 -17
- package/src/core/agents/state.ts +2 -0
- package/src/core/events/base-event-store.ts +24 -16
- package/src/core/events/file.ts +198 -22
- package/src/core/file-store/containment.ts +75 -0
- package/src/core/file-store/file-store.ts +106 -25
- package/src/core/file-store/types.ts +14 -1
- package/src/core/image/image-processor.ts +3 -1
- package/src/core/image/types.ts +2 -0
- package/src/core/image/vips-resizer.ts +102 -37
- package/src/core/llm/anthropic.ts +63 -79
- package/src/core/llm/logger.ts +175 -37
- package/src/core/llm/openrouter.ts +57 -72
- package/src/core/llm/provider-request.ts +40 -0
- package/src/core/llm/provider.ts +28 -0
- package/src/core/plugins/plugin-builder.ts +106 -14
- package/src/core/preset/preset-builder.ts +12 -22
- package/src/core/sessions/context.ts +18 -0
- package/src/core/sessions/runtime-activity.ts +78 -0
- package/src/core/sessions/schema.ts +29 -3
- package/src/core/sessions/session-manager.ts +560 -140
- package/src/core/sessions/session-store.ts +38 -0
- package/src/core/sessions/session.ts +187 -47
- package/src/core/sessions/state.ts +11 -0
- package/src/core/system.ts +15 -0
- package/src/core/wake-key.ts +65 -0
- package/src/index.ts +30 -12
- package/src/lib/archive/archive-inspection.fixtures.ts +138 -0
- package/src/lib/archive/archive-inspection.ts +359 -0
- package/src/lib/archive/index.ts +17 -0
- package/src/lib/logger/console.ts +7 -57
- package/src/lib/logger/file.ts +28 -50
- package/src/lib/logger/index.ts +3 -1
- package/src/lib/logger/jsonl.ts +36 -0
- package/src/lib/logger/logger.ts +48 -0
- package/src/lib/logger/session-log.ts +42 -0
- package/src/lib/utils/concurrency.ts +37 -8
- package/src/lib/utils/fs-batch.ts +49 -0
- package/src/lib/utils/result.ts +7 -31
- package/src/lib/utils/sleep.ts +23 -0
- package/src/platform/fs-revision.ts +21 -0
- package/src/platform/fs.ts +109 -1
- package/src/platform/git.ts +67 -0
- package/src/platform/index.ts +51 -1
- package/src/platform/llm-call-log.ts +86 -0
- package/src/platform/scheduler.ts +71 -0
- package/src/platform/session-log.ts +39 -0
- package/src/platform/shell.ts +89 -0
- package/src/plugins/agent-status/plugin.ts +1 -0
- package/src/plugins/agents/plugin.ts +134 -81
- package/src/plugins/filesystem/listing.ts +70 -12
- package/src/plugins/filesystem/plugin.ts +8 -6
- package/src/plugins/git-status/plugin.ts +204 -77
- package/src/plugins/llm-debug/plugin.ts +1 -0
- package/src/plugins/logs/plugin.ts +9 -0
- package/src/plugins/mailbox/plugin.ts +46 -64
- package/src/plugins/mailbox/query.ts +15 -7
- package/src/plugins/mailbox/state.ts +1 -0
- package/src/plugins/resources/filename.ts +19 -0
- package/src/plugins/resources/plugin.ts +111 -59
- package/src/plugins/result-eviction/plugin.ts +6 -1
- package/src/plugins/services/plugin.ts +254 -98
- package/src/plugins/services/prompt.ts +1 -6
- package/src/plugins/services/schema.ts +17 -3
- package/src/plugins/services/service.ts +702 -252
- package/src/plugins/session-lifecycle/plugin.ts +56 -41
- package/src/plugins/session-state/plugin.ts +242 -88
- package/src/plugins/session-stats/plugin.ts +1 -0
- package/src/plugins/shell/executor.ts +144 -226
- package/src/plugins/shell/index.ts +1 -4
- package/src/plugins/shell/plugin.ts +7 -9
- package/src/plugins/skills/discovery.ts +10 -8
- package/src/plugins/skills/index.ts +1 -1
- package/src/plugins/skills/plugin.ts +19 -27
- package/src/plugins/skills/prompts.ts +3 -3
- package/src/plugins/skills/schema.ts +5 -2
- package/src/plugins/uploads/plugin.ts +991 -273
- package/src/plugins/uploads/preprocessor.ts +25 -0
- package/src/plugins/uploads/preprocessors/image-classifier.ts +56 -25
- package/src/plugins/uploads/preprocessors/markitdown-preprocessor.ts +41 -15
- package/src/plugins/uploads/preprocessors/pdf-preprocessor.ts +32 -16
- package/src/plugins/uploads/preprocessors/zip-preprocessor.ts +67 -38
- package/src/plugins/uploads/schema.ts +30 -3
- package/src/plugins/uploads/state.ts +12 -0
- package/src/plugins/user-chat/plugin.ts +196 -40
- package/src/plugins/workers/context.ts +102 -39
- package/src/plugins/workers/definition.ts +7 -0
- package/src/plugins/workers/index.ts +4 -4
- package/src/plugins/workers/plugin.ts +461 -151
- package/src/plugins/workers/state.ts +64 -0
- package/src/plugins/workers/worker.ts +15 -4
- package/src/testing/conformance.ts +1751 -0
- package/src/testing/node-platform.ts +5 -0
- package/src/testing/test-harness.ts +21 -38
- package/src/testing/wait-helpers.ts +1 -4
- package/src/transport/adapter/client-adapter.ts +60 -3
- package/src/transport/adapter/heartbeat.ts +50 -0
- package/src/transport/adapter/server-adapter.ts +6 -0
- package/src/transport/http/app.ts +7 -35
- package/src/transport/http/context.ts +46 -0
- package/src/transport/http/fetch-guard.ts +318 -0
- package/src/transport/http/index.ts +1 -1
- package/src/transport/http/middleware/bearer-auth.ts +1 -1
- package/src/transport/http/middleware/error-handler.ts +1 -1
- package/src/transport/http/path-containment.ts +44 -0
- package/src/transport/http/responses.ts +20 -0
- package/src/transport/http/routes/files.ts +69 -159
- package/src/transport/http/routes/resources.ts +124 -93
- package/src/transport/http/routes/rpc.ts +63 -26
- package/src/transport/http/routes/upload.ts +327 -258
- package/src/user-config.ts +93 -7
- package/dist/config.test.d.ts +0 -2
- package/dist/config.test.d.ts.map +0 -1
- package/dist/config.test.js +0 -155
- package/dist/config.test.js.map +0 -1
- package/dist/core/agent-loop.integration.test.d.ts +0 -2
- package/dist/core/agent-loop.integration.test.d.ts.map +0 -1
- package/dist/core/agent-loop.integration.test.js +0 -414
- package/dist/core/agent-loop.integration.test.js.map +0 -1
- package/dist/core/agents/agent-config.test.d.ts +0 -2
- package/dist/core/agents/agent-config.test.d.ts.map +0 -1
- package/dist/core/agents/agent-config.test.js +0 -194
- package/dist/core/agents/agent-config.test.js.map +0 -1
- package/dist/core/agents/agent-shutdown.test.d.ts +0 -6
- package/dist/core/agents/agent-shutdown.test.d.ts.map +0 -1
- package/dist/core/agents/agent-shutdown.test.js +0 -180
- package/dist/core/agents/agent-shutdown.test.js.map +0 -1
- package/dist/core/agents/agent.test.d.ts +0 -2
- package/dist/core/agents/agent.test.d.ts.map +0 -1
- package/dist/core/agents/agent.test.js +0 -313
- package/dist/core/agents/agent.test.js.map +0 -1
- package/dist/core/agents/communicator.d.ts +0 -3
- package/dist/core/agents/communicator.d.ts.map +0 -1
- package/dist/core/agents/communicator.js +0 -13
- package/dist/core/agents/communicator.js.map +0 -1
- package/dist/core/agents/handler-events.test.d.ts +0 -5
- package/dist/core/agents/handler-events.test.d.ts.map +0 -1
- package/dist/core/agents/handler-events.test.js +0 -115
- package/dist/core/agents/handler-events.test.js.map +0 -1
- package/dist/core/agents/response-sanitizer.test.d.ts +0 -2
- package/dist/core/agents/response-sanitizer.test.d.ts.map +0 -1
- package/dist/core/agents/response-sanitizer.test.js +0 -101
- package/dist/core/agents/response-sanitizer.test.js.map +0 -1
- package/dist/core/error-recovery.integration.test.d.ts +0 -2
- package/dist/core/error-recovery.integration.test.d.ts.map +0 -1
- package/dist/core/error-recovery.integration.test.js +0 -288
- package/dist/core/error-recovery.integration.test.js.map +0 -1
- package/dist/core/event-sourcing.integration.test.d.ts +0 -2
- package/dist/core/event-sourcing.integration.test.d.ts.map +0 -1
- package/dist/core/event-sourcing.integration.test.js +0 -154
- package/dist/core/event-sourcing.integration.test.js.map +0 -1
- package/dist/core/events/file.test.d.ts +0 -2
- package/dist/core/events/file.test.d.ts.map +0 -1
- package/dist/core/events/file.test.js +0 -284
- package/dist/core/events/file.test.js.map +0 -1
- package/dist/core/events/memory.test.d.ts +0 -2
- package/dist/core/events/memory.test.d.ts.map +0 -1
- package/dist/core/events/memory.test.js +0 -502
- package/dist/core/events/memory.test.js.map +0 -1
- package/dist/core/image/image-processor.test.d.ts +0 -2
- package/dist/core/image/image-processor.test.d.ts.map +0 -1
- package/dist/core/image/image-processor.test.js +0 -171
- package/dist/core/image/image-processor.test.js.map +0 -1
- package/dist/core/image/vips-resizer.test.d.ts +0 -2
- package/dist/core/image/vips-resizer.test.d.ts.map +0 -1
- package/dist/core/image/vips-resizer.test.js +0 -336
- package/dist/core/image/vips-resizer.test.js.map +0 -1
- package/dist/core/llm/anthropic.test.d.ts +0 -2
- package/dist/core/llm/anthropic.test.d.ts.map +0 -1
- package/dist/core/llm/anthropic.test.js +0 -464
- package/dist/core/llm/anthropic.test.js.map +0 -1
- package/dist/core/llm/cache-breakpoints.test.d.ts +0 -2
- package/dist/core/llm/cache-breakpoints.test.d.ts.map +0 -1
- package/dist/core/llm/cache-breakpoints.test.js +0 -69
- package/dist/core/llm/cache-breakpoints.test.js.map +0 -1
- package/dist/core/llm/cache-live.test.d.ts +0 -16
- package/dist/core/llm/cache-live.test.d.ts.map +0 -1
- package/dist/core/llm/cache-live.test.js +0 -137
- package/dist/core/llm/cache-live.test.js.map +0 -1
- package/dist/core/llm/logger.test.d.ts +0 -7
- package/dist/core/llm/logger.test.d.ts.map +0 -1
- package/dist/core/llm/logger.test.js +0 -228
- package/dist/core/llm/logger.test.js.map +0 -1
- package/dist/core/llm/message-sanitization.test.d.ts +0 -2
- package/dist/core/llm/message-sanitization.test.d.ts.map +0 -1
- package/dist/core/llm/message-sanitization.test.js +0 -181
- package/dist/core/llm/message-sanitization.test.js.map +0 -1
- package/dist/core/llm/mock.test.d.ts +0 -2
- package/dist/core/llm/mock.test.d.ts.map +0 -1
- package/dist/core/llm/mock.test.js +0 -318
- package/dist/core/llm/mock.test.js.map +0 -1
- package/dist/core/llm/model-shortlist-live.test.d.ts +0 -35
- package/dist/core/llm/model-shortlist-live.test.d.ts.map +0 -1
- package/dist/core/llm/model-shortlist-live.test.js +0 -452
- package/dist/core/llm/model-shortlist-live.test.js.map +0 -1
- package/dist/core/llm/openrouter-mapping.test.d.ts +0 -2
- package/dist/core/llm/openrouter-mapping.test.d.ts.map +0 -1
- package/dist/core/llm/openrouter-mapping.test.js +0 -190
- package/dist/core/llm/openrouter-mapping.test.js.map +0 -1
- package/dist/core/llm/openrouter.test.d.ts +0 -2
- package/dist/core/llm/openrouter.test.d.ts.map +0 -1
- package/dist/core/llm/openrouter.test.js +0 -377
- package/dist/core/llm/openrouter.test.js.map +0 -1
- package/dist/core/llm/provider-integration.test.d.ts +0 -12
- package/dist/core/llm/provider-integration.test.d.ts.map +0 -1
- package/dist/core/llm/provider-integration.test.js +0 -350
- package/dist/core/llm/provider-integration.test.js.map +0 -1
- package/dist/core/llm/reasoning-details.test.d.ts +0 -2
- package/dist/core/llm/reasoning-details.test.d.ts.map +0 -1
- package/dist/core/llm/reasoning-details.test.js +0 -144
- package/dist/core/llm/reasoning-details.test.js.map +0 -1
- package/dist/core/llm/routing-provider.test.d.ts +0 -2
- package/dist/core/llm/routing-provider.test.d.ts.map +0 -1
- package/dist/core/llm/routing-provider.test.js +0 -94
- package/dist/core/llm/routing-provider.test.js.map +0 -1
- package/dist/core/llm/snapshot-middleware.test.d.ts +0 -2
- package/dist/core/llm/snapshot-middleware.test.d.ts.map +0 -1
- package/dist/core/llm/snapshot-middleware.test.js +0 -144
- package/dist/core/llm/snapshot-middleware.test.js.map +0 -1
- package/dist/core/llm/thinking-blocks.test.d.ts +0 -2
- package/dist/core/llm/thinking-blocks.test.d.ts.map +0 -1
- package/dist/core/llm/thinking-blocks.test.js +0 -291
- package/dist/core/llm/thinking-blocks.test.js.map +0 -1
- package/dist/core/multi-agent.integration.test.d.ts +0 -2
- package/dist/core/multi-agent.integration.test.d.ts.map +0 -1
- package/dist/core/multi-agent.integration.test.js +0 -298
- package/dist/core/multi-agent.integration.test.js.map +0 -1
- package/dist/core/plugin-hooks.integration.test.d.ts +0 -2
- package/dist/core/plugin-hooks.integration.test.d.ts.map +0 -1
- package/dist/core/plugin-hooks.integration.test.js +0 -458
- package/dist/core/plugin-hooks.integration.test.js.map +0 -1
- package/dist/core/reasoning-round-trip.integration.test.d.ts +0 -2
- package/dist/core/reasoning-round-trip.integration.test.d.ts.map +0 -1
- package/dist/core/reasoning-round-trip.integration.test.js +0 -150
- package/dist/core/reasoning-round-trip.integration.test.js.map +0 -1
- package/dist/core/session-lifecycle.integration.test.d.ts +0 -2
- package/dist/core/session-lifecycle.integration.test.d.ts.map +0 -1
- package/dist/core/session-lifecycle.integration.test.js +0 -159
- package/dist/core/session-lifecycle.integration.test.js.map +0 -1
- package/dist/core/sessions/fork-utils.test.d.ts +0 -2
- package/dist/core/sessions/fork-utils.test.d.ts.map +0 -1
- package/dist/core/sessions/fork-utils.test.js +0 -129
- package/dist/core/sessions/fork-utils.test.js.map +0 -1
- package/dist/core/sessions/session-overrides.integration.test.d.ts +0 -2
- package/dist/core/sessions/session-overrides.integration.test.d.ts.map +0 -1
- package/dist/core/sessions/session-overrides.integration.test.js +0 -154
- package/dist/core/sessions/session-overrides.integration.test.js.map +0 -1
- package/dist/core/sessions/session.test.d.ts +0 -2
- package/dist/core/sessions/session.test.d.ts.map +0 -1
- package/dist/core/sessions/session.test.js +0 -1100
- package/dist/core/sessions/session.test.js.map +0 -1
- package/dist/core/tools/executor.test.d.ts +0 -2
- package/dist/core/tools/executor.test.d.ts.map +0 -1
- package/dist/core/tools/executor.test.js +0 -144
- package/dist/core/tools/executor.test.js.map +0 -1
- package/dist/lib/logger/console.test.d.ts +0 -2
- package/dist/lib/logger/console.test.d.ts.map +0 -1
- package/dist/lib/logger/console.test.js +0 -258
- package/dist/lib/logger/console.test.js.map +0 -1
- package/dist/lib/never.d.ts +0 -2
- package/dist/lib/never.d.ts.map +0 -1
- package/dist/lib/never.js +0 -4
- package/dist/lib/never.js.map +0 -1
- package/dist/lib/utils/concurrency.test.d.ts +0 -2
- package/dist/lib/utils/concurrency.test.d.ts.map +0 -1
- package/dist/lib/utils/concurrency.test.js +0 -135
- package/dist/lib/utils/concurrency.test.js.map +0 -1
- package/dist/plugins/agent-status/agent-status.test.d.ts +0 -2
- package/dist/plugins/agent-status/agent-status.test.d.ts.map +0 -1
- package/dist/plugins/agent-status/agent-status.test.js +0 -136
- package/dist/plugins/agent-status/agent-status.test.js.map +0 -1
- package/dist/plugins/agents/agents.integration.test.d.ts +0 -2
- package/dist/plugins/agents/agents.integration.test.d.ts.map +0 -1
- package/dist/plugins/agents/agents.integration.test.js +0 -830
- package/dist/plugins/agents/agents.integration.test.js.map +0 -1
- package/dist/plugins/agents/supervision.integration.test.d.ts +0 -2
- package/dist/plugins/agents/supervision.integration.test.d.ts.map +0 -1
- package/dist/plugins/agents/supervision.integration.test.js +0 -215
- package/dist/plugins/agents/supervision.integration.test.js.map +0 -1
- package/dist/plugins/context-compact/compaction-live.test.d.ts +0 -17
- package/dist/plugins/context-compact/compaction-live.test.d.ts.map +0 -1
- package/dist/plugins/context-compact/compaction-live.test.js +0 -177
- package/dist/plugins/context-compact/compaction-live.test.js.map +0 -1
- package/dist/plugins/context-compact/context-compact.integration.test.d.ts +0 -2
- package/dist/plugins/context-compact/context-compact.integration.test.d.ts.map +0 -1
- package/dist/plugins/context-compact/context-compact.integration.test.js +0 -364
- package/dist/plugins/context-compact/context-compact.integration.test.js.map +0 -1
- package/dist/plugins/context-compact/context-compactor.test.d.ts +0 -2
- package/dist/plugins/context-compact/context-compactor.test.d.ts.map +0 -1
- package/dist/plugins/context-compact/context-compactor.test.js +0 -858
- package/dist/plugins/context-compact/context-compactor.test.js.map +0 -1
- package/dist/plugins/context-compact/history-offloader.test.d.ts +0 -2
- package/dist/plugins/context-compact/history-offloader.test.d.ts.map +0 -1
- package/dist/plugins/context-compact/history-offloader.test.js +0 -77
- package/dist/plugins/context-compact/history-offloader.test.js.map +0 -1
- package/dist/plugins/filesystem/filesystem.integration.test.d.ts +0 -2
- package/dist/plugins/filesystem/filesystem.integration.test.d.ts.map +0 -1
- package/dist/plugins/filesystem/filesystem.integration.test.js +0 -447
- package/dist/plugins/filesystem/filesystem.integration.test.js.map +0 -1
- package/dist/plugins/filesystem/schema.d.ts +0 -7
- package/dist/plugins/filesystem/schema.d.ts.map +0 -1
- package/dist/plugins/filesystem/schema.js +0 -2
- package/dist/plugins/filesystem/schema.js.map +0 -1
- package/dist/plugins/limits-guard/limit-guard.test.d.ts +0 -2
- package/dist/plugins/limits-guard/limit-guard.test.d.ts.map +0 -1
- package/dist/plugins/limits-guard/limit-guard.test.js +0 -194
- package/dist/plugins/limits-guard/limit-guard.test.js.map +0 -1
- package/dist/plugins/limits-guard/limits-guard.integration.test.d.ts +0 -2
- package/dist/plugins/limits-guard/limits-guard.integration.test.d.ts.map +0 -1
- package/dist/plugins/limits-guard/limits-guard.integration.test.js +0 -887
- package/dist/plugins/limits-guard/limits-guard.integration.test.js.map +0 -1
- package/dist/plugins/llm-debug/llm-debug.integration.test.d.ts +0 -2
- package/dist/plugins/llm-debug/llm-debug.integration.test.d.ts.map +0 -1
- package/dist/plugins/llm-debug/llm-debug.integration.test.js +0 -157
- package/dist/plugins/llm-debug/llm-debug.integration.test.js.map +0 -1
- package/dist/plugins/mailbox/mailbox.integration.test.d.ts +0 -2
- package/dist/plugins/mailbox/mailbox.integration.test.d.ts.map +0 -1
- package/dist/plugins/mailbox/mailbox.integration.test.js +0 -678
- package/dist/plugins/mailbox/mailbox.integration.test.js.map +0 -1
- package/dist/plugins/resources/resources.integration.test.d.ts +0 -2
- package/dist/plugins/resources/resources.integration.test.d.ts.map +0 -1
- package/dist/plugins/resources/resources.integration.test.js +0 -62
- package/dist/plugins/resources/resources.integration.test.js.map +0 -1
- package/dist/plugins/result-eviction/result-eviction.integration.test.d.ts +0 -2
- package/dist/plugins/result-eviction/result-eviction.integration.test.d.ts.map +0 -1
- package/dist/plugins/result-eviction/result-eviction.integration.test.js +0 -294
- package/dist/plugins/result-eviction/result-eviction.integration.test.js.map +0 -1
- package/dist/plugins/services/pid-registry.test.d.ts +0 -2
- package/dist/plugins/services/pid-registry.test.d.ts.map +0 -1
- package/dist/plugins/services/pid-registry.test.js +0 -161
- package/dist/plugins/services/pid-registry.test.js.map +0 -1
- package/dist/plugins/services/services.integration.test.d.ts +0 -2
- package/dist/plugins/services/services.integration.test.d.ts.map +0 -1
- package/dist/plugins/services/services.integration.test.js +0 -1057
- package/dist/plugins/services/services.integration.test.js.map +0 -1
- package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.d.ts +0 -2
- package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.d.ts.map +0 -1
- package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.js +0 -498
- package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.js.map +0 -1
- package/dist/plugins/session-stats/plugin.test.d.ts +0 -2
- package/dist/plugins/session-stats/plugin.test.d.ts.map +0 -1
- package/dist/plugins/session-stats/plugin.test.js +0 -145
- package/dist/plugins/session-stats/plugin.test.js.map +0 -1
- package/dist/plugins/shell/shell.integration.test.d.ts +0 -2
- package/dist/plugins/shell/shell.integration.test.d.ts.map +0 -1
- package/dist/plugins/shell/shell.integration.test.js +0 -234
- package/dist/plugins/shell/shell.integration.test.js.map +0 -1
- package/dist/plugins/shell/shell.test.d.ts +0 -2
- package/dist/plugins/shell/shell.test.d.ts.map +0 -1
- package/dist/plugins/shell/shell.test.js +0 -236
- package/dist/plugins/shell/shell.test.js.map +0 -1
- package/dist/plugins/skills/discovery.test.d.ts +0 -2
- package/dist/plugins/skills/discovery.test.d.ts.map +0 -1
- package/dist/plugins/skills/discovery.test.js +0 -312
- package/dist/plugins/skills/discovery.test.js.map +0 -1
- package/dist/plugins/skills/skills.integration.test.d.ts +0 -2
- package/dist/plugins/skills/skills.integration.test.d.ts.map +0 -1
- package/dist/plugins/skills/skills.integration.test.js +0 -475
- package/dist/plugins/skills/skills.integration.test.js.map +0 -1
- package/dist/plugins/todo/todo.integration.test.d.ts +0 -2
- package/dist/plugins/todo/todo.integration.test.d.ts.map +0 -1
- package/dist/plugins/todo/todo.integration.test.js +0 -605
- package/dist/plugins/todo/todo.integration.test.js.map +0 -1
- package/dist/plugins/uploads/preprocessors/image-classifier.test.d.ts +0 -2
- package/dist/plugins/uploads/preprocessors/image-classifier.test.d.ts.map +0 -1
- package/dist/plugins/uploads/preprocessors/image-classifier.test.js +0 -113
- package/dist/plugins/uploads/preprocessors/image-classifier.test.js.map +0 -1
- package/dist/plugins/uploads/uploads.integration.test.d.ts +0 -2
- package/dist/plugins/uploads/uploads.integration.test.d.ts.map +0 -1
- package/dist/plugins/uploads/uploads.integration.test.js +0 -639
- package/dist/plugins/uploads/uploads.integration.test.js.map +0 -1
- package/dist/plugins/user-chat/user-chat.integration.test.d.ts +0 -2
- package/dist/plugins/user-chat/user-chat.integration.test.d.ts.map +0 -1
- package/dist/plugins/user-chat/user-chat.integration.test.js +0 -754
- package/dist/plugins/user-chat/user-chat.integration.test.js.map +0 -1
- package/dist/plugins/workers/workers.integration.test.d.ts +0 -2
- package/dist/plugins/workers/workers.integration.test.d.ts.map +0 -1
- package/dist/plugins/workers/workers.integration.test.js +0 -629
- package/dist/plugins/workers/workers.integration.test.js.map +0 -1
- package/dist/prompts/macros.test.d.ts +0 -2
- package/dist/prompts/macros.test.d.ts.map +0 -1
- package/dist/prompts/macros.test.js +0 -80
- package/dist/prompts/macros.test.js.map +0 -1
- package/dist/testing/test-harness.test.d.ts +0 -2
- package/dist/testing/test-harness.test.d.ts.map +0 -1
- package/dist/testing/test-harness.test.js +0 -92
- package/dist/testing/test-harness.test.js.map +0 -1
- package/dist/transport/http/routes/rpc.integration.test.d.ts +0 -2
- package/dist/transport/http/routes/rpc.integration.test.d.ts.map +0 -1
- package/dist/transport/http/routes/rpc.integration.test.js +0 -189
- package/dist/transport/http/routes/rpc.integration.test.js.map +0 -1
- package/dist/transport/http/routes/rpc.test.d.ts +0 -2
- package/dist/transport/http/routes/rpc.test.d.ts.map +0 -1
- package/dist/transport/http/routes/rpc.test.js +0 -316
- package/dist/transport/http/routes/rpc.test.js.map +0 -1
- package/src/config.test.ts +0 -174
- package/src/core/agent-loop.integration.test.ts +0 -503
- package/src/core/agents/agent-config.test.ts +0 -240
- package/src/core/agents/agent-shutdown.test.ts +0 -236
- package/src/core/agents/agent.test.ts +0 -387
- package/src/core/agents/communicator.ts +0 -16
- package/src/core/agents/handler-events.test.ts +0 -167
- package/src/core/agents/response-sanitizer.test.ts +0 -137
- package/src/core/error-recovery.integration.test.ts +0 -343
- package/src/core/event-sourcing.integration.test.ts +0 -191
- package/src/core/events/file.test.ts +0 -436
- package/src/core/events/memory.test.ts +0 -741
- package/src/core/image/image-processor.test.ts +0 -218
- package/src/core/image/vips-resizer.test.ts +0 -388
- package/src/core/llm/anthropic.test.ts +0 -546
- package/src/core/llm/cache-breakpoints.test.ts +0 -85
- package/src/core/llm/cache-live.test.ts +0 -155
- package/src/core/llm/logger.test.ts +0 -270
- package/src/core/llm/message-sanitization.test.ts +0 -217
- package/src/core/llm/mock.test.ts +0 -402
- package/src/core/llm/model-shortlist-live.test.ts +0 -577
- package/src/core/llm/openrouter-mapping.test.ts +0 -234
- package/src/core/llm/openrouter.test.ts +0 -499
- package/src/core/llm/provider-integration.test.ts +0 -408
- package/src/core/llm/reasoning-details.test.ts +0 -177
- package/src/core/llm/routing-provider.test.ts +0 -113
- package/src/core/llm/snapshot-middleware.test.ts +0 -168
- package/src/core/llm/thinking-blocks.test.ts +0 -345
- package/src/core/multi-agent.integration.test.ts +0 -340
- package/src/core/plugin-hooks.integration.test.ts +0 -568
- package/src/core/reasoning-round-trip.integration.test.ts +0 -188
- package/src/core/session-lifecycle.integration.test.ts +0 -202
- package/src/core/sessions/fork-utils.test.ts +0 -158
- package/src/core/sessions/session-overrides.integration.test.ts +0 -210
- package/src/core/sessions/session.test.ts +0 -1736
- package/src/core/tools/executor.test.ts +0 -161
- package/src/lib/logger/console.test.ts +0 -348
- package/src/lib/never.ts +0 -3
- package/src/lib/utils/concurrency.test.ts +0 -169
- package/src/plugins/agent-status/agent-status.test.ts +0 -164
- package/src/plugins/agents/agents.integration.test.ts +0 -976
- package/src/plugins/agents/supervision.integration.test.ts +0 -249
- package/src/plugins/context-compact/compaction-live.test.ts +0 -221
- package/src/plugins/context-compact/context-compact.integration.test.ts +0 -418
- package/src/plugins/context-compact/context-compactor.test.ts +0 -1036
- package/src/plugins/context-compact/history-offloader.test.ts +0 -100
- package/src/plugins/filesystem/filesystem.integration.test.ts +0 -529
- package/src/plugins/filesystem/schema.ts +0 -6
- package/src/plugins/limits-guard/limit-guard.test.ts +0 -253
- package/src/plugins/limits-guard/limits-guard.integration.test.ts +0 -1012
- package/src/plugins/llm-debug/llm-debug.integration.test.ts +0 -192
- package/src/plugins/mailbox/mailbox.integration.test.ts +0 -794
- package/src/plugins/resources/resources.integration.test.ts +0 -68
- package/src/plugins/result-eviction/result-eviction.integration.test.ts +0 -332
- package/src/plugins/services/pid-registry.test.ts +0 -180
- package/src/plugins/services/services.integration.test.ts +0 -1223
- package/src/plugins/session-lifecycle/session-lifecycle.integration.test.ts +0 -639
- package/src/plugins/session-stats/plugin.test.ts +0 -160
- package/src/plugins/shell/shell.integration.test.ts +0 -273
- package/src/plugins/shell/shell.test.ts +0 -314
- package/src/plugins/skills/discovery.test.ts +0 -395
- package/src/plugins/skills/skills.integration.test.ts +0 -580
- package/src/plugins/todo/todo.integration.test.ts +0 -687
- package/src/plugins/uploads/preprocessors/image-classifier.test.ts +0 -142
- package/src/plugins/uploads/uploads.integration.test.ts +0 -831
- package/src/plugins/user-chat/user-chat.integration.test.ts +0 -893
- package/src/plugins/workers/workers.integration.test.ts +0 -778
- package/src/prompts/macros.test.ts +0 -107
- package/src/testing/test-harness.test.ts +0 -114
- package/src/transport/http/routes/rpc.integration.test.ts +0 -243
- package/src/transport/http/routes/rpc.test.ts +0 -372
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { resolve } from 'node:path'
|
|
10
|
+
import type { SessionCloseReason } from '~/core/plugins/plugin-builder.js'
|
|
10
11
|
import type { SessionId } from '~/core/sessions/schema.js'
|
|
11
12
|
import type { Result } from '~/lib/utils/result.js'
|
|
12
13
|
import { Err, Ok } from '~/lib/utils/result.js'
|
|
@@ -14,9 +15,9 @@ import type { FileSystem } from '~/platform/fs.js'
|
|
|
14
15
|
import type { ChildProcess, ProcessRunner } from '~/platform/process.js'
|
|
15
16
|
import type { PortPool } from '~/plugins/services/port-pool.js'
|
|
16
17
|
import type { ServicePidRegistry } from '~/plugins/services/pid-registry.js'
|
|
17
|
-
import type { ServiceConfig, ServiceStartArgs, ServiceStatus } from '~/plugins/services/schema.js'
|
|
18
|
+
import type { ServiceConfig, ServiceStartArgs, ServiceStatus, ServiceStopSource } from '~/plugins/services/schema.js'
|
|
18
19
|
import type { ToolError } from '../../core/tools/executor.js'
|
|
19
|
-
import type { Logger } from '../../lib/logger/logger.js'
|
|
20
|
+
import type { LogContext, Logger } from '../../lib/logger/logger.js'
|
|
20
21
|
import { RingBuffer } from '../../lib/logger/ring-buffer.js'
|
|
21
22
|
|
|
22
23
|
// ============================================================================
|
|
@@ -56,13 +57,34 @@ interface RunningService {
|
|
|
56
57
|
config: ServiceConfig
|
|
57
58
|
process: ChildProcess
|
|
58
59
|
pid: number
|
|
60
|
+
/** Captured at spawn so a later reap can tell our process from a recycled pid. */
|
|
61
|
+
pidStartTime?: number
|
|
59
62
|
status: ServiceStatus
|
|
60
63
|
port: number
|
|
61
64
|
cwd?: string
|
|
62
65
|
command: string
|
|
63
66
|
logs: RingBuffer
|
|
67
|
+
forgetPid: () => Promise<void>
|
|
68
|
+
/** Set once the child's `close` fired, or once we gave up our claim on the group. */
|
|
69
|
+
exited: boolean
|
|
70
|
+
/** Who asked for the stop in progress — read back when the child's exit lands. */
|
|
71
|
+
stopSource?: ServiceStopSource
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A process we spawned and may still have to reclaim. */
|
|
75
|
+
export interface ReapTarget {
|
|
76
|
+
pid: number
|
|
77
|
+
/** Start time captured at spawn; undefined on non-Linux, or when /proc was unreadable. */
|
|
78
|
+
pidStartTime?: number
|
|
64
79
|
}
|
|
65
80
|
|
|
81
|
+
/**
|
|
82
|
+
* What a guarded reap did. `killed` covers a group that was already gone — the signal
|
|
83
|
+
* had nothing left to reach. `pid-reused` means the kernel handed the pid on and the
|
|
84
|
+
* process under it is somebody else's.
|
|
85
|
+
*/
|
|
86
|
+
export type ReapOutcome = 'killed' | 'pid-reused'
|
|
87
|
+
|
|
66
88
|
export interface ServiceStatusChangeDetails {
|
|
67
89
|
port?: number
|
|
68
90
|
error?: string
|
|
@@ -70,6 +92,8 @@ export interface ServiceStatusChangeDetails {
|
|
|
70
92
|
pidStartTime?: number
|
|
71
93
|
cwd?: string
|
|
72
94
|
command?: string
|
|
95
|
+
/** Set on a `stopped` change: who asked for it. See ServiceEntry.stoppedBy. */
|
|
96
|
+
stoppedBy?: ServiceStopSource
|
|
73
97
|
/** Set on a `failed` change that already has a revival queued — see ServiceEntry.restartAt. */
|
|
74
98
|
restartAt?: number
|
|
75
99
|
restartAttempt?: number
|
|
@@ -94,6 +118,16 @@ const PORT_CONFLICT_PATTERN = /EADDRINUSE|address already in use/i
|
|
|
94
118
|
*/
|
|
95
119
|
const MAX_PORT_CONFLICT_RETRIES = 3
|
|
96
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Deadline for embedder callbacks and PID-registry writes inside a transition.
|
|
123
|
+
*
|
|
124
|
+
* A transition holds the runtime lease that idle eviction waits on, so a resolver
|
|
125
|
+
* or a registry write that never settles pins the session resident forever and
|
|
126
|
+
* defeats the bound the lease exists to enforce. None of them is meant to take
|
|
127
|
+
* 30 s — the deadline only turns "wedged" into "failed".
|
|
128
|
+
*/
|
|
129
|
+
const DEFAULT_SERVICE_HOOK_TIMEOUT_MS = 30_000
|
|
130
|
+
|
|
97
131
|
/** `restartPolicy` defaults — see ServiceConfig.restartPolicy for the rationale. */
|
|
98
132
|
const DEFAULT_MAX_RESTART_RETRIES = 3
|
|
99
133
|
const DEFAULT_RESTART_DELAY_MS = 1000
|
|
@@ -108,7 +142,24 @@ export interface ServiceExecutorDeps {
|
|
|
108
142
|
fs: FileSystem
|
|
109
143
|
process: ProcessRunner
|
|
110
144
|
/** Durable pid record, swept at agent boot. Optional so embedders without a data dir still work. */
|
|
111
|
-
pidRegistry?: ServicePidRegistry
|
|
145
|
+
pidRegistry?: Pick<ServicePidRegistry, 'record' | 'forget'>
|
|
146
|
+
/** Test seam for process-group failure handling. */
|
|
147
|
+
kill?: typeof process.kill
|
|
148
|
+
/** Deadline for embedder callbacks and PID-registry writes; test seam, defaults to 30 s. */
|
|
149
|
+
hookTimeoutMs?: number
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
type ProcessGroupProbe = { state: 'alive' | 'gone' } | { state: 'error'; error: Error }
|
|
153
|
+
|
|
154
|
+
let serviceExecutorObserverForTesting: ((executor: ServiceExecutor) => void) | undefined
|
|
155
|
+
|
|
156
|
+
/** Observe executor creation in integration tests without exposing lifecycle methods as plugin API. */
|
|
157
|
+
export function setServiceExecutorObserverForTesting(observer: (executor: ServiceExecutor) => void): () => void {
|
|
158
|
+
const previous = serviceExecutorObserverForTesting
|
|
159
|
+
serviceExecutorObserverForTesting = observer
|
|
160
|
+
return () => {
|
|
161
|
+
if (serviceExecutorObserverForTesting === observer) serviceExecutorObserverForTesting = previous
|
|
162
|
+
}
|
|
112
163
|
}
|
|
113
164
|
|
|
114
165
|
export class ServiceExecutor {
|
|
@@ -117,8 +168,13 @@ export class ServiceExecutor {
|
|
|
117
168
|
private readonly waiters = new Map<string, Array<{ resolve: (result: Result<void, ToolError>) => void; timer: ReturnType<typeof setTimeout> }>>()
|
|
118
169
|
/** Per-type lock collapsing concurrent start() calls onto a single in-flight start. */
|
|
119
170
|
private readonly startInFlight = new Map<string, Promise<Result<void, ToolError>>>()
|
|
171
|
+
/** Per-type stop operation awaited by runtime disposal. */
|
|
172
|
+
private readonly stopInFlight = new Map<string, Promise<Result<void, ToolError>>>()
|
|
173
|
+
/** Registry deletions that must settle before a replacement can record its PID. */
|
|
174
|
+
private readonly pidForgets = new Map<string, Promise<void>>()
|
|
120
175
|
/** Per-type counter bounding automatic EADDRINUSE port re-allocation retries. */
|
|
121
176
|
private readonly portConflictRetries = new Map<string, number>()
|
|
177
|
+
private readonly portConflictTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
122
178
|
/** Per-type counter bounding `restartPolicy` revivals after an unexpected exit. */
|
|
123
179
|
private readonly restartRetries = new Map<string, number>()
|
|
124
180
|
/** Pending `restartPolicy` timers, cancelled when someone stops or starts the service by hand. */
|
|
@@ -127,7 +183,11 @@ export class ServiceExecutor {
|
|
|
127
183
|
private readonly portPool: PortPool
|
|
128
184
|
private readonly fs: FileSystem
|
|
129
185
|
private readonly processRunner: ProcessRunner
|
|
130
|
-
private readonly pidRegistry?: ServicePidRegistry
|
|
186
|
+
private readonly pidRegistry?: Pick<ServicePidRegistry, 'record' | 'forget'>
|
|
187
|
+
private readonly killProcess: typeof process.kill
|
|
188
|
+
private readonly hookTimeoutMs: number
|
|
189
|
+
private closing = false
|
|
190
|
+
private closePromise?: Promise<void>
|
|
131
191
|
|
|
132
192
|
/** Optional callback invoked on every service status change */
|
|
133
193
|
onStatusChanged?: (
|
|
@@ -136,6 +196,8 @@ export class ServiceExecutor {
|
|
|
136
196
|
status: ServiceStatus,
|
|
137
197
|
details: ServiceStatusChangeDetails,
|
|
138
198
|
) => void
|
|
199
|
+
/** Invoked after every owned start attempt, including deferred retries. */
|
|
200
|
+
onStartSettled?: (serviceType: string) => void
|
|
139
201
|
|
|
140
202
|
constructor(logger: Logger, portPool: PortPool, deps: ServiceExecutorDeps) {
|
|
141
203
|
this.logger = logger
|
|
@@ -143,6 +205,32 @@ export class ServiceExecutor {
|
|
|
143
205
|
this.fs = deps.fs
|
|
144
206
|
this.processRunner = deps.process
|
|
145
207
|
this.pidRegistry = deps.pidRegistry
|
|
208
|
+
this.killProcess = deps.kill ?? process.kill
|
|
209
|
+
this.hookTimeoutMs = deps.hookTimeoutMs ?? DEFAULT_SERVICE_HOOK_TIMEOUT_MS
|
|
210
|
+
serviceExecutorObserverForTesting?.(this)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Reject when `operation` outlives {@link DEFAULT_SERVICE_HOOK_TIMEOUT_MS}.
|
|
215
|
+
*
|
|
216
|
+
* The operation itself cannot be cancelled — an embedder callback keeps running —
|
|
217
|
+
* but the transition stops waiting on it, which is what releases the lease.
|
|
218
|
+
*/
|
|
219
|
+
private async withDeadline<T>(label: string, operation: () => Promise<T> | T): Promise<T> {
|
|
220
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
221
|
+
try {
|
|
222
|
+
return await Promise.race([
|
|
223
|
+
(async () => await operation())(),
|
|
224
|
+
new Promise<never>((_, reject) => {
|
|
225
|
+
timer = setTimeout(
|
|
226
|
+
() => reject(new Error(`${label} did not settle within ${this.hookTimeoutMs}ms`)),
|
|
227
|
+
this.hookTimeoutMs,
|
|
228
|
+
)
|
|
229
|
+
}),
|
|
230
|
+
])
|
|
231
|
+
} finally {
|
|
232
|
+
if (timer) clearTimeout(timer)
|
|
233
|
+
}
|
|
146
234
|
}
|
|
147
235
|
|
|
148
236
|
private notifyStatusChanged(
|
|
@@ -161,7 +249,10 @@ export class ServiceExecutor {
|
|
|
161
249
|
if (!config.availableWhen) return Ok(true)
|
|
162
250
|
|
|
163
251
|
try {
|
|
164
|
-
return Ok(await
|
|
252
|
+
return Ok(await this.withDeadline(
|
|
253
|
+
`Service '${config.type}' availability check`,
|
|
254
|
+
() => config.availableWhen?.({ sessionId: String(sessionId), workspaceDir }) ?? true,
|
|
255
|
+
))
|
|
165
256
|
} catch (error) {
|
|
166
257
|
const message = error instanceof Error ? error.message : String(error)
|
|
167
258
|
return Err({ message: `Service '${config.type}' availability check failed: ${message}`, recoverable: true })
|
|
@@ -177,7 +268,11 @@ export class ServiceExecutor {
|
|
|
177
268
|
recoverable: true,
|
|
178
269
|
})
|
|
179
270
|
}
|
|
180
|
-
const
|
|
271
|
+
const cwdResolver = config.cwd
|
|
272
|
+
const raw = await this.withDeadline(
|
|
273
|
+
`Service '${config.type}' cwd resolver`,
|
|
274
|
+
() => cwdResolver({ sessionId: String(sessionId), workspaceDir }),
|
|
275
|
+
)
|
|
181
276
|
return Ok(resolve(workspaceDir, raw))
|
|
182
277
|
}
|
|
183
278
|
|
|
@@ -257,6 +352,7 @@ export class ServiceExecutor {
|
|
|
257
352
|
workspaceDir?: string,
|
|
258
353
|
preferredPort?: number,
|
|
259
354
|
): Promise<Result<void, ToolError>> {
|
|
355
|
+
if (this.closing) return Err({ message: 'Service executor is closing', recoverable: false })
|
|
260
356
|
const inFlight = this.startInFlight.get(config.type)
|
|
261
357
|
if (inFlight) return inFlight
|
|
262
358
|
|
|
@@ -266,6 +362,7 @@ export class ServiceExecutor {
|
|
|
266
362
|
return await promise
|
|
267
363
|
} finally {
|
|
268
364
|
this.startInFlight.delete(config.type)
|
|
365
|
+
this.onStartSettled?.(config.type)
|
|
269
366
|
}
|
|
270
367
|
}
|
|
271
368
|
|
|
@@ -275,6 +372,7 @@ export class ServiceExecutor {
|
|
|
275
372
|
workspaceDir?: string,
|
|
276
373
|
preferredPort?: number,
|
|
277
374
|
): Promise<Result<void, ToolError>> {
|
|
375
|
+
if (this.closing) return Err({ message: 'Service executor is closing', recoverable: false })
|
|
278
376
|
// This start supersedes any revival the policy had queued.
|
|
279
377
|
this.cancelPendingRestart(config.type)
|
|
280
378
|
|
|
@@ -282,8 +380,30 @@ export class ServiceExecutor {
|
|
|
282
380
|
if (existing && (existing.status === 'starting' || existing.status === 'ready')) {
|
|
283
381
|
return Ok(undefined)
|
|
284
382
|
}
|
|
383
|
+
if (existing?.status === 'stopping') {
|
|
384
|
+
return Err({ message: `Service '${config.type}' is stopping, cannot start`, recoverable: true })
|
|
385
|
+
}
|
|
386
|
+
if (existing) {
|
|
387
|
+
// Whatever sits under this type is about to be overwritten — in this map and in
|
|
388
|
+
// the pid registry, both keyed by (session, type) — and to lose its port to the
|
|
389
|
+
// process spawned below, since a restart deliberately reuses it. A `failed`
|
|
390
|
+
// entry can still own a live process group, so reclaim it before its handle is
|
|
391
|
+
// dropped or nothing will ever be able to find it again.
|
|
392
|
+
const reaped = await this.reapEntry(existing, 'superseded by a new start')
|
|
393
|
+
if (!reaped.ok) {
|
|
394
|
+
this.logger.warn('Could not reclaim the superseded service process group', {
|
|
395
|
+
serviceType: config.type,
|
|
396
|
+
pid: existing.pid,
|
|
397
|
+
error: reaped.error.message,
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
await existing.forgetPid()
|
|
401
|
+
}
|
|
402
|
+
await this.pidForgets.get(config.type)
|
|
403
|
+
if (this.closing) return Err({ message: 'Service executor is closing', recoverable: false })
|
|
285
404
|
|
|
286
405
|
const availability = await this.isAvailable(config, sessionId, workspaceDir)
|
|
406
|
+
if (this.closing) return Err({ message: 'Service executor is closing', recoverable: false })
|
|
287
407
|
if (!availability.ok) return availability
|
|
288
408
|
if (!availability.value) {
|
|
289
409
|
return Err({ message: `Service '${config.type}' is not available in this workspace`, recoverable: true })
|
|
@@ -313,10 +433,30 @@ export class ServiceExecutor {
|
|
|
313
433
|
}
|
|
314
434
|
|
|
315
435
|
const cwd = cwdResult.value
|
|
316
|
-
|
|
436
|
+
if (this.closing) {
|
|
437
|
+
if (allocatedNow) {
|
|
438
|
+
this.allocatedPorts.delete(config.type)
|
|
439
|
+
this.portPool.release(port)
|
|
440
|
+
}
|
|
441
|
+
return Err({ message: 'Service executor is closing', recoverable: false })
|
|
442
|
+
}
|
|
443
|
+
const logBufferSize = Math.max(1, config.logBufferSize ?? 200)
|
|
317
444
|
const startupTimeoutMs = config.startupTimeoutMs ?? 30_000
|
|
318
445
|
|
|
319
446
|
const readyRegex = config.readyPattern ? new RegExp(config.readyPattern) : undefined
|
|
447
|
+
const logs = new RingBuffer(logBufferSize)
|
|
448
|
+
const maxDiagnosticLength = 16_384
|
|
449
|
+
const startTime = Date.now()
|
|
450
|
+
let portConflictDetected = false
|
|
451
|
+
let setupComplete = false
|
|
452
|
+
let retryPortConflictDuringSetup = false
|
|
453
|
+
let readyDetectedDuringSetup = false
|
|
454
|
+
let readyLineDuringSetup: string | undefined
|
|
455
|
+
let stdoutPartial = ''
|
|
456
|
+
let stderrPartial = ''
|
|
457
|
+
let outputSequence = 0
|
|
458
|
+
let stdoutPartialSequence = 0
|
|
459
|
+
let stderrPartialSequence = 0
|
|
320
460
|
|
|
321
461
|
const startArgs = {
|
|
322
462
|
port,
|
|
@@ -328,12 +468,14 @@ export class ServiceExecutor {
|
|
|
328
468
|
let command: string
|
|
329
469
|
let serviceEnv: Record<string, string> | undefined
|
|
330
470
|
try {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
471
|
+
const commandResolver = config.command
|
|
472
|
+
command = typeof commandResolver === 'function'
|
|
473
|
+
? await this.withDeadline(`Service '${config.type}' command resolver`, () => commandResolver(startArgs))
|
|
474
|
+
: commandResolver
|
|
475
|
+
const envResolver = config.env
|
|
476
|
+
serviceEnv = typeof envResolver === 'function'
|
|
477
|
+
? await this.withDeadline(`Service '${config.type}' env resolver`, () => envResolver(startArgs))
|
|
478
|
+
: envResolver
|
|
337
479
|
} catch (error) {
|
|
338
480
|
const message = error instanceof Error ? error.message : String(error)
|
|
339
481
|
const errorMessage = `Service '${config.type}' start resolver failed: ${message}`
|
|
@@ -344,6 +486,13 @@ export class ServiceExecutor {
|
|
|
344
486
|
this.notifyStatusChanged(sessionId, config.type, 'failed', { port, cwd, error: errorMessage })
|
|
345
487
|
return Err({ message: errorMessage, recoverable: true })
|
|
346
488
|
}
|
|
489
|
+
if (this.closing) {
|
|
490
|
+
if (allocatedNow) {
|
|
491
|
+
this.allocatedPorts.delete(config.type)
|
|
492
|
+
this.portPool.release(port)
|
|
493
|
+
}
|
|
494
|
+
return Err({ message: 'Service executor is closing', recoverable: false })
|
|
495
|
+
}
|
|
347
496
|
|
|
348
497
|
const shell = process.platform === 'win32' ? 'cmd.exe' : '/bin/sh'
|
|
349
498
|
const shellFlag = process.platform === 'win32' ? '/c' : '-c'
|
|
@@ -362,10 +511,18 @@ export class ServiceExecutor {
|
|
|
362
511
|
const current = this.services.get(config.type)
|
|
363
512
|
if (current && current.process === child && current.status !== 'stopped' && current.status !== 'failed') {
|
|
364
513
|
current.status = 'failed'
|
|
514
|
+
// The pid rides along on purpose — see the startup timeout below.
|
|
365
515
|
this.notifyStatusChanged(sessionId, config.type, 'failed', {
|
|
366
516
|
error: error.message,
|
|
367
517
|
cwd,
|
|
368
518
|
command,
|
|
519
|
+
pid: current.pid,
|
|
520
|
+
pidStartTime: current.pidStartTime,
|
|
521
|
+
})
|
|
522
|
+
void this.reapEntry(current, 'process error').then((reaped) => {
|
|
523
|
+
if (!reaped.ok) {
|
|
524
|
+
this.logger.error('Could not kill errored service process group', reaped.error, { serviceType: config.type, pid: current.pid })
|
|
525
|
+
}
|
|
369
526
|
})
|
|
370
527
|
}
|
|
371
528
|
})
|
|
@@ -378,6 +535,88 @@ export class ServiceExecutor {
|
|
|
378
535
|
this.notifyStatusChanged(sessionId, config.type, 'failed', { port, cwd, command, error: 'Failed to spawn process' })
|
|
379
536
|
return Err({ message: 'Failed to spawn service process', recoverable: true })
|
|
380
537
|
}
|
|
538
|
+
// Bind the narrowed pid: the deferred registry call below cannot carry the
|
|
539
|
+
// narrowing across the closure boundary.
|
|
540
|
+
const pid = child.pid
|
|
541
|
+
|
|
542
|
+
const matches = (regex: RegExp, content: string): boolean => {
|
|
543
|
+
regex.lastIndex = 0
|
|
544
|
+
const matched = regex.test(content)
|
|
545
|
+
regex.lastIndex = 0
|
|
546
|
+
return matched
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const recordDiagnostic = (line: string, prefix: string) => {
|
|
550
|
+
const maxContentLength = Math.max(0, maxDiagnosticLength - prefix.length)
|
|
551
|
+
const bounded = line.length > maxContentLength ? line.slice(-maxContentLength) : line
|
|
552
|
+
logs.push(`${prefix}${bounded}`)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Detection sees the full incoming content before diagnostics are truncated.
|
|
556
|
+
const appendOutput = (partial: string, data: Buffer, prefix: string): string => {
|
|
557
|
+
const combined = partial + data.toString()
|
|
558
|
+
if (matches(PORT_CONFLICT_PATTERN, combined)) portConflictDetected = true
|
|
559
|
+
const lines = combined.split('\n')
|
|
560
|
+
const nextPartial = lines.pop() ?? ''
|
|
561
|
+
const readyMatched = readyRegex ? matches(readyRegex, combined) || lines.some((line) => matches(readyRegex, line)) : false
|
|
562
|
+
if (readyMatched && !setupComplete) {
|
|
563
|
+
readyDetectedDuringSetup = true
|
|
564
|
+
readyLineDuringSetup = combined.slice(-maxDiagnosticLength)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
for (const line of lines) recordDiagnostic(line, prefix)
|
|
568
|
+
|
|
569
|
+
if (setupComplete) {
|
|
570
|
+
const current = this.services.get(config.type)
|
|
571
|
+
if (current?.process === child && current.status === 'starting') {
|
|
572
|
+
this.logger.debug('Service output', { serviceType: config.type })
|
|
573
|
+
if (readyMatched) markReady(combined.slice(-maxDiagnosticLength))
|
|
574
|
+
}
|
|
575
|
+
void checkReadyWhen()
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Retain enough suffix for markers split across chunks, never an entire line.
|
|
579
|
+
return nextPartial.length > maxDiagnosticLength ? nextPartial.slice(-maxDiagnosticLength) : nextPartial
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const onStdout = (data: Buffer) => {
|
|
583
|
+
stdoutPartial = appendOutput(stdoutPartial, data, '')
|
|
584
|
+
stdoutPartialSequence = stdoutPartial ? ++outputSequence : 0
|
|
585
|
+
}
|
|
586
|
+
const onStderr = (data: Buffer) => {
|
|
587
|
+
stderrPartial = appendOutput(stderrPartial, data, '[stderr] ')
|
|
588
|
+
stderrPartialSequence = stderrPartial ? ++outputSequence : 0
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
let exitDuringSetup: { code: number | null } | undefined
|
|
592
|
+
let forgetPromise: Promise<void> | undefined
|
|
593
|
+
const forgetPid = (): Promise<void> => {
|
|
594
|
+
if (forgetPromise) return forgetPromise
|
|
595
|
+
const previous = this.pidForgets.get(config.type)
|
|
596
|
+
const runForget = () => this.withDeadline(
|
|
597
|
+
`Service '${config.type}' PID forget`,
|
|
598
|
+
() => this.pidRegistry?.forget(String(sessionId), config.type) ?? Promise.resolve(),
|
|
599
|
+
)
|
|
600
|
+
const operation = previous ? previous.then(runForget, runForget) : runForget()
|
|
601
|
+
forgetPromise = operation
|
|
602
|
+
this.pidForgets.set(config.type, operation)
|
|
603
|
+
void operation.then(
|
|
604
|
+
() => {
|
|
605
|
+
if (this.pidForgets.get(config.type) === operation) this.pidForgets.delete(config.type)
|
|
606
|
+
},
|
|
607
|
+
() => {
|
|
608
|
+
if (this.pidForgets.get(config.type) === operation) this.pidForgets.delete(config.type)
|
|
609
|
+
if (forgetPromise === operation) forgetPromise = undefined
|
|
610
|
+
},
|
|
611
|
+
)
|
|
612
|
+
return operation
|
|
613
|
+
}
|
|
614
|
+
const bufferClose = (code: number | null) => {
|
|
615
|
+
exitDuringSetup = { code }
|
|
616
|
+
}
|
|
617
|
+
child.stdout?.on('data', onStdout)
|
|
618
|
+
child.stderr?.on('data', onStderr)
|
|
619
|
+
child.on('close', bufferClose)
|
|
381
620
|
|
|
382
621
|
// Capture start time immediately so a later PID-reuse check can distinguish
|
|
383
622
|
// "our process" from "an unrelated process that grabbed this PID after ours died"
|
|
@@ -386,26 +625,58 @@ export class ServiceExecutor {
|
|
|
386
625
|
// Durably record the process so a future agent can reap it if we die before the
|
|
387
626
|
// `close` handler gets to forget it. The session projection cannot serve that
|
|
388
627
|
// purpose — see ServicePidRegistry.
|
|
389
|
-
|
|
628
|
+
try {
|
|
629
|
+
await this.withDeadline(
|
|
630
|
+
`Service '${config.type}' PID record`,
|
|
631
|
+
() => this.pidRegistry?.record({ sessionId: String(sessionId), serviceType: config.type, pid, pidStartTime, command })
|
|
632
|
+
?? Promise.resolve(),
|
|
633
|
+
)
|
|
634
|
+
} catch (error) {
|
|
635
|
+
// The child is already running but unrecorded, so nothing could reap it
|
|
636
|
+
// after a crash — tear it down here rather than leak it.
|
|
637
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
638
|
+
const errorMessage = `Service '${config.type}' could not record its PID: ${message}`
|
|
639
|
+
try {
|
|
640
|
+
this.killProcess(-child.pid, 'SIGKILL')
|
|
641
|
+
} catch {
|
|
642
|
+
// The process may already be gone.
|
|
643
|
+
}
|
|
644
|
+
if (allocatedNow) {
|
|
645
|
+
this.allocatedPorts.delete(config.type)
|
|
646
|
+
this.portPool.release(port)
|
|
647
|
+
}
|
|
648
|
+
this.notifyStatusChanged(sessionId, config.type, 'failed', { port, cwd, command, error: errorMessage })
|
|
649
|
+
return Err({ message: errorMessage, recoverable: true })
|
|
650
|
+
}
|
|
651
|
+
if (this.closing) {
|
|
652
|
+
try {
|
|
653
|
+
this.killProcess(-child.pid, 'SIGKILL')
|
|
654
|
+
} catch {
|
|
655
|
+
// The process may already be gone.
|
|
656
|
+
}
|
|
657
|
+
await forgetPid()
|
|
658
|
+
if (allocatedNow) {
|
|
659
|
+
this.allocatedPorts.delete(config.type)
|
|
660
|
+
this.portPool.release(port)
|
|
661
|
+
}
|
|
662
|
+
return Err({ message: 'Service executor is closing', recoverable: false })
|
|
663
|
+
}
|
|
390
664
|
|
|
391
665
|
// Emit starting event with PID, port, resolved cwd/command, and start time.
|
|
392
666
|
this.notifyStatusChanged(sessionId, config.type, 'starting', { port, pid: child.pid, pidStartTime, cwd, command })
|
|
393
667
|
|
|
394
|
-
const logs = new RingBuffer(logBufferSize)
|
|
395
|
-
const startTime = Date.now()
|
|
396
|
-
// Set when the child reports a port bind-conflict; read by the close handler
|
|
397
|
-
// to decide between a fresh-port retry and a terminal failure.
|
|
398
|
-
let portConflictDetected = false
|
|
399
|
-
|
|
400
668
|
const entry: RunningService = {
|
|
401
669
|
config,
|
|
402
670
|
process: child,
|
|
403
671
|
pid: child.pid,
|
|
672
|
+
pidStartTime,
|
|
404
673
|
status: 'starting',
|
|
405
674
|
port,
|
|
406
675
|
cwd,
|
|
407
676
|
command,
|
|
408
677
|
logs,
|
|
678
|
+
forgetPid,
|
|
679
|
+
exited: false,
|
|
409
680
|
}
|
|
410
681
|
this.services.set(config.type, entry)
|
|
411
682
|
|
|
@@ -467,27 +738,6 @@ export class ServiceExecutor {
|
|
|
467
738
|
}
|
|
468
739
|
}
|
|
469
740
|
|
|
470
|
-
const processLine = (line: string) => {
|
|
471
|
-
logs.push(line)
|
|
472
|
-
if (PORT_CONFLICT_PATTERN.test(line)) {
|
|
473
|
-
portConflictDetected = true
|
|
474
|
-
}
|
|
475
|
-
const current = this.services.get(config.type)
|
|
476
|
-
if (!current || current.process !== child) return
|
|
477
|
-
|
|
478
|
-
if (current.status === 'starting') {
|
|
479
|
-
this.logger.debug('Service output', { serviceType: config.type, line })
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (readyRegex && current.status === 'starting') {
|
|
483
|
-
if (readyRegex.test(line)) {
|
|
484
|
-
markReady(line)
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
void checkReadyWhen()
|
|
489
|
-
}
|
|
490
|
-
|
|
491
741
|
// Startup timeout — mark as failed if not ready in time
|
|
492
742
|
if (readyRegex || config.readyWhen) {
|
|
493
743
|
startupTimer = setTimeout(() => {
|
|
@@ -498,19 +748,23 @@ export class ServiceExecutor {
|
|
|
498
748
|
const errorMsg = `Service startup timed out after ${startupTimeoutMs}ms`
|
|
499
749
|
this.logger.error(errorMsg, undefined, { serviceType: config.type })
|
|
500
750
|
clearReadinessTimers()
|
|
751
|
+
// The pid rides along on purpose: should this agent die before the reap
|
|
752
|
+
// below completes, the recorded pid is all a later boot has to find the
|
|
753
|
+
// survivor. The child's exit takes the claim away again.
|
|
501
754
|
this.notifyStatusChanged(sessionId, config.type, 'failed', {
|
|
502
755
|
port: current.port,
|
|
503
756
|
cwd: current.cwd,
|
|
504
757
|
command: current.command,
|
|
505
758
|
error: errorMsg,
|
|
759
|
+
pid: current.pid,
|
|
760
|
+
pidStartTime: current.pidStartTime,
|
|
506
761
|
})
|
|
507
762
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
763
|
+
void this.reapEntry(current, 'startup timeout').then((reaped) => {
|
|
764
|
+
if (!reaped.ok) {
|
|
765
|
+
this.logger.error('Could not kill timed-out service process group', reaped.error, { serviceType: config.type, pid: current.pid })
|
|
766
|
+
}
|
|
767
|
+
})
|
|
514
768
|
}, startupTimeoutMs)
|
|
515
769
|
}
|
|
516
770
|
|
|
@@ -519,55 +773,38 @@ export class ServiceExecutor {
|
|
|
519
773
|
readyCheckTimer = setInterval(() => {
|
|
520
774
|
void checkReadyWhen()
|
|
521
775
|
}, intervalMs)
|
|
522
|
-
void checkReadyWhen()
|
|
523
776
|
}
|
|
524
777
|
|
|
525
|
-
//
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
processLine(line)
|
|
533
|
-
}
|
|
534
|
-
})
|
|
535
|
-
|
|
536
|
-
let stderrPartial = ''
|
|
537
|
-
child.stderr?.on('data', (data: Buffer) => {
|
|
538
|
-
stderrPartial += data.toString()
|
|
539
|
-
const lines = stderrPartial.split('\n')
|
|
540
|
-
stderrPartial = lines.pop()!
|
|
541
|
-
for (const line of lines) {
|
|
542
|
-
processLine(`[stderr] ${line}`)
|
|
543
|
-
}
|
|
544
|
-
})
|
|
545
|
-
|
|
546
|
-
// Handle unexpected exit
|
|
547
|
-
child.on('close', (code) => {
|
|
778
|
+
// Handle unexpected exit. Named and guarded because it also has to be
|
|
779
|
+
// replayable — see the exit-during-setup check after markReady() below.
|
|
780
|
+
let closeHandled = false
|
|
781
|
+
const handleClose = (code: number | null) => {
|
|
782
|
+
if (closeHandled) return
|
|
783
|
+
closeHandled = true
|
|
784
|
+
entry.exited = true
|
|
548
785
|
clearReadinessTimers()
|
|
549
|
-
//
|
|
550
|
-
void
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
processLine(`[stderr] ${stderrPartial}`)
|
|
558
|
-
stderrPartial = ''
|
|
786
|
+
// A replacement must not be recorded until this deletion finishes.
|
|
787
|
+
void forgetPid()
|
|
788
|
+
const partials = [
|
|
789
|
+
{ content: stdoutPartial, prefix: '', sequence: stdoutPartialSequence },
|
|
790
|
+
{ content: stderrPartial, prefix: '[stderr] ', sequence: stderrPartialSequence },
|
|
791
|
+
]
|
|
792
|
+
for (const partial of partials.filter(({ content }) => content).sort((left, right) => left.sequence - right.sequence)) {
|
|
793
|
+
recordDiagnostic(partial.content, partial.prefix)
|
|
559
794
|
}
|
|
795
|
+
stdoutPartial = ''
|
|
796
|
+
stderrPartial = ''
|
|
560
797
|
|
|
561
798
|
const current = this.services.get(config.type)
|
|
562
799
|
if (!current || current.process !== child) return
|
|
563
800
|
|
|
564
801
|
if (current.status === 'stopping') {
|
|
565
|
-
// Expected stop
|
|
802
|
+
// Expected stop — the child's exit, not stopInternal, is what publishes it.
|
|
566
803
|
current.status = 'stopped'
|
|
567
|
-
this.notifyStatusChanged(sessionId, config.type, 'stopped')
|
|
804
|
+
this.notifyStatusChanged(sessionId, config.type, 'stopped', { stoppedBy: current.stopSource ?? 'agent' })
|
|
568
805
|
} else if (current.status === 'starting' || current.status === 'ready') {
|
|
569
806
|
const retries = this.portConflictRetries.get(config.type) ?? 0
|
|
570
|
-
if (portConflictDetected && retries < MAX_PORT_CONFLICT_RETRIES) {
|
|
807
|
+
if (!this.closing && current.status === 'starting' && portConflictDetected && retries < MAX_PORT_CONFLICT_RETRIES) {
|
|
571
808
|
// The chosen port is held by a foreign process (e.g. a service
|
|
572
809
|
// leaked from another session, or a survivor of a previous boot).
|
|
573
810
|
// Re-allocate a fresh port and retry so the service recovers on a
|
|
@@ -584,7 +821,16 @@ export class ServiceExecutor {
|
|
|
584
821
|
})
|
|
585
822
|
this.allocatedPorts.delete(config.type)
|
|
586
823
|
this.services.delete(config.type)
|
|
587
|
-
|
|
824
|
+
if (!setupComplete) {
|
|
825
|
+
retryPortConflictDuringSetup = true
|
|
826
|
+
} else {
|
|
827
|
+
const retryTimer = setTimeout(() => {
|
|
828
|
+
this.portConflictTimers.delete(config.type)
|
|
829
|
+
if (!this.closing) void this.start(config, sessionId, workspaceDir)
|
|
830
|
+
}, 0)
|
|
831
|
+
retryTimer.unref?.()
|
|
832
|
+
this.portConflictTimers.set(config.type, retryTimer)
|
|
833
|
+
}
|
|
588
834
|
return
|
|
589
835
|
}
|
|
590
836
|
|
|
@@ -606,8 +852,16 @@ export class ServiceExecutor {
|
|
|
606
852
|
serviceType: config.type,
|
|
607
853
|
code,
|
|
608
854
|
})
|
|
855
|
+
} else if (current.status === 'failed') {
|
|
856
|
+
// The process this entry still claimed is now confirmed gone — a startup
|
|
857
|
+
// timeout or a process error published the pid without being able to
|
|
858
|
+
// confirm the kill. Take the claim away so no later boot goes looking for
|
|
859
|
+
// it; the status and the error that explain the failure stay as they are.
|
|
860
|
+
this.notifyStatusChanged(sessionId, config.type, 'failed')
|
|
609
861
|
}
|
|
610
|
-
}
|
|
862
|
+
}
|
|
863
|
+
child.on('close', handleClose)
|
|
864
|
+
child.off('close', bufferClose)
|
|
611
865
|
|
|
612
866
|
this.logger.info('Service starting', {
|
|
613
867
|
serviceType: config.type,
|
|
@@ -619,9 +873,34 @@ export class ServiceExecutor {
|
|
|
619
873
|
startupTimeoutMs,
|
|
620
874
|
})
|
|
621
875
|
|
|
622
|
-
//
|
|
623
|
-
|
|
876
|
+
// A service that died inside the bookkeeping above closed while only the
|
|
877
|
+
// setup close collector was listening, so replay that close now.
|
|
878
|
+
// `alreadyReaped` covers the in-between state: the exit is recorded but
|
|
879
|
+
// 'close' has not fired yet because it waits for the stdio EOF. The listener
|
|
880
|
+
// above is guaranteed to receive it, so calling handleClose here would only
|
|
881
|
+
// drop the tail of the log the failure has to explain — while marking such a
|
|
882
|
+
// child ready would be a lie.
|
|
883
|
+
const alreadyReaped = child.exitCode != null || child.signalCode != null
|
|
884
|
+
if (exitDuringSetup) {
|
|
885
|
+
handleClose(exitDuringSetup.code)
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
setupComplete = true
|
|
889
|
+
if (retryPortConflictDuringSetup) {
|
|
890
|
+
await forgetPid()
|
|
891
|
+
if (this.closing) return Err({ message: 'Service executor is closing', recoverable: false })
|
|
892
|
+
return this.startInternal(config, sessionId, workspaceDir)
|
|
893
|
+
}
|
|
894
|
+
if (exitDuringSetup || alreadyReaped) {
|
|
895
|
+
return Ok(undefined)
|
|
896
|
+
}
|
|
897
|
+
if (readyDetectedDuringSetup) {
|
|
898
|
+
markReady(readyLineDuringSetup)
|
|
899
|
+
} else if (!readyRegex && !config.readyWhen) {
|
|
900
|
+
// If no ready condition is configured, a live child is ready immediately.
|
|
624
901
|
markReady()
|
|
902
|
+
} else if (config.readyWhen) {
|
|
903
|
+
void checkReadyWhen()
|
|
625
904
|
}
|
|
626
905
|
|
|
627
906
|
return Ok(undefined)
|
|
@@ -641,6 +920,7 @@ export class ServiceExecutor {
|
|
|
641
920
|
uptimeMs: number,
|
|
642
921
|
exitCode: number | null,
|
|
643
922
|
): ScheduledRestart | undefined {
|
|
923
|
+
if (this.closing) return undefined
|
|
644
924
|
const policy = config.restartPolicy
|
|
645
925
|
if (!policy) return undefined
|
|
646
926
|
|
|
@@ -675,7 +955,7 @@ export class ServiceExecutor {
|
|
|
675
955
|
|
|
676
956
|
const timer = setTimeout(() => {
|
|
677
957
|
this.restartTimers.delete(config.type)
|
|
678
|
-
void this.start(config, sessionId, workspaceDir)
|
|
958
|
+
if (!this.closing) void this.start(config, sessionId, workspaceDir)
|
|
679
959
|
}, delayMs)
|
|
680
960
|
// A pending revival must not keep the process alive on its own.
|
|
681
961
|
timer.unref?.()
|
|
@@ -693,151 +973,389 @@ export class ServiceExecutor {
|
|
|
693
973
|
return true
|
|
694
974
|
}
|
|
695
975
|
|
|
696
|
-
/**
|
|
976
|
+
/**
|
|
977
|
+
* Whether the service is down but coming back on its own — a `restartPolicy`
|
|
978
|
+
* revival or a queued port-conflict retry. Both are timers this executor
|
|
979
|
+
* owns, so both need the runtime to outlive the call that armed them.
|
|
980
|
+
*/
|
|
697
981
|
hasScheduledRestart(serviceType: string): boolean {
|
|
698
|
-
return this.restartTimers.has(serviceType)
|
|
982
|
+
return this.restartTimers.has(serviceType) || this.portConflictTimers.has(serviceType)
|
|
699
983
|
}
|
|
700
984
|
|
|
701
985
|
/**
|
|
702
|
-
* Stop
|
|
703
|
-
*
|
|
986
|
+
* Stop accepting work and drain all runtime-owned processes and timers.
|
|
987
|
+
*
|
|
988
|
+
* `reason: 'evicted'` only parks the session: every service keeps its full
|
|
989
|
+
* graceful window and is recorded as stopped by the runtime, which is what
|
|
990
|
+
* lets autoStart bring it back once the runtime is rebuilt. The other
|
|
991
|
+
* reasons end the session, so a process group that ignores SIGTERM is taken
|
|
992
|
+
* down without waiting.
|
|
704
993
|
*/
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
this.
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
994
|
+
close(sessionId: SessionId, reason: SessionCloseReason = 'closed'): Promise<void> {
|
|
995
|
+
this.closing = true
|
|
996
|
+
this.closePromise ??= this.performClose(sessionId, reason).finally(() => {
|
|
997
|
+
this.onStatusChanged = undefined
|
|
998
|
+
this.onStartSettled = undefined
|
|
999
|
+
})
|
|
1000
|
+
return this.closePromise
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
private async performClose(sessionId: SessionId, reason: SessionCloseReason): Promise<void> {
|
|
1004
|
+
this.cancelPortConflictTimers()
|
|
1005
|
+
await Promise.allSettled([...this.startInFlight.values()])
|
|
1006
|
+
this.cancelPortConflictTimers()
|
|
1007
|
+
const concurrentStopTypes = new Set(this.stopInFlight.keys())
|
|
1008
|
+
await Promise.allSettled([...this.stopInFlight.values()])
|
|
1009
|
+
|
|
1010
|
+
// One service that cannot be signalled must not strand the others, and must not
|
|
1011
|
+
// skip the tail — portPool is process-global, so a skipped release loses those
|
|
1012
|
+
// ports for the life of the server.
|
|
1013
|
+
const failures: unknown[] = []
|
|
1014
|
+
const parking = reason === 'evicted'
|
|
1015
|
+
try {
|
|
1016
|
+
for (const [serviceType, entry] of [...this.services]) {
|
|
1017
|
+
if (entry.status === 'stopped') continue
|
|
1018
|
+
if (entry.status === 'starting' || entry.status === 'ready' || entry.status === 'stopping' || this.restartTimers.has(serviceType)) {
|
|
1019
|
+
const forcedGraceMs = parking ? entry.config.gracefulStopMs ?? 5000 : 0
|
|
1020
|
+
try {
|
|
1021
|
+
if (concurrentStopTypes.has(serviceType)) {
|
|
1022
|
+
await this.forceStopForClose(serviceType, entry, sessionId, forcedGraceMs)
|
|
1023
|
+
continue
|
|
1024
|
+
}
|
|
1025
|
+
const stopped = await this.stop(serviceType, sessionId, 'eviction')
|
|
1026
|
+
if (!stopped.ok) await this.forceStopForClose(serviceType, entry, sessionId, forcedGraceMs)
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
failures.push(error)
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
} finally {
|
|
1033
|
+
this.cancelAllDeferredStarts()
|
|
1034
|
+
await Promise.allSettled([...this.pidForgets.values()])
|
|
1035
|
+
for (const port of this.allocatedPorts.values()) this.portPool.release(port)
|
|
1036
|
+
this.allocatedPorts.clear()
|
|
1037
|
+
this.services.clear()
|
|
1038
|
+
for (const serviceType of this.waiters.keys()) this.resolveWaiters(serviceType, 'stopped')
|
|
711
1039
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1040
|
+
|
|
1041
|
+
// Still reject — a close that could not terminate a process is not a clean close.
|
|
1042
|
+
if (failures.length === 1) throw failures[0]
|
|
1043
|
+
if (failures.length > 1) {
|
|
1044
|
+
throw new AggregateError(failures, `Failed to stop ${failures.length} services during close`)
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
private async forceStopForClose(serviceType: string, entry: RunningService, sessionId: SessionId, gracefulStopMs: number): Promise<void> {
|
|
1049
|
+
const previousStatus = entry.status
|
|
1050
|
+
this.recordStopSource(entry, 'eviction')
|
|
1051
|
+
if (entry.status !== 'stopping') {
|
|
1052
|
+
entry.status = 'stopping'
|
|
1053
|
+
this.notifyStatusChanged(sessionId, serviceType, 'stopping')
|
|
1054
|
+
}
|
|
1055
|
+
const stopped = await this.terminateProcessGroup(entry.pid, gracefulStopMs)
|
|
1056
|
+
if (!stopped.ok) {
|
|
1057
|
+
// Mirror stopInternal: no recovery path matches a stranded `stopping`, so a
|
|
1058
|
+
// force-stop that failed has to leave a status the next runtime can reap.
|
|
1059
|
+
if (entry.status === 'stopping' && previousStatus !== 'stopping') {
|
|
1060
|
+
entry.status = previousStatus
|
|
1061
|
+
this.notifyStatusChanged(sessionId, serviceType, previousStatus, {
|
|
1062
|
+
port: entry.port,
|
|
1063
|
+
cwd: entry.cwd,
|
|
1064
|
+
command: entry.command,
|
|
1065
|
+
error: stopped.error.message,
|
|
1066
|
+
})
|
|
719
1067
|
}
|
|
720
|
-
|
|
1068
|
+
throw stopped.error
|
|
1069
|
+
}
|
|
1070
|
+
// The group is confirmed gone, so nothing may offer this pid up for a reap again.
|
|
1071
|
+
entry.exited = true
|
|
1072
|
+
await entry.forgetPid()
|
|
1073
|
+
if (this.services.get(serviceType) === entry) {
|
|
1074
|
+
entry.status = 'stopped'
|
|
1075
|
+
this.notifyStatusChanged(sessionId, serviceType, 'stopped', { stoppedBy: entry.stopSource ?? 'eviction' })
|
|
721
1076
|
}
|
|
1077
|
+
}
|
|
722
1078
|
|
|
723
|
-
|
|
724
|
-
|
|
1079
|
+
/** An agent stop stays the agent's decision even when a lifecycle stop is what finishes it. */
|
|
1080
|
+
private recordStopSource(entry: RunningService, stoppedBy: ServiceStopSource): void {
|
|
1081
|
+
if (stoppedBy === 'agent' || entry.stopSource === undefined) entry.stopSource = stoppedBy
|
|
1082
|
+
}
|
|
725
1083
|
|
|
726
|
-
|
|
1084
|
+
private cancelAllDeferredStarts(): void {
|
|
1085
|
+
for (const timer of this.restartTimers.values()) clearTimeout(timer)
|
|
1086
|
+
this.restartTimers.clear()
|
|
1087
|
+
this.cancelPortConflictTimers()
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
private cancelPortConflictTimers(): void {
|
|
1091
|
+
for (const timer of this.portConflictTimers.values()) clearTimeout(timer)
|
|
1092
|
+
this.portConflictTimers.clear()
|
|
1093
|
+
}
|
|
727
1094
|
|
|
728
|
-
|
|
1095
|
+
private errorWithContext(error: unknown, context: string): Error {
|
|
1096
|
+
const cause = error instanceof Error ? error.message : String(error)
|
|
1097
|
+
return new Error(`${context}: ${cause}`)
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
private hasErrorCode(error: unknown, code: string): boolean {
|
|
1101
|
+
return error instanceof Error && 'code' in error && error.code === code
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
private async hasNonZombieProcessGroupMember(pid: number): Promise<boolean> {
|
|
1105
|
+
if (process.platform !== 'linux') return true
|
|
1106
|
+
|
|
1107
|
+
let entries: string[]
|
|
729
1108
|
try {
|
|
730
|
-
|
|
1109
|
+
entries = await this.fs.readdir('/proc')
|
|
731
1110
|
} catch {
|
|
732
|
-
|
|
733
|
-
entry.status = 'stopped'
|
|
734
|
-
this.notifyStatusChanged(sessionId, serviceType, 'stopped')
|
|
735
|
-
return Ok(undefined)
|
|
1111
|
+
return true
|
|
736
1112
|
}
|
|
737
1113
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1114
|
+
let foundGroupMember = false
|
|
1115
|
+
for (const entry of entries) {
|
|
1116
|
+
if (!/^\d+$/.test(entry)) continue
|
|
1117
|
+
try {
|
|
1118
|
+
const stat = await this.fs.readFile(`/proc/${entry}/stat`, 'utf-8')
|
|
1119
|
+
const rparen = stat.lastIndexOf(')')
|
|
1120
|
+
if (rparen === -1) continue
|
|
1121
|
+
const fields = stat.slice(rparen + 2).split(' ')
|
|
1122
|
+
if (Number(fields[2]) !== pid) continue
|
|
1123
|
+
foundGroupMember = true
|
|
1124
|
+
if (fields[0] !== 'Z') return true
|
|
1125
|
+
} catch {
|
|
1126
|
+
// Processes can disappear while /proc is scanned.
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
// A successful group probe with no readable member is still evidence of life.
|
|
1130
|
+
return !foundGroupMember
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
private async probeProcessGroup(pid: number): Promise<ProcessGroupProbe> {
|
|
1134
|
+
try {
|
|
1135
|
+
this.killProcess(-pid, 0)
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
if (this.hasErrorCode(error, 'ESRCH')) return { state: 'gone' }
|
|
1138
|
+
if (this.hasErrorCode(error, 'EPERM')) return { state: 'alive' }
|
|
1139
|
+
return { state: 'error', error: this.errorWithContext(error, `Failed to probe process group ${pid}`) }
|
|
1140
|
+
}
|
|
751
1141
|
|
|
752
|
-
|
|
1142
|
+
return await this.hasNonZombieProcessGroupMember(pid) ? { state: 'alive' } : { state: 'gone' }
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
private async signalProcessGroup(pid: number, signal: NodeJS.Signals): Promise<ProcessGroupProbe> {
|
|
1146
|
+
try {
|
|
1147
|
+
this.killProcess(-pid, signal)
|
|
1148
|
+
return { state: 'alive' }
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
if (this.hasErrorCode(error, 'ESRCH')) return { state: 'gone' }
|
|
1151
|
+
return { state: 'error', error: this.errorWithContext(error, `Failed to send ${signal} to process group ${pid}`) }
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
private async waitForProcessGroupExit(pid: number, timeoutMs: number): Promise<ProcessGroupProbe> {
|
|
1156
|
+
const initial = await this.probeProcessGroup(pid)
|
|
1157
|
+
if (initial.state !== 'alive') return initial
|
|
1158
|
+
|
|
1159
|
+
return new Promise((resolve) => {
|
|
1160
|
+
let settled = false
|
|
1161
|
+
const finish = (result: ProcessGroupProbe) => {
|
|
1162
|
+
if (settled) return
|
|
1163
|
+
settled = true
|
|
753
1164
|
clearInterval(checkInterval)
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1165
|
+
clearTimeout(timeout)
|
|
1166
|
+
resolve(result)
|
|
1167
|
+
}
|
|
1168
|
+
const checkInterval = setInterval(() => {
|
|
1169
|
+
void this.probeProcessGroup(pid).then((result) => {
|
|
1170
|
+
if (result.state !== 'alive') finish(result)
|
|
1171
|
+
})
|
|
1172
|
+
}, 50)
|
|
1173
|
+
const timeout = setTimeout(() => {
|
|
1174
|
+
void this.probeProcessGroup(pid).then(finish)
|
|
1175
|
+
}, timeoutMs)
|
|
761
1176
|
})
|
|
1177
|
+
}
|
|
762
1178
|
|
|
763
|
-
|
|
1179
|
+
/**
|
|
1180
|
+
* Reclaim a service process group we spawned, unless the kernel recycled its pid.
|
|
1181
|
+
*
|
|
1182
|
+
* Every path that gives up on a service comes through here, because ending the
|
|
1183
|
+
* *wait* is not ending the *process*: a dev server that missed its readiness
|
|
1184
|
+
* window keeps its several hundred MB of RSS until somebody kills it, which on a
|
|
1185
|
+
* memory-starved box is what makes the next startup miss its window too.
|
|
1186
|
+
*
|
|
1187
|
+
* `pidStartTime` was captured at spawn, so a different current start time means
|
|
1188
|
+
* the pid now belongs to an unrelated process and killing it would be disastrous.
|
|
1189
|
+
* An unknown start time (non-Linux, no /proc) leaves only the kill-and-hope path.
|
|
1190
|
+
*/
|
|
1191
|
+
async reapProcessGroup(target: ReapTarget, context: LogContext = {}): Promise<Result<ReapOutcome, Error>> {
|
|
1192
|
+
const currentStartTime = await getProcessStartTime(this.fs, target.pid)
|
|
1193
|
+
if (target.pidStartTime !== undefined && currentStartTime !== undefined && currentStartTime !== target.pidStartTime) {
|
|
1194
|
+
this.logger.warn('PID reuse detected — refusing to kill service process group', {
|
|
1195
|
+
...context,
|
|
1196
|
+
pid: target.pid,
|
|
1197
|
+
storedStartTime: target.pidStartTime,
|
|
1198
|
+
currentStartTime,
|
|
1199
|
+
})
|
|
1200
|
+
return Ok('pid-reused')
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const killed = this.killProcessGroup(target.pid)
|
|
1204
|
+
if (!killed.ok) return killed
|
|
1205
|
+
return Ok('killed')
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* Reclaim the process group of an entry we are giving up on or replacing.
|
|
1210
|
+
*
|
|
1211
|
+
* A no-op once the child's `close` has fired: there is nothing left to reclaim,
|
|
1212
|
+
* and skipping it also keeps the non-Linux kill-and-hope path away from a pid the
|
|
1213
|
+
* kernel may since have handed to somebody else. A settled reap ends our claim,
|
|
1214
|
+
* so the entry is never offered up twice.
|
|
1215
|
+
*/
|
|
1216
|
+
private async reapEntry(entry: RunningService, reason: string): Promise<Result<void, Error>> {
|
|
1217
|
+
if (entry.exited) return Ok(undefined)
|
|
1218
|
+
const reaped = await this.reapProcessGroup(
|
|
1219
|
+
{ pid: entry.pid, pidStartTime: entry.pidStartTime },
|
|
1220
|
+
{ serviceType: entry.config.type, reason },
|
|
1221
|
+
)
|
|
1222
|
+
if (!reaped.ok) return reaped
|
|
1223
|
+
entry.exited = true
|
|
764
1224
|
return Ok(undefined)
|
|
765
1225
|
}
|
|
766
1226
|
|
|
767
1227
|
/**
|
|
768
|
-
*
|
|
1228
|
+
* SIGKILL a process group through the injectable kill seam, without waiting
|
|
1229
|
+
* for it to go. Reached through {@link reapProcessGroup} wherever a recorded
|
|
1230
|
+
* pid could have been recycled.
|
|
769
1231
|
*/
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (entry && (entry.status === 'starting' || entry.status === 'ready' || entry.status === 'paused')) {
|
|
778
|
-
const stopResult = await this.stop(config.type, sessionId)
|
|
779
|
-
if (!stopResult.ok) return stopResult
|
|
1232
|
+
killProcessGroup(pid: number): Result<void, Error> {
|
|
1233
|
+
try {
|
|
1234
|
+
this.killProcess(-pid, 'SIGKILL')
|
|
1235
|
+
return Ok(undefined)
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
if (this.hasErrorCode(error, 'ESRCH')) return Ok(undefined) // Already gone.
|
|
1238
|
+
return Err(this.errorWithContext(error, `Failed to SIGKILL process group ${pid}`))
|
|
780
1239
|
}
|
|
1240
|
+
}
|
|
781
1241
|
|
|
782
|
-
|
|
1242
|
+
private async terminateProcessGroup(pid: number, gracefulStopMs: number): Promise<Result<void, Error>> {
|
|
1243
|
+
const initial = await this.probeProcessGroup(pid)
|
|
1244
|
+
if (initial.state === 'gone') return Ok(undefined)
|
|
1245
|
+
if (initial.state === 'error') return Err(initial.error)
|
|
1246
|
+
|
|
1247
|
+
const term = await this.signalProcessGroup(pid, 'SIGTERM')
|
|
1248
|
+
if (term.state === 'gone') return Ok(undefined)
|
|
1249
|
+
if (term.state === 'error') return Err(term.error)
|
|
1250
|
+
const graceful = await this.waitForProcessGroupExit(pid, gracefulStopMs)
|
|
1251
|
+
if (graceful.state === 'gone') return Ok(undefined)
|
|
1252
|
+
if (graceful.state === 'error') return Err(graceful.error)
|
|
1253
|
+
|
|
1254
|
+
const kill = await this.signalProcessGroup(pid, 'SIGKILL')
|
|
1255
|
+
if (kill.state === 'gone') return Ok(undefined)
|
|
1256
|
+
if (kill.state === 'error') return Err(kill.error)
|
|
1257
|
+
const forced = await this.waitForProcessGroupExit(pid, 5000)
|
|
1258
|
+
if (forced.state === 'gone') return Ok(undefined)
|
|
1259
|
+
if (forced.state === 'error') return Err(forced.error)
|
|
1260
|
+
return Err(new Error(`Process group ${pid} survived SIGKILL`))
|
|
783
1261
|
}
|
|
784
1262
|
|
|
785
1263
|
/**
|
|
786
|
-
*
|
|
1264
|
+
* Stop a running service gracefully.
|
|
1265
|
+
* Port is NOT released — kept for session-level stability across restarts.
|
|
787
1266
|
*/
|
|
788
|
-
async
|
|
1267
|
+
async stop(serviceType: string, sessionId: SessionId, stoppedBy: ServiceStopSource = 'agent'): Promise<Result<void, ToolError>> {
|
|
1268
|
+
const inFlight = this.stopInFlight.get(serviceType)
|
|
1269
|
+
if (inFlight) return inFlight
|
|
1270
|
+
|
|
1271
|
+
let operation: Promise<Result<void, ToolError>>
|
|
1272
|
+
operation = this.stopInternal(serviceType, sessionId, stoppedBy).finally(() => {
|
|
1273
|
+
if (this.stopInFlight.get(serviceType) === operation) this.stopInFlight.delete(serviceType)
|
|
1274
|
+
})
|
|
1275
|
+
this.stopInFlight.set(serviceType, operation)
|
|
1276
|
+
return operation
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
private async stopInternal(serviceType: string, sessionId: SessionId, stoppedBy: ServiceStopSource): Promise<Result<void, ToolError>> {
|
|
1280
|
+
const hadPendingRestart = this.cancelPendingRestart(serviceType)
|
|
1281
|
+
this.restartRetries.delete(serviceType)
|
|
789
1282
|
const entry = this.services.get(serviceType)
|
|
790
1283
|
if (!entry) {
|
|
791
1284
|
return Err({ message: `Service '${serviceType}' not found`, recoverable: false })
|
|
792
1285
|
}
|
|
793
|
-
if (entry.status !== 'ready') {
|
|
794
|
-
|
|
1286
|
+
if (entry.status !== 'starting' && entry.status !== 'ready') {
|
|
1287
|
+
// `failed` says the start gave up, not that the process did — and refusing to
|
|
1288
|
+
// stop it left the platform-side reaper, the one caller that can free a leaked
|
|
1289
|
+
// dev server's memory, with no way to do so. A failed service with a revival
|
|
1290
|
+
// queued is on top of that really "about to restart", and calling that off is a
|
|
1291
|
+
// legitimate stop rather than an error.
|
|
1292
|
+
if (entry.status === 'failed' || hadPendingRestart) {
|
|
1293
|
+
const reaped = await this.reapEntry(entry, 'stopped while failed')
|
|
1294
|
+
if (!reaped.ok) return Err({ message: reaped.error.message, recoverable: true })
|
|
1295
|
+
try {
|
|
1296
|
+
await entry.forgetPid()
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
return Err({ message: this.errorWithContext(error, `Failed to forget PID for service '${serviceType}'`).message, recoverable: true })
|
|
1299
|
+
}
|
|
1300
|
+
entry.status = 'stopped'
|
|
1301
|
+
this.notifyStatusChanged(sessionId, serviceType, 'stopped', { stoppedBy })
|
|
1302
|
+
return Ok(undefined)
|
|
1303
|
+
}
|
|
1304
|
+
return Err({ message: `Service '${serviceType}' is ${entry.status}, cannot stop`, recoverable: false })
|
|
795
1305
|
}
|
|
796
1306
|
|
|
1307
|
+
const previousStatus = entry.status
|
|
1308
|
+
this.recordStopSource(entry, stoppedBy)
|
|
1309
|
+
entry.status = 'stopping'
|
|
1310
|
+
this.notifyStatusChanged(sessionId, serviceType, 'stopping')
|
|
1311
|
+
|
|
1312
|
+
const gracefulStopMs = entry.config.gracefulStopMs ?? 5000
|
|
1313
|
+
|
|
1314
|
+
const stopped = await this.terminateProcessGroup(entry.pid, gracefulStopMs)
|
|
1315
|
+
if (!stopped.ok) {
|
|
1316
|
+
if (entry.status === 'stopping') {
|
|
1317
|
+
entry.status = previousStatus
|
|
1318
|
+
this.notifyStatusChanged(sessionId, serviceType, previousStatus, {
|
|
1319
|
+
port: entry.port,
|
|
1320
|
+
cwd: entry.cwd,
|
|
1321
|
+
command: entry.command,
|
|
1322
|
+
error: stopped.error.message,
|
|
1323
|
+
})
|
|
1324
|
+
}
|
|
1325
|
+
return Err({ message: stopped.error.message, recoverable: true })
|
|
1326
|
+
}
|
|
1327
|
+
// The group is confirmed gone, so nothing may offer this pid up for a reap again.
|
|
1328
|
+
entry.exited = true
|
|
797
1329
|
try {
|
|
798
|
-
|
|
799
|
-
} catch {
|
|
800
|
-
return Err({ message: `Failed to
|
|
1330
|
+
await entry.forgetPid()
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
return Err({ message: this.errorWithContext(error, `Failed to forget PID for service '${serviceType}'`).message, recoverable: true })
|
|
1333
|
+
}
|
|
1334
|
+
if (entry.status === 'stopping') {
|
|
1335
|
+
entry.status = 'stopped'
|
|
1336
|
+
this.notifyStatusChanged(sessionId, serviceType, 'stopped', { stoppedBy: entry.stopSource ?? stoppedBy })
|
|
801
1337
|
}
|
|
802
1338
|
|
|
803
|
-
|
|
804
|
-
this.notifyStatusChanged(sessionId, serviceType, 'paused')
|
|
805
|
-
|
|
806
|
-
this.logger.info('Service paused', { serviceType })
|
|
1339
|
+
this.logger.info('Service stopped', { serviceType })
|
|
807
1340
|
return Ok(undefined)
|
|
808
1341
|
}
|
|
809
1342
|
|
|
810
1343
|
/**
|
|
811
|
-
*
|
|
1344
|
+
* Restart a service (stop + start).
|
|
812
1345
|
*/
|
|
813
|
-
async
|
|
1346
|
+
async restart(
|
|
814
1347
|
config: ServiceConfig,
|
|
815
1348
|
sessionId: SessionId,
|
|
816
|
-
|
|
1349
|
+
workspaceDir?: string,
|
|
1350
|
+
preferredPort?: number,
|
|
817
1351
|
): Promise<Result<void, ToolError>> {
|
|
818
1352
|
const entry = this.services.get(config.type)
|
|
819
|
-
if (
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
if (entry.status !== 'paused') {
|
|
823
|
-
return Err({ message: `Service '${config.type}' is ${entry.status}, cannot resume`, recoverable: false })
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
try {
|
|
827
|
-
process.kill(entry.pid, 'SIGCONT')
|
|
828
|
-
} catch {
|
|
829
|
-
return Err({ message: `Failed to resume service '${config.type}'`, recoverable: false })
|
|
1353
|
+
if (entry && (entry.status === 'starting' || entry.status === 'ready')) {
|
|
1354
|
+
const stopResult = await this.stop(config.type, sessionId)
|
|
1355
|
+
if (!stopResult.ok) return stopResult
|
|
830
1356
|
}
|
|
831
1357
|
|
|
832
|
-
|
|
833
|
-
this.notifyStatusChanged(sessionId, config.type, 'ready', {
|
|
834
|
-
port: entry.port,
|
|
835
|
-
cwd: entry.cwd,
|
|
836
|
-
command: entry.command,
|
|
837
|
-
})
|
|
838
|
-
|
|
839
|
-
this.logger.info('Service resumed', { serviceType: config.type })
|
|
840
|
-
return Ok(undefined)
|
|
1358
|
+
return this.start(config, sessionId, workspaceDir, preferredPort)
|
|
841
1359
|
}
|
|
842
1360
|
|
|
843
1361
|
/**
|
|
@@ -865,72 +1383,4 @@ export class ServiceExecutor {
|
|
|
865
1383
|
const status = this.getStatus(serviceType)
|
|
866
1384
|
return status === 'starting' || status === 'ready'
|
|
867
1385
|
}
|
|
868
|
-
|
|
869
|
-
/**
|
|
870
|
-
* Shutdown all services and release all ports back to pool.
|
|
871
|
-
* Called on session close.
|
|
872
|
-
*/
|
|
873
|
-
async shutdown(): Promise<void> {
|
|
874
|
-
// Nothing queued may spawn after the executor is gone.
|
|
875
|
-
for (const serviceType of [...this.restartTimers.keys()]) this.cancelPendingRestart(serviceType)
|
|
876
|
-
this.restartRetries.clear()
|
|
877
|
-
|
|
878
|
-
const promises: Promise<void>[] = []
|
|
879
|
-
|
|
880
|
-
for (const [serviceType, entry] of this.services) {
|
|
881
|
-
if (entry.status === 'starting' || entry.status === 'ready' || entry.status === 'paused') {
|
|
882
|
-
const gracefulStopMs = entry.config.gracefulStopMs ?? 5000
|
|
883
|
-
|
|
884
|
-
const killPromise = new Promise<void>((resolve) => {
|
|
885
|
-
try {
|
|
886
|
-
process.kill(-entry.pid, 'SIGTERM')
|
|
887
|
-
} catch {
|
|
888
|
-
resolve()
|
|
889
|
-
return
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
const killTimeout = setTimeout(() => {
|
|
893
|
-
try {
|
|
894
|
-
process.kill(-entry.pid, 'SIGKILL')
|
|
895
|
-
} catch {
|
|
896
|
-
// Already gone
|
|
897
|
-
}
|
|
898
|
-
resolve()
|
|
899
|
-
}, gracefulStopMs)
|
|
900
|
-
|
|
901
|
-
const checkInterval = setInterval(() => {
|
|
902
|
-
try {
|
|
903
|
-
process.kill(entry.pid, 0)
|
|
904
|
-
} catch {
|
|
905
|
-
clearInterval(checkInterval)
|
|
906
|
-
clearTimeout(killTimeout)
|
|
907
|
-
resolve()
|
|
908
|
-
}
|
|
909
|
-
}, 200)
|
|
910
|
-
})
|
|
911
|
-
|
|
912
|
-
promises.push(killPromise)
|
|
913
|
-
entry.status = 'stopping'
|
|
914
|
-
this.logger.info('Shutting down service', { serviceType })
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
await Promise.all(promises)
|
|
919
|
-
this.services.clear()
|
|
920
|
-
|
|
921
|
-
// Drain all waiters with error
|
|
922
|
-
for (const [serviceType, pending] of this.waiters) {
|
|
923
|
-
for (const waiter of pending) {
|
|
924
|
-
clearTimeout(waiter.timer)
|
|
925
|
-
waiter.resolve(Err({ message: `Service '${serviceType}' shut down`, recoverable: false }))
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
this.waiters.clear()
|
|
929
|
-
|
|
930
|
-
// Release all allocated ports back to pool
|
|
931
|
-
for (const port of this.allocatedPorts.values()) {
|
|
932
|
-
this.portPool.release(port)
|
|
933
|
-
}
|
|
934
|
-
this.allocatedPorts.clear()
|
|
935
|
-
}
|
|
936
1386
|
}
|