@roj-ai/sdk 0.1.36 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +25 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -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/index.d.ts +1 -0
- package/dist/plugins/git-status/index.d.ts.map +1 -1
- package/dist/plugins/git-status/plugin.d.ts +38 -7
- package/dist/plugins/git-status/plugin.d.ts.map +1 -1
- package/dist/plugins/git-status/plugin.js +169 -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 +32 -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/index.ts +1 -0
- package/src/plugins/git-status/plugin.ts +211 -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
|
@@ -46,19 +46,43 @@ const PORT_CONFLICT_PATTERN = /EADDRINUSE|address already in use/i;
|
|
|
46
46
|
* `failed` instead of looping forever.
|
|
47
47
|
*/
|
|
48
48
|
const MAX_PORT_CONFLICT_RETRIES = 3;
|
|
49
|
+
/**
|
|
50
|
+
* Deadline for embedder callbacks and PID-registry writes inside a transition.
|
|
51
|
+
*
|
|
52
|
+
* A transition holds the runtime lease that idle eviction waits on, so a resolver
|
|
53
|
+
* or a registry write that never settles pins the session resident forever and
|
|
54
|
+
* defeats the bound the lease exists to enforce. None of them is meant to take
|
|
55
|
+
* 30 s — the deadline only turns "wedged" into "failed".
|
|
56
|
+
*/
|
|
57
|
+
const DEFAULT_SERVICE_HOOK_TIMEOUT_MS = 30_000;
|
|
49
58
|
/** `restartPolicy` defaults — see ServiceConfig.restartPolicy for the rationale. */
|
|
50
59
|
const DEFAULT_MAX_RESTART_RETRIES = 3;
|
|
51
60
|
const DEFAULT_RESTART_DELAY_MS = 1000;
|
|
52
61
|
const DEFAULT_MAX_RESTART_DELAY_MS = 30_000;
|
|
53
62
|
const DEFAULT_RESTART_HEALTHY_AFTER_MS = 60_000;
|
|
63
|
+
let serviceExecutorObserverForTesting;
|
|
64
|
+
/** Observe executor creation in integration tests without exposing lifecycle methods as plugin API. */
|
|
65
|
+
export function setServiceExecutorObserverForTesting(observer) {
|
|
66
|
+
const previous = serviceExecutorObserverForTesting;
|
|
67
|
+
serviceExecutorObserverForTesting = observer;
|
|
68
|
+
return () => {
|
|
69
|
+
if (serviceExecutorObserverForTesting === observer)
|
|
70
|
+
serviceExecutorObserverForTesting = previous;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
54
73
|
export class ServiceExecutor {
|
|
55
74
|
services = new Map();
|
|
56
75
|
allocatedPorts = new Map();
|
|
57
76
|
waiters = new Map();
|
|
58
77
|
/** Per-type lock collapsing concurrent start() calls onto a single in-flight start. */
|
|
59
78
|
startInFlight = new Map();
|
|
79
|
+
/** Per-type stop operation awaited by runtime disposal. */
|
|
80
|
+
stopInFlight = new Map();
|
|
81
|
+
/** Registry deletions that must settle before a replacement can record its PID. */
|
|
82
|
+
pidForgets = new Map();
|
|
60
83
|
/** Per-type counter bounding automatic EADDRINUSE port re-allocation retries. */
|
|
61
84
|
portConflictRetries = new Map();
|
|
85
|
+
portConflictTimers = new Map();
|
|
62
86
|
/** Per-type counter bounding `restartPolicy` revivals after an unexpected exit. */
|
|
63
87
|
restartRetries = new Map();
|
|
64
88
|
/** Pending `restartPolicy` timers, cancelled when someone stops or starts the service by hand. */
|
|
@@ -68,14 +92,44 @@ export class ServiceExecutor {
|
|
|
68
92
|
fs;
|
|
69
93
|
processRunner;
|
|
70
94
|
pidRegistry;
|
|
95
|
+
killProcess;
|
|
96
|
+
hookTimeoutMs;
|
|
97
|
+
closing = false;
|
|
98
|
+
closePromise;
|
|
71
99
|
/** Optional callback invoked on every service status change */
|
|
72
100
|
onStatusChanged;
|
|
101
|
+
/** Invoked after every owned start attempt, including deferred retries. */
|
|
102
|
+
onStartSettled;
|
|
73
103
|
constructor(logger, portPool, deps) {
|
|
74
104
|
this.logger = logger;
|
|
75
105
|
this.portPool = portPool;
|
|
76
106
|
this.fs = deps.fs;
|
|
77
107
|
this.processRunner = deps.process;
|
|
78
108
|
this.pidRegistry = deps.pidRegistry;
|
|
109
|
+
this.killProcess = deps.kill ?? process.kill;
|
|
110
|
+
this.hookTimeoutMs = deps.hookTimeoutMs ?? DEFAULT_SERVICE_HOOK_TIMEOUT_MS;
|
|
111
|
+
serviceExecutorObserverForTesting?.(this);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Reject when `operation` outlives {@link DEFAULT_SERVICE_HOOK_TIMEOUT_MS}.
|
|
115
|
+
*
|
|
116
|
+
* The operation itself cannot be cancelled — an embedder callback keeps running —
|
|
117
|
+
* but the transition stops waiting on it, which is what releases the lease.
|
|
118
|
+
*/
|
|
119
|
+
async withDeadline(label, operation) {
|
|
120
|
+
let timer;
|
|
121
|
+
try {
|
|
122
|
+
return await Promise.race([
|
|
123
|
+
(async () => await operation())(),
|
|
124
|
+
new Promise((_, reject) => {
|
|
125
|
+
timer = setTimeout(() => reject(new Error(`${label} did not settle within ${this.hookTimeoutMs}ms`)), this.hookTimeoutMs);
|
|
126
|
+
}),
|
|
127
|
+
]);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
if (timer)
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
}
|
|
79
133
|
}
|
|
80
134
|
notifyStatusChanged(sessionId, serviceType, status, details = {}) {
|
|
81
135
|
this.onStatusChanged?.(sessionId, serviceType, status, details);
|
|
@@ -87,7 +141,7 @@ export class ServiceExecutor {
|
|
|
87
141
|
if (!config.availableWhen)
|
|
88
142
|
return Ok(true);
|
|
89
143
|
try {
|
|
90
|
-
return Ok(await config.availableWhen({ sessionId: String(sessionId), workspaceDir }));
|
|
144
|
+
return Ok(await this.withDeadline(`Service '${config.type}' availability check`, () => config.availableWhen?.({ sessionId: String(sessionId), workspaceDir }) ?? true));
|
|
91
145
|
}
|
|
92
146
|
catch (error) {
|
|
93
147
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -103,7 +157,8 @@ export class ServiceExecutor {
|
|
|
103
157
|
recoverable: true,
|
|
104
158
|
});
|
|
105
159
|
}
|
|
106
|
-
const
|
|
160
|
+
const cwdResolver = config.cwd;
|
|
161
|
+
const raw = await this.withDeadline(`Service '${config.type}' cwd resolver`, () => cwdResolver({ sessionId: String(sessionId), workspaceDir }));
|
|
107
162
|
return Ok(resolve(workspaceDir, raw));
|
|
108
163
|
}
|
|
109
164
|
if (config.cwd !== undefined) {
|
|
@@ -172,6 +227,8 @@ export class ServiceExecutor {
|
|
|
172
227
|
* Deduping here guarantees exactly one process per type.
|
|
173
228
|
*/
|
|
174
229
|
async start(config, sessionId, workspaceDir, preferredPort) {
|
|
230
|
+
if (this.closing)
|
|
231
|
+
return Err({ message: 'Service executor is closing', recoverable: false });
|
|
175
232
|
const inFlight = this.startInFlight.get(config.type);
|
|
176
233
|
if (inFlight)
|
|
177
234
|
return inFlight;
|
|
@@ -182,16 +239,43 @@ export class ServiceExecutor {
|
|
|
182
239
|
}
|
|
183
240
|
finally {
|
|
184
241
|
this.startInFlight.delete(config.type);
|
|
242
|
+
this.onStartSettled?.(config.type);
|
|
185
243
|
}
|
|
186
244
|
}
|
|
187
245
|
async startInternal(config, sessionId, workspaceDir, preferredPort) {
|
|
246
|
+
if (this.closing)
|
|
247
|
+
return Err({ message: 'Service executor is closing', recoverable: false });
|
|
188
248
|
// This start supersedes any revival the policy had queued.
|
|
189
249
|
this.cancelPendingRestart(config.type);
|
|
190
250
|
const existing = this.services.get(config.type);
|
|
191
251
|
if (existing && (existing.status === 'starting' || existing.status === 'ready')) {
|
|
192
252
|
return Ok(undefined);
|
|
193
253
|
}
|
|
254
|
+
if (existing?.status === 'stopping') {
|
|
255
|
+
return Err({ message: `Service '${config.type}' is stopping, cannot start`, recoverable: true });
|
|
256
|
+
}
|
|
257
|
+
if (existing) {
|
|
258
|
+
// Whatever sits under this type is about to be overwritten — in this map and in
|
|
259
|
+
// the pid registry, both keyed by (session, type) — and to lose its port to the
|
|
260
|
+
// process spawned below, since a restart deliberately reuses it. A `failed`
|
|
261
|
+
// entry can still own a live process group, so reclaim it before its handle is
|
|
262
|
+
// dropped or nothing will ever be able to find it again.
|
|
263
|
+
const reaped = await this.reapEntry(existing, 'superseded by a new start');
|
|
264
|
+
if (!reaped.ok) {
|
|
265
|
+
this.logger.warn('Could not reclaim the superseded service process group', {
|
|
266
|
+
serviceType: config.type,
|
|
267
|
+
pid: existing.pid,
|
|
268
|
+
error: reaped.error.message,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
await existing.forgetPid();
|
|
272
|
+
}
|
|
273
|
+
await this.pidForgets.get(config.type);
|
|
274
|
+
if (this.closing)
|
|
275
|
+
return Err({ message: 'Service executor is closing', recoverable: false });
|
|
194
276
|
const availability = await this.isAvailable(config, sessionId, workspaceDir);
|
|
277
|
+
if (this.closing)
|
|
278
|
+
return Err({ message: 'Service executor is closing', recoverable: false });
|
|
195
279
|
if (!availability.ok)
|
|
196
280
|
return availability;
|
|
197
281
|
if (!availability.value) {
|
|
@@ -219,9 +303,29 @@ export class ServiceExecutor {
|
|
|
219
303
|
return cwdResult;
|
|
220
304
|
}
|
|
221
305
|
const cwd = cwdResult.value;
|
|
222
|
-
|
|
306
|
+
if (this.closing) {
|
|
307
|
+
if (allocatedNow) {
|
|
308
|
+
this.allocatedPorts.delete(config.type);
|
|
309
|
+
this.portPool.release(port);
|
|
310
|
+
}
|
|
311
|
+
return Err({ message: 'Service executor is closing', recoverable: false });
|
|
312
|
+
}
|
|
313
|
+
const logBufferSize = Math.max(1, config.logBufferSize ?? 200);
|
|
223
314
|
const startupTimeoutMs = config.startupTimeoutMs ?? 30_000;
|
|
224
315
|
const readyRegex = config.readyPattern ? new RegExp(config.readyPattern) : undefined;
|
|
316
|
+
const logs = new RingBuffer(logBufferSize);
|
|
317
|
+
const maxDiagnosticLength = 16_384;
|
|
318
|
+
const startTime = Date.now();
|
|
319
|
+
let portConflictDetected = false;
|
|
320
|
+
let setupComplete = false;
|
|
321
|
+
let retryPortConflictDuringSetup = false;
|
|
322
|
+
let readyDetectedDuringSetup = false;
|
|
323
|
+
let readyLineDuringSetup;
|
|
324
|
+
let stdoutPartial = '';
|
|
325
|
+
let stderrPartial = '';
|
|
326
|
+
let outputSequence = 0;
|
|
327
|
+
let stdoutPartialSequence = 0;
|
|
328
|
+
let stderrPartialSequence = 0;
|
|
225
329
|
const startArgs = {
|
|
226
330
|
port,
|
|
227
331
|
sessionId: String(sessionId),
|
|
@@ -231,12 +335,14 @@ export class ServiceExecutor {
|
|
|
231
335
|
let command;
|
|
232
336
|
let serviceEnv;
|
|
233
337
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
338
|
+
const commandResolver = config.command;
|
|
339
|
+
command = typeof commandResolver === 'function'
|
|
340
|
+
? await this.withDeadline(`Service '${config.type}' command resolver`, () => commandResolver(startArgs))
|
|
341
|
+
: commandResolver;
|
|
342
|
+
const envResolver = config.env;
|
|
343
|
+
serviceEnv = typeof envResolver === 'function'
|
|
344
|
+
? await this.withDeadline(`Service '${config.type}' env resolver`, () => envResolver(startArgs))
|
|
345
|
+
: envResolver;
|
|
240
346
|
}
|
|
241
347
|
catch (error) {
|
|
242
348
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -248,6 +354,13 @@ export class ServiceExecutor {
|
|
|
248
354
|
this.notifyStatusChanged(sessionId, config.type, 'failed', { port, cwd, error: errorMessage });
|
|
249
355
|
return Err({ message: errorMessage, recoverable: true });
|
|
250
356
|
}
|
|
357
|
+
if (this.closing) {
|
|
358
|
+
if (allocatedNow) {
|
|
359
|
+
this.allocatedPorts.delete(config.type);
|
|
360
|
+
this.portPool.release(port);
|
|
361
|
+
}
|
|
362
|
+
return Err({ message: 'Service executor is closing', recoverable: false });
|
|
363
|
+
}
|
|
251
364
|
const shell = process.platform === 'win32' ? 'cmd.exe' : '/bin/sh';
|
|
252
365
|
const shellFlag = process.platform === 'win32' ? '/c' : '-c';
|
|
253
366
|
const child = this.processRunner.spawn(shell, [shellFlag, command], {
|
|
@@ -263,10 +376,18 @@ export class ServiceExecutor {
|
|
|
263
376
|
const current = this.services.get(config.type);
|
|
264
377
|
if (current && current.process === child && current.status !== 'stopped' && current.status !== 'failed') {
|
|
265
378
|
current.status = 'failed';
|
|
379
|
+
// The pid rides along on purpose — see the startup timeout below.
|
|
266
380
|
this.notifyStatusChanged(sessionId, config.type, 'failed', {
|
|
267
381
|
error: error.message,
|
|
268
382
|
cwd,
|
|
269
383
|
command,
|
|
384
|
+
pid: current.pid,
|
|
385
|
+
pidStartTime: current.pidStartTime,
|
|
386
|
+
});
|
|
387
|
+
void this.reapEntry(current, 'process error').then((reaped) => {
|
|
388
|
+
if (!reaped.ok) {
|
|
389
|
+
this.logger.error('Could not kill errored service process group', reaped.error, { serviceType: config.type, pid: current.pid });
|
|
390
|
+
}
|
|
270
391
|
});
|
|
271
392
|
}
|
|
272
393
|
});
|
|
@@ -278,29 +399,137 @@ export class ServiceExecutor {
|
|
|
278
399
|
this.notifyStatusChanged(sessionId, config.type, 'failed', { port, cwd, command, error: 'Failed to spawn process' });
|
|
279
400
|
return Err({ message: 'Failed to spawn service process', recoverable: true });
|
|
280
401
|
}
|
|
402
|
+
// Bind the narrowed pid: the deferred registry call below cannot carry the
|
|
403
|
+
// narrowing across the closure boundary.
|
|
404
|
+
const pid = child.pid;
|
|
405
|
+
const matches = (regex, content) => {
|
|
406
|
+
regex.lastIndex = 0;
|
|
407
|
+
const matched = regex.test(content);
|
|
408
|
+
regex.lastIndex = 0;
|
|
409
|
+
return matched;
|
|
410
|
+
};
|
|
411
|
+
const recordDiagnostic = (line, prefix) => {
|
|
412
|
+
const maxContentLength = Math.max(0, maxDiagnosticLength - prefix.length);
|
|
413
|
+
const bounded = line.length > maxContentLength ? line.slice(-maxContentLength) : line;
|
|
414
|
+
logs.push(`${prefix}${bounded}`);
|
|
415
|
+
};
|
|
416
|
+
// Detection sees the full incoming content before diagnostics are truncated.
|
|
417
|
+
const appendOutput = (partial, data, prefix) => {
|
|
418
|
+
const combined = partial + data.toString();
|
|
419
|
+
if (matches(PORT_CONFLICT_PATTERN, combined))
|
|
420
|
+
portConflictDetected = true;
|
|
421
|
+
const lines = combined.split('\n');
|
|
422
|
+
const nextPartial = lines.pop() ?? '';
|
|
423
|
+
const readyMatched = readyRegex ? matches(readyRegex, combined) || lines.some((line) => matches(readyRegex, line)) : false;
|
|
424
|
+
if (readyMatched && !setupComplete) {
|
|
425
|
+
readyDetectedDuringSetup = true;
|
|
426
|
+
readyLineDuringSetup = combined.slice(-maxDiagnosticLength);
|
|
427
|
+
}
|
|
428
|
+
for (const line of lines)
|
|
429
|
+
recordDiagnostic(line, prefix);
|
|
430
|
+
if (setupComplete) {
|
|
431
|
+
const current = this.services.get(config.type);
|
|
432
|
+
if (current?.process === child && current.status === 'starting') {
|
|
433
|
+
this.logger.debug('Service output', { serviceType: config.type });
|
|
434
|
+
if (readyMatched)
|
|
435
|
+
markReady(combined.slice(-maxDiagnosticLength));
|
|
436
|
+
}
|
|
437
|
+
void checkReadyWhen();
|
|
438
|
+
}
|
|
439
|
+
// Retain enough suffix for markers split across chunks, never an entire line.
|
|
440
|
+
return nextPartial.length > maxDiagnosticLength ? nextPartial.slice(-maxDiagnosticLength) : nextPartial;
|
|
441
|
+
};
|
|
442
|
+
const onStdout = (data) => {
|
|
443
|
+
stdoutPartial = appendOutput(stdoutPartial, data, '');
|
|
444
|
+
stdoutPartialSequence = stdoutPartial ? ++outputSequence : 0;
|
|
445
|
+
};
|
|
446
|
+
const onStderr = (data) => {
|
|
447
|
+
stderrPartial = appendOutput(stderrPartial, data, '[stderr] ');
|
|
448
|
+
stderrPartialSequence = stderrPartial ? ++outputSequence : 0;
|
|
449
|
+
};
|
|
450
|
+
let exitDuringSetup;
|
|
451
|
+
let forgetPromise;
|
|
452
|
+
const forgetPid = () => {
|
|
453
|
+
if (forgetPromise)
|
|
454
|
+
return forgetPromise;
|
|
455
|
+
const previous = this.pidForgets.get(config.type);
|
|
456
|
+
const runForget = () => this.withDeadline(`Service '${config.type}' PID forget`, () => this.pidRegistry?.forget(String(sessionId), config.type) ?? Promise.resolve());
|
|
457
|
+
const operation = previous ? previous.then(runForget, runForget) : runForget();
|
|
458
|
+
forgetPromise = operation;
|
|
459
|
+
this.pidForgets.set(config.type, operation);
|
|
460
|
+
void operation.then(() => {
|
|
461
|
+
if (this.pidForgets.get(config.type) === operation)
|
|
462
|
+
this.pidForgets.delete(config.type);
|
|
463
|
+
}, () => {
|
|
464
|
+
if (this.pidForgets.get(config.type) === operation)
|
|
465
|
+
this.pidForgets.delete(config.type);
|
|
466
|
+
if (forgetPromise === operation)
|
|
467
|
+
forgetPromise = undefined;
|
|
468
|
+
});
|
|
469
|
+
return operation;
|
|
470
|
+
};
|
|
471
|
+
const bufferClose = (code) => {
|
|
472
|
+
exitDuringSetup = { code };
|
|
473
|
+
};
|
|
474
|
+
child.stdout?.on('data', onStdout);
|
|
475
|
+
child.stderr?.on('data', onStderr);
|
|
476
|
+
child.on('close', bufferClose);
|
|
281
477
|
// Capture start time immediately so a later PID-reuse check can distinguish
|
|
282
478
|
// "our process" from "an unrelated process that grabbed this PID after ours died"
|
|
283
479
|
const pidStartTime = await getProcessStartTime(this.fs, child.pid);
|
|
284
480
|
// Durably record the process so a future agent can reap it if we die before the
|
|
285
481
|
// `close` handler gets to forget it. The session projection cannot serve that
|
|
286
482
|
// purpose — see ServicePidRegistry.
|
|
287
|
-
|
|
483
|
+
try {
|
|
484
|
+
await this.withDeadline(`Service '${config.type}' PID record`, () => this.pidRegistry?.record({ sessionId: String(sessionId), serviceType: config.type, pid, pidStartTime, command })
|
|
485
|
+
?? Promise.resolve());
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
// The child is already running but unrecorded, so nothing could reap it
|
|
489
|
+
// after a crash — tear it down here rather than leak it.
|
|
490
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
491
|
+
const errorMessage = `Service '${config.type}' could not record its PID: ${message}`;
|
|
492
|
+
try {
|
|
493
|
+
this.killProcess(-child.pid, 'SIGKILL');
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
// The process may already be gone.
|
|
497
|
+
}
|
|
498
|
+
if (allocatedNow) {
|
|
499
|
+
this.allocatedPorts.delete(config.type);
|
|
500
|
+
this.portPool.release(port);
|
|
501
|
+
}
|
|
502
|
+
this.notifyStatusChanged(sessionId, config.type, 'failed', { port, cwd, command, error: errorMessage });
|
|
503
|
+
return Err({ message: errorMessage, recoverable: true });
|
|
504
|
+
}
|
|
505
|
+
if (this.closing) {
|
|
506
|
+
try {
|
|
507
|
+
this.killProcess(-child.pid, 'SIGKILL');
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
// The process may already be gone.
|
|
511
|
+
}
|
|
512
|
+
await forgetPid();
|
|
513
|
+
if (allocatedNow) {
|
|
514
|
+
this.allocatedPorts.delete(config.type);
|
|
515
|
+
this.portPool.release(port);
|
|
516
|
+
}
|
|
517
|
+
return Err({ message: 'Service executor is closing', recoverable: false });
|
|
518
|
+
}
|
|
288
519
|
// Emit starting event with PID, port, resolved cwd/command, and start time.
|
|
289
520
|
this.notifyStatusChanged(sessionId, config.type, 'starting', { port, pid: child.pid, pidStartTime, cwd, command });
|
|
290
|
-
const logs = new RingBuffer(logBufferSize);
|
|
291
|
-
const startTime = Date.now();
|
|
292
|
-
// Set when the child reports a port bind-conflict; read by the close handler
|
|
293
|
-
// to decide between a fresh-port retry and a terminal failure.
|
|
294
|
-
let portConflictDetected = false;
|
|
295
521
|
const entry = {
|
|
296
522
|
config,
|
|
297
523
|
process: child,
|
|
298
524
|
pid: child.pid,
|
|
525
|
+
pidStartTime,
|
|
299
526
|
status: 'starting',
|
|
300
527
|
port,
|
|
301
528
|
cwd,
|
|
302
529
|
command,
|
|
303
530
|
logs,
|
|
531
|
+
forgetPid,
|
|
532
|
+
exited: false,
|
|
304
533
|
};
|
|
305
534
|
this.services.set(config.type, entry);
|
|
306
535
|
let startupTimer;
|
|
@@ -359,24 +588,6 @@ export class ServiceExecutor {
|
|
|
359
588
|
readyCheckInFlight = false;
|
|
360
589
|
}
|
|
361
590
|
};
|
|
362
|
-
const processLine = (line) => {
|
|
363
|
-
logs.push(line);
|
|
364
|
-
if (PORT_CONFLICT_PATTERN.test(line)) {
|
|
365
|
-
portConflictDetected = true;
|
|
366
|
-
}
|
|
367
|
-
const current = this.services.get(config.type);
|
|
368
|
-
if (!current || current.process !== child)
|
|
369
|
-
return;
|
|
370
|
-
if (current.status === 'starting') {
|
|
371
|
-
this.logger.debug('Service output', { serviceType: config.type, line });
|
|
372
|
-
}
|
|
373
|
-
if (readyRegex && current.status === 'starting') {
|
|
374
|
-
if (readyRegex.test(line)) {
|
|
375
|
-
markReady(line);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
void checkReadyWhen();
|
|
379
|
-
};
|
|
380
591
|
// Startup timeout — mark as failed if not ready in time
|
|
381
592
|
if (readyRegex || config.readyWhen) {
|
|
382
593
|
startupTimer = setTimeout(() => {
|
|
@@ -387,19 +598,22 @@ export class ServiceExecutor {
|
|
|
387
598
|
const errorMsg = `Service startup timed out after ${startupTimeoutMs}ms`;
|
|
388
599
|
this.logger.error(errorMsg, undefined, { serviceType: config.type });
|
|
389
600
|
clearReadinessTimers();
|
|
601
|
+
// The pid rides along on purpose: should this agent die before the reap
|
|
602
|
+
// below completes, the recorded pid is all a later boot has to find the
|
|
603
|
+
// survivor. The child's exit takes the claim away again.
|
|
390
604
|
this.notifyStatusChanged(sessionId, config.type, 'failed', {
|
|
391
605
|
port: current.port,
|
|
392
606
|
cwd: current.cwd,
|
|
393
607
|
command: current.command,
|
|
394
608
|
error: errorMsg,
|
|
609
|
+
pid: current.pid,
|
|
610
|
+
pidStartTime: current.pidStartTime,
|
|
611
|
+
});
|
|
612
|
+
void this.reapEntry(current, 'startup timeout').then((reaped) => {
|
|
613
|
+
if (!reaped.ok) {
|
|
614
|
+
this.logger.error('Could not kill timed-out service process group', reaped.error, { serviceType: config.type, pid: current.pid });
|
|
615
|
+
}
|
|
395
616
|
});
|
|
396
|
-
// Kill the timed-out process
|
|
397
|
-
try {
|
|
398
|
-
process.kill(-current.pid, 'SIGKILL');
|
|
399
|
-
}
|
|
400
|
-
catch {
|
|
401
|
-
// Already gone
|
|
402
|
-
}
|
|
403
617
|
}, startupTimeoutMs);
|
|
404
618
|
}
|
|
405
619
|
if (config.readyWhen) {
|
|
@@ -407,52 +621,38 @@ export class ServiceExecutor {
|
|
|
407
621
|
readyCheckTimer = setInterval(() => {
|
|
408
622
|
void checkReadyWhen();
|
|
409
623
|
}, intervalMs);
|
|
410
|
-
void checkReadyWhen();
|
|
411
624
|
}
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
let stderrPartial = '';
|
|
423
|
-
child.stderr?.on('data', (data) => {
|
|
424
|
-
stderrPartial += data.toString();
|
|
425
|
-
const lines = stderrPartial.split('\n');
|
|
426
|
-
stderrPartial = lines.pop();
|
|
427
|
-
for (const line of lines) {
|
|
428
|
-
processLine(`[stderr] ${line}`);
|
|
429
|
-
}
|
|
430
|
-
});
|
|
431
|
-
// Handle unexpected exit
|
|
432
|
-
child.on('close', (code) => {
|
|
625
|
+
// Handle unexpected exit. Named and guarded because it also has to be
|
|
626
|
+
// replayable — see the exit-during-setup check after markReady() below.
|
|
627
|
+
let closeHandled = false;
|
|
628
|
+
const handleClose = (code) => {
|
|
629
|
+
if (closeHandled)
|
|
630
|
+
return;
|
|
631
|
+
closeHandled = true;
|
|
632
|
+
entry.exited = true;
|
|
433
633
|
clearReadinessTimers();
|
|
434
|
-
//
|
|
435
|
-
void
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
processLine(`[stderr] ${stderrPartial}`);
|
|
443
|
-
stderrPartial = '';
|
|
634
|
+
// A replacement must not be recorded until this deletion finishes.
|
|
635
|
+
void forgetPid();
|
|
636
|
+
const partials = [
|
|
637
|
+
{ content: stdoutPartial, prefix: '', sequence: stdoutPartialSequence },
|
|
638
|
+
{ content: stderrPartial, prefix: '[stderr] ', sequence: stderrPartialSequence },
|
|
639
|
+
];
|
|
640
|
+
for (const partial of partials.filter(({ content }) => content).sort((left, right) => left.sequence - right.sequence)) {
|
|
641
|
+
recordDiagnostic(partial.content, partial.prefix);
|
|
444
642
|
}
|
|
643
|
+
stdoutPartial = '';
|
|
644
|
+
stderrPartial = '';
|
|
445
645
|
const current = this.services.get(config.type);
|
|
446
646
|
if (!current || current.process !== child)
|
|
447
647
|
return;
|
|
448
648
|
if (current.status === 'stopping') {
|
|
449
|
-
// Expected stop
|
|
649
|
+
// Expected stop — the child's exit, not stopInternal, is what publishes it.
|
|
450
650
|
current.status = 'stopped';
|
|
451
|
-
this.notifyStatusChanged(sessionId, config.type, 'stopped');
|
|
651
|
+
this.notifyStatusChanged(sessionId, config.type, 'stopped', { stoppedBy: current.stopSource ?? 'agent' });
|
|
452
652
|
}
|
|
453
653
|
else if (current.status === 'starting' || current.status === 'ready') {
|
|
454
654
|
const retries = this.portConflictRetries.get(config.type) ?? 0;
|
|
455
|
-
if (portConflictDetected && retries < MAX_PORT_CONFLICT_RETRIES) {
|
|
655
|
+
if (!this.closing && current.status === 'starting' && portConflictDetected && retries < MAX_PORT_CONFLICT_RETRIES) {
|
|
456
656
|
// The chosen port is held by a foreign process (e.g. a service
|
|
457
657
|
// leaked from another session, or a survivor of a previous boot).
|
|
458
658
|
// Re-allocate a fresh port and retry so the service recovers on a
|
|
@@ -469,7 +669,18 @@ export class ServiceExecutor {
|
|
|
469
669
|
});
|
|
470
670
|
this.allocatedPorts.delete(config.type);
|
|
471
671
|
this.services.delete(config.type);
|
|
472
|
-
|
|
672
|
+
if (!setupComplete) {
|
|
673
|
+
retryPortConflictDuringSetup = true;
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
const retryTimer = setTimeout(() => {
|
|
677
|
+
this.portConflictTimers.delete(config.type);
|
|
678
|
+
if (!this.closing)
|
|
679
|
+
void this.start(config, sessionId, workspaceDir);
|
|
680
|
+
}, 0);
|
|
681
|
+
retryTimer.unref?.();
|
|
682
|
+
this.portConflictTimers.set(config.type, retryTimer);
|
|
683
|
+
}
|
|
473
684
|
return;
|
|
474
685
|
}
|
|
475
686
|
// Unexpected exit
|
|
@@ -491,7 +702,16 @@ export class ServiceExecutor {
|
|
|
491
702
|
code,
|
|
492
703
|
});
|
|
493
704
|
}
|
|
494
|
-
|
|
705
|
+
else if (current.status === 'failed') {
|
|
706
|
+
// The process this entry still claimed is now confirmed gone — a startup
|
|
707
|
+
// timeout or a process error published the pid without being able to
|
|
708
|
+
// confirm the kill. Take the claim away so no later boot goes looking for
|
|
709
|
+
// it; the status and the error that explain the failure stay as they are.
|
|
710
|
+
this.notifyStatusChanged(sessionId, config.type, 'failed');
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
child.on('close', handleClose);
|
|
714
|
+
child.off('close', bufferClose);
|
|
495
715
|
this.logger.info('Service starting', {
|
|
496
716
|
serviceType: config.type,
|
|
497
717
|
pid: child.pid,
|
|
@@ -501,10 +721,37 @@ export class ServiceExecutor {
|
|
|
501
721
|
readyPattern: config.readyPattern,
|
|
502
722
|
startupTimeoutMs,
|
|
503
723
|
});
|
|
504
|
-
//
|
|
505
|
-
|
|
724
|
+
// A service that died inside the bookkeeping above closed while only the
|
|
725
|
+
// setup close collector was listening, so replay that close now.
|
|
726
|
+
// `alreadyReaped` covers the in-between state: the exit is recorded but
|
|
727
|
+
// 'close' has not fired yet because it waits for the stdio EOF. The listener
|
|
728
|
+
// above is guaranteed to receive it, so calling handleClose here would only
|
|
729
|
+
// drop the tail of the log the failure has to explain — while marking such a
|
|
730
|
+
// child ready would be a lie.
|
|
731
|
+
const alreadyReaped = child.exitCode != null || child.signalCode != null;
|
|
732
|
+
if (exitDuringSetup) {
|
|
733
|
+
handleClose(exitDuringSetup.code);
|
|
734
|
+
}
|
|
735
|
+
setupComplete = true;
|
|
736
|
+
if (retryPortConflictDuringSetup) {
|
|
737
|
+
await forgetPid();
|
|
738
|
+
if (this.closing)
|
|
739
|
+
return Err({ message: 'Service executor is closing', recoverable: false });
|
|
740
|
+
return this.startInternal(config, sessionId, workspaceDir);
|
|
741
|
+
}
|
|
742
|
+
if (exitDuringSetup || alreadyReaped) {
|
|
743
|
+
return Ok(undefined);
|
|
744
|
+
}
|
|
745
|
+
if (readyDetectedDuringSetup) {
|
|
746
|
+
markReady(readyLineDuringSetup);
|
|
747
|
+
}
|
|
748
|
+
else if (!readyRegex && !config.readyWhen) {
|
|
749
|
+
// If no ready condition is configured, a live child is ready immediately.
|
|
506
750
|
markReady();
|
|
507
751
|
}
|
|
752
|
+
else if (config.readyWhen) {
|
|
753
|
+
void checkReadyWhen();
|
|
754
|
+
}
|
|
508
755
|
return Ok(undefined);
|
|
509
756
|
}
|
|
510
757
|
/**
|
|
@@ -515,6 +762,8 @@ export class ServiceExecutor {
|
|
|
515
762
|
* change, or undefined when the service is left failed for good.
|
|
516
763
|
*/
|
|
517
764
|
scheduleRestart(config, sessionId, workspaceDir, uptimeMs, exitCode) {
|
|
765
|
+
if (this.closing)
|
|
766
|
+
return undefined;
|
|
518
767
|
const policy = config.restartPolicy;
|
|
519
768
|
if (!policy)
|
|
520
769
|
return undefined;
|
|
@@ -546,7 +795,8 @@ export class ServiceExecutor {
|
|
|
546
795
|
});
|
|
547
796
|
const timer = setTimeout(() => {
|
|
548
797
|
this.restartTimers.delete(config.type);
|
|
549
|
-
|
|
798
|
+
if (!this.closing)
|
|
799
|
+
void this.start(config, sessionId, workspaceDir);
|
|
550
800
|
}, delayMs);
|
|
551
801
|
// A pending revival must not keep the process alive on its own.
|
|
552
802
|
timer.unref?.();
|
|
@@ -562,69 +812,379 @@ export class ServiceExecutor {
|
|
|
562
812
|
this.restartTimers.delete(serviceType);
|
|
563
813
|
return true;
|
|
564
814
|
}
|
|
565
|
-
/**
|
|
815
|
+
/**
|
|
816
|
+
* Whether the service is down but coming back on its own — a `restartPolicy`
|
|
817
|
+
* revival or a queued port-conflict retry. Both are timers this executor
|
|
818
|
+
* owns, so both need the runtime to outlive the call that armed them.
|
|
819
|
+
*/
|
|
566
820
|
hasScheduledRestart(serviceType) {
|
|
567
|
-
return this.restartTimers.has(serviceType);
|
|
821
|
+
return this.restartTimers.has(serviceType) || this.portConflictTimers.has(serviceType);
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Stop accepting work and drain all runtime-owned processes and timers.
|
|
825
|
+
*
|
|
826
|
+
* `reason: 'evicted'` only parks the session: every service keeps its full
|
|
827
|
+
* graceful window and is recorded as stopped by the runtime, which is what
|
|
828
|
+
* lets autoStart bring it back once the runtime is rebuilt. The other
|
|
829
|
+
* reasons end the session, so a process group that ignores SIGTERM is taken
|
|
830
|
+
* down without waiting.
|
|
831
|
+
*/
|
|
832
|
+
close(sessionId, reason = 'closed') {
|
|
833
|
+
this.closing = true;
|
|
834
|
+
this.closePromise ??= this.performClose(sessionId, reason).finally(() => {
|
|
835
|
+
this.onStatusChanged = undefined;
|
|
836
|
+
this.onStartSettled = undefined;
|
|
837
|
+
});
|
|
838
|
+
return this.closePromise;
|
|
839
|
+
}
|
|
840
|
+
async performClose(sessionId, reason) {
|
|
841
|
+
this.cancelPortConflictTimers();
|
|
842
|
+
await Promise.allSettled([...this.startInFlight.values()]);
|
|
843
|
+
this.cancelPortConflictTimers();
|
|
844
|
+
const concurrentStopTypes = new Set(this.stopInFlight.keys());
|
|
845
|
+
await Promise.allSettled([...this.stopInFlight.values()]);
|
|
846
|
+
// One service that cannot be signalled must not strand the others, and must not
|
|
847
|
+
// skip the tail — portPool is process-global, so a skipped release loses those
|
|
848
|
+
// ports for the life of the server.
|
|
849
|
+
const failures = [];
|
|
850
|
+
const parking = reason === 'evicted';
|
|
851
|
+
try {
|
|
852
|
+
for (const [serviceType, entry] of [...this.services]) {
|
|
853
|
+
if (entry.status === 'stopped')
|
|
854
|
+
continue;
|
|
855
|
+
if (entry.status === 'starting' || entry.status === 'ready' || entry.status === 'stopping' || this.restartTimers.has(serviceType)) {
|
|
856
|
+
const forcedGraceMs = parking ? entry.config.gracefulStopMs ?? 5000 : 0;
|
|
857
|
+
try {
|
|
858
|
+
if (concurrentStopTypes.has(serviceType)) {
|
|
859
|
+
await this.forceStopForClose(serviceType, entry, sessionId, forcedGraceMs);
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
const stopped = await this.stop(serviceType, sessionId, 'eviction');
|
|
863
|
+
if (!stopped.ok)
|
|
864
|
+
await this.forceStopForClose(serviceType, entry, sessionId, forcedGraceMs);
|
|
865
|
+
}
|
|
866
|
+
catch (error) {
|
|
867
|
+
failures.push(error);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
finally {
|
|
873
|
+
this.cancelAllDeferredStarts();
|
|
874
|
+
await Promise.allSettled([...this.pidForgets.values()]);
|
|
875
|
+
for (const port of this.allocatedPorts.values())
|
|
876
|
+
this.portPool.release(port);
|
|
877
|
+
this.allocatedPorts.clear();
|
|
878
|
+
this.services.clear();
|
|
879
|
+
for (const serviceType of this.waiters.keys())
|
|
880
|
+
this.resolveWaiters(serviceType, 'stopped');
|
|
881
|
+
}
|
|
882
|
+
// Still reject — a close that could not terminate a process is not a clean close.
|
|
883
|
+
if (failures.length === 1)
|
|
884
|
+
throw failures[0];
|
|
885
|
+
if (failures.length > 1) {
|
|
886
|
+
throw new AggregateError(failures, `Failed to stop ${failures.length} services during close`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
async forceStopForClose(serviceType, entry, sessionId, gracefulStopMs) {
|
|
890
|
+
const previousStatus = entry.status;
|
|
891
|
+
this.recordStopSource(entry, 'eviction');
|
|
892
|
+
if (entry.status !== 'stopping') {
|
|
893
|
+
entry.status = 'stopping';
|
|
894
|
+
this.notifyStatusChanged(sessionId, serviceType, 'stopping');
|
|
895
|
+
}
|
|
896
|
+
const stopped = await this.terminateProcessGroup(entry.pid, gracefulStopMs);
|
|
897
|
+
if (!stopped.ok) {
|
|
898
|
+
// Mirror stopInternal: no recovery path matches a stranded `stopping`, so a
|
|
899
|
+
// force-stop that failed has to leave a status the next runtime can reap.
|
|
900
|
+
if (entry.status === 'stopping' && previousStatus !== 'stopping') {
|
|
901
|
+
entry.status = previousStatus;
|
|
902
|
+
this.notifyStatusChanged(sessionId, serviceType, previousStatus, {
|
|
903
|
+
port: entry.port,
|
|
904
|
+
cwd: entry.cwd,
|
|
905
|
+
command: entry.command,
|
|
906
|
+
error: stopped.error.message,
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
throw stopped.error;
|
|
910
|
+
}
|
|
911
|
+
// The group is confirmed gone, so nothing may offer this pid up for a reap again.
|
|
912
|
+
entry.exited = true;
|
|
913
|
+
await entry.forgetPid();
|
|
914
|
+
if (this.services.get(serviceType) === entry) {
|
|
915
|
+
entry.status = 'stopped';
|
|
916
|
+
this.notifyStatusChanged(sessionId, serviceType, 'stopped', { stoppedBy: entry.stopSource ?? 'eviction' });
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
/** An agent stop stays the agent's decision even when a lifecycle stop is what finishes it. */
|
|
920
|
+
recordStopSource(entry, stoppedBy) {
|
|
921
|
+
if (stoppedBy === 'agent' || entry.stopSource === undefined)
|
|
922
|
+
entry.stopSource = stoppedBy;
|
|
923
|
+
}
|
|
924
|
+
cancelAllDeferredStarts() {
|
|
925
|
+
for (const timer of this.restartTimers.values())
|
|
926
|
+
clearTimeout(timer);
|
|
927
|
+
this.restartTimers.clear();
|
|
928
|
+
this.cancelPortConflictTimers();
|
|
929
|
+
}
|
|
930
|
+
cancelPortConflictTimers() {
|
|
931
|
+
for (const timer of this.portConflictTimers.values())
|
|
932
|
+
clearTimeout(timer);
|
|
933
|
+
this.portConflictTimers.clear();
|
|
934
|
+
}
|
|
935
|
+
errorWithContext(error, context) {
|
|
936
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
937
|
+
return new Error(`${context}: ${cause}`);
|
|
938
|
+
}
|
|
939
|
+
hasErrorCode(error, code) {
|
|
940
|
+
return error instanceof Error && 'code' in error && error.code === code;
|
|
941
|
+
}
|
|
942
|
+
async hasNonZombieProcessGroupMember(pid) {
|
|
943
|
+
if (process.platform !== 'linux')
|
|
944
|
+
return true;
|
|
945
|
+
let entries;
|
|
946
|
+
try {
|
|
947
|
+
entries = await this.fs.readdir('/proc');
|
|
948
|
+
}
|
|
949
|
+
catch {
|
|
950
|
+
return true;
|
|
951
|
+
}
|
|
952
|
+
let foundGroupMember = false;
|
|
953
|
+
for (const entry of entries) {
|
|
954
|
+
if (!/^\d+$/.test(entry))
|
|
955
|
+
continue;
|
|
956
|
+
try {
|
|
957
|
+
const stat = await this.fs.readFile(`/proc/${entry}/stat`, 'utf-8');
|
|
958
|
+
const rparen = stat.lastIndexOf(')');
|
|
959
|
+
if (rparen === -1)
|
|
960
|
+
continue;
|
|
961
|
+
const fields = stat.slice(rparen + 2).split(' ');
|
|
962
|
+
if (Number(fields[2]) !== pid)
|
|
963
|
+
continue;
|
|
964
|
+
foundGroupMember = true;
|
|
965
|
+
if (fields[0] !== 'Z')
|
|
966
|
+
return true;
|
|
967
|
+
}
|
|
968
|
+
catch {
|
|
969
|
+
// Processes can disappear while /proc is scanned.
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// A successful group probe with no readable member is still evidence of life.
|
|
973
|
+
return !foundGroupMember;
|
|
974
|
+
}
|
|
975
|
+
async probeProcessGroup(pid) {
|
|
976
|
+
try {
|
|
977
|
+
this.killProcess(-pid, 0);
|
|
978
|
+
}
|
|
979
|
+
catch (error) {
|
|
980
|
+
if (this.hasErrorCode(error, 'ESRCH'))
|
|
981
|
+
return { state: 'gone' };
|
|
982
|
+
if (this.hasErrorCode(error, 'EPERM'))
|
|
983
|
+
return { state: 'alive' };
|
|
984
|
+
return { state: 'error', error: this.errorWithContext(error, `Failed to probe process group ${pid}`) };
|
|
985
|
+
}
|
|
986
|
+
return await this.hasNonZombieProcessGroupMember(pid) ? { state: 'alive' } : { state: 'gone' };
|
|
987
|
+
}
|
|
988
|
+
async signalProcessGroup(pid, signal) {
|
|
989
|
+
try {
|
|
990
|
+
this.killProcess(-pid, signal);
|
|
991
|
+
return { state: 'alive' };
|
|
992
|
+
}
|
|
993
|
+
catch (error) {
|
|
994
|
+
if (this.hasErrorCode(error, 'ESRCH'))
|
|
995
|
+
return { state: 'gone' };
|
|
996
|
+
return { state: 'error', error: this.errorWithContext(error, `Failed to send ${signal} to process group ${pid}`) };
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
async waitForProcessGroupExit(pid, timeoutMs) {
|
|
1000
|
+
const initial = await this.probeProcessGroup(pid);
|
|
1001
|
+
if (initial.state !== 'alive')
|
|
1002
|
+
return initial;
|
|
1003
|
+
return new Promise((resolve) => {
|
|
1004
|
+
let settled = false;
|
|
1005
|
+
const finish = (result) => {
|
|
1006
|
+
if (settled)
|
|
1007
|
+
return;
|
|
1008
|
+
settled = true;
|
|
1009
|
+
clearInterval(checkInterval);
|
|
1010
|
+
clearTimeout(timeout);
|
|
1011
|
+
resolve(result);
|
|
1012
|
+
};
|
|
1013
|
+
const checkInterval = setInterval(() => {
|
|
1014
|
+
void this.probeProcessGroup(pid).then((result) => {
|
|
1015
|
+
if (result.state !== 'alive')
|
|
1016
|
+
finish(result);
|
|
1017
|
+
});
|
|
1018
|
+
}, 50);
|
|
1019
|
+
const timeout = setTimeout(() => {
|
|
1020
|
+
void this.probeProcessGroup(pid).then(finish);
|
|
1021
|
+
}, timeoutMs);
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Reclaim a service process group we spawned, unless the kernel recycled its pid.
|
|
1026
|
+
*
|
|
1027
|
+
* Every path that gives up on a service comes through here, because ending the
|
|
1028
|
+
* *wait* is not ending the *process*: a dev server that missed its readiness
|
|
1029
|
+
* window keeps its several hundred MB of RSS until somebody kills it, which on a
|
|
1030
|
+
* memory-starved box is what makes the next startup miss its window too.
|
|
1031
|
+
*
|
|
1032
|
+
* `pidStartTime` was captured at spawn, so a different current start time means
|
|
1033
|
+
* the pid now belongs to an unrelated process and killing it would be disastrous.
|
|
1034
|
+
* An unknown start time (non-Linux, no /proc) leaves only the kill-and-hope path.
|
|
1035
|
+
*/
|
|
1036
|
+
async reapProcessGroup(target, context = {}) {
|
|
1037
|
+
const currentStartTime = await getProcessStartTime(this.fs, target.pid);
|
|
1038
|
+
if (target.pidStartTime !== undefined && currentStartTime !== undefined && currentStartTime !== target.pidStartTime) {
|
|
1039
|
+
this.logger.warn('PID reuse detected — refusing to kill service process group', {
|
|
1040
|
+
...context,
|
|
1041
|
+
pid: target.pid,
|
|
1042
|
+
storedStartTime: target.pidStartTime,
|
|
1043
|
+
currentStartTime,
|
|
1044
|
+
});
|
|
1045
|
+
return Ok('pid-reused');
|
|
1046
|
+
}
|
|
1047
|
+
const killed = this.killProcessGroup(target.pid);
|
|
1048
|
+
if (!killed.ok)
|
|
1049
|
+
return killed;
|
|
1050
|
+
return Ok('killed');
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Reclaim the process group of an entry we are giving up on or replacing.
|
|
1054
|
+
*
|
|
1055
|
+
* A no-op once the child's `close` has fired: there is nothing left to reclaim,
|
|
1056
|
+
* and skipping it also keeps the non-Linux kill-and-hope path away from a pid the
|
|
1057
|
+
* kernel may since have handed to somebody else. A settled reap ends our claim,
|
|
1058
|
+
* so the entry is never offered up twice.
|
|
1059
|
+
*/
|
|
1060
|
+
async reapEntry(entry, reason) {
|
|
1061
|
+
if (entry.exited)
|
|
1062
|
+
return Ok(undefined);
|
|
1063
|
+
const reaped = await this.reapProcessGroup({ pid: entry.pid, pidStartTime: entry.pidStartTime }, { serviceType: entry.config.type, reason });
|
|
1064
|
+
if (!reaped.ok)
|
|
1065
|
+
return reaped;
|
|
1066
|
+
entry.exited = true;
|
|
1067
|
+
return Ok(undefined);
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* SIGKILL a process group through the injectable kill seam, without waiting
|
|
1071
|
+
* for it to go. Reached through {@link reapProcessGroup} wherever a recorded
|
|
1072
|
+
* pid could have been recycled.
|
|
1073
|
+
*/
|
|
1074
|
+
killProcessGroup(pid) {
|
|
1075
|
+
try {
|
|
1076
|
+
this.killProcess(-pid, 'SIGKILL');
|
|
1077
|
+
return Ok(undefined);
|
|
1078
|
+
}
|
|
1079
|
+
catch (error) {
|
|
1080
|
+
if (this.hasErrorCode(error, 'ESRCH'))
|
|
1081
|
+
return Ok(undefined); // Already gone.
|
|
1082
|
+
return Err(this.errorWithContext(error, `Failed to SIGKILL process group ${pid}`));
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
async terminateProcessGroup(pid, gracefulStopMs) {
|
|
1086
|
+
const initial = await this.probeProcessGroup(pid);
|
|
1087
|
+
if (initial.state === 'gone')
|
|
1088
|
+
return Ok(undefined);
|
|
1089
|
+
if (initial.state === 'error')
|
|
1090
|
+
return Err(initial.error);
|
|
1091
|
+
const term = await this.signalProcessGroup(pid, 'SIGTERM');
|
|
1092
|
+
if (term.state === 'gone')
|
|
1093
|
+
return Ok(undefined);
|
|
1094
|
+
if (term.state === 'error')
|
|
1095
|
+
return Err(term.error);
|
|
1096
|
+
const graceful = await this.waitForProcessGroupExit(pid, gracefulStopMs);
|
|
1097
|
+
if (graceful.state === 'gone')
|
|
1098
|
+
return Ok(undefined);
|
|
1099
|
+
if (graceful.state === 'error')
|
|
1100
|
+
return Err(graceful.error);
|
|
1101
|
+
const kill = await this.signalProcessGroup(pid, 'SIGKILL');
|
|
1102
|
+
if (kill.state === 'gone')
|
|
1103
|
+
return Ok(undefined);
|
|
1104
|
+
if (kill.state === 'error')
|
|
1105
|
+
return Err(kill.error);
|
|
1106
|
+
const forced = await this.waitForProcessGroupExit(pid, 5000);
|
|
1107
|
+
if (forced.state === 'gone')
|
|
1108
|
+
return Ok(undefined);
|
|
1109
|
+
if (forced.state === 'error')
|
|
1110
|
+
return Err(forced.error);
|
|
1111
|
+
return Err(new Error(`Process group ${pid} survived SIGKILL`));
|
|
568
1112
|
}
|
|
569
1113
|
/**
|
|
570
1114
|
* Stop a running service gracefully.
|
|
571
1115
|
* Port is NOT released — kept for session-level stability across restarts.
|
|
572
1116
|
*/
|
|
573
|
-
async stop(serviceType, sessionId) {
|
|
1117
|
+
async stop(serviceType, sessionId, stoppedBy = 'agent') {
|
|
1118
|
+
const inFlight = this.stopInFlight.get(serviceType);
|
|
1119
|
+
if (inFlight)
|
|
1120
|
+
return inFlight;
|
|
1121
|
+
let operation;
|
|
1122
|
+
operation = this.stopInternal(serviceType, sessionId, stoppedBy).finally(() => {
|
|
1123
|
+
if (this.stopInFlight.get(serviceType) === operation)
|
|
1124
|
+
this.stopInFlight.delete(serviceType);
|
|
1125
|
+
});
|
|
1126
|
+
this.stopInFlight.set(serviceType, operation);
|
|
1127
|
+
return operation;
|
|
1128
|
+
}
|
|
1129
|
+
async stopInternal(serviceType, sessionId, stoppedBy) {
|
|
574
1130
|
const hadPendingRestart = this.cancelPendingRestart(serviceType);
|
|
575
1131
|
this.restartRetries.delete(serviceType);
|
|
576
1132
|
const entry = this.services.get(serviceType);
|
|
577
1133
|
if (!entry) {
|
|
578
1134
|
return Err({ message: `Service '${serviceType}' not found`, recoverable: false });
|
|
579
1135
|
}
|
|
580
|
-
if (entry.status !== 'starting' && entry.status !== 'ready'
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
|
|
1136
|
+
if (entry.status !== 'starting' && entry.status !== 'ready') {
|
|
1137
|
+
// `failed` says the start gave up, not that the process did — and refusing to
|
|
1138
|
+
// stop it left the platform-side reaper, the one caller that can free a leaked
|
|
1139
|
+
// dev server's memory, with no way to do so. A failed service with a revival
|
|
1140
|
+
// queued is on top of that really "about to restart", and calling that off is a
|
|
1141
|
+
// legitimate stop rather than an error.
|
|
1142
|
+
if (entry.status === 'failed' || hadPendingRestart) {
|
|
1143
|
+
const reaped = await this.reapEntry(entry, 'stopped while failed');
|
|
1144
|
+
if (!reaped.ok)
|
|
1145
|
+
return Err({ message: reaped.error.message, recoverable: true });
|
|
1146
|
+
try {
|
|
1147
|
+
await entry.forgetPid();
|
|
1148
|
+
}
|
|
1149
|
+
catch (error) {
|
|
1150
|
+
return Err({ message: this.errorWithContext(error, `Failed to forget PID for service '${serviceType}'`).message, recoverable: true });
|
|
1151
|
+
}
|
|
584
1152
|
entry.status = 'stopped';
|
|
585
|
-
this.notifyStatusChanged(sessionId, serviceType, 'stopped');
|
|
1153
|
+
this.notifyStatusChanged(sessionId, serviceType, 'stopped', { stoppedBy });
|
|
586
1154
|
return Ok(undefined);
|
|
587
1155
|
}
|
|
588
1156
|
return Err({ message: `Service '${serviceType}' is ${entry.status}, cannot stop`, recoverable: false });
|
|
589
1157
|
}
|
|
1158
|
+
const previousStatus = entry.status;
|
|
1159
|
+
this.recordStopSource(entry, stoppedBy);
|
|
590
1160
|
entry.status = 'stopping';
|
|
591
1161
|
this.notifyStatusChanged(sessionId, serviceType, 'stopping');
|
|
592
1162
|
const gracefulStopMs = entry.config.gracefulStopMs ?? 5000;
|
|
593
|
-
|
|
1163
|
+
const stopped = await this.terminateProcessGroup(entry.pid, gracefulStopMs);
|
|
1164
|
+
if (!stopped.ok) {
|
|
1165
|
+
if (entry.status === 'stopping') {
|
|
1166
|
+
entry.status = previousStatus;
|
|
1167
|
+
this.notifyStatusChanged(sessionId, serviceType, previousStatus, {
|
|
1168
|
+
port: entry.port,
|
|
1169
|
+
cwd: entry.cwd,
|
|
1170
|
+
command: entry.command,
|
|
1171
|
+
error: stopped.error.message,
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
return Err({ message: stopped.error.message, recoverable: true });
|
|
1175
|
+
}
|
|
1176
|
+
// The group is confirmed gone, so nothing may offer this pid up for a reap again.
|
|
1177
|
+
entry.exited = true;
|
|
594
1178
|
try {
|
|
595
|
-
|
|
1179
|
+
await entry.forgetPid();
|
|
596
1180
|
}
|
|
597
|
-
catch {
|
|
598
|
-
|
|
1181
|
+
catch (error) {
|
|
1182
|
+
return Err({ message: this.errorWithContext(error, `Failed to forget PID for service '${serviceType}'`).message, recoverable: true });
|
|
1183
|
+
}
|
|
1184
|
+
if (entry.status === 'stopping') {
|
|
599
1185
|
entry.status = 'stopped';
|
|
600
|
-
this.notifyStatusChanged(sessionId, serviceType, 'stopped');
|
|
601
|
-
return Ok(undefined);
|
|
1186
|
+
this.notifyStatusChanged(sessionId, serviceType, 'stopped', { stoppedBy: entry.stopSource ?? stoppedBy });
|
|
602
1187
|
}
|
|
603
|
-
// Wait for graceful shutdown, then SIGKILL
|
|
604
|
-
await new Promise((resolve) => {
|
|
605
|
-
const checkInterval = setInterval(() => {
|
|
606
|
-
try {
|
|
607
|
-
// Check if process is still alive (signal 0 doesn't kill, just checks)
|
|
608
|
-
process.kill(entry.pid, 0);
|
|
609
|
-
}
|
|
610
|
-
catch {
|
|
611
|
-
// Process gone
|
|
612
|
-
clearInterval(checkInterval);
|
|
613
|
-
clearTimeout(killTimeout);
|
|
614
|
-
resolve();
|
|
615
|
-
}
|
|
616
|
-
}, 200);
|
|
617
|
-
const killTimeout = setTimeout(() => {
|
|
618
|
-
clearInterval(checkInterval);
|
|
619
|
-
try {
|
|
620
|
-
process.kill(-entry.pid, 'SIGKILL');
|
|
621
|
-
}
|
|
622
|
-
catch {
|
|
623
|
-
// Already gone
|
|
624
|
-
}
|
|
625
|
-
resolve();
|
|
626
|
-
}, gracefulStopMs);
|
|
627
|
-
});
|
|
628
1188
|
this.logger.info('Service stopped', { serviceType });
|
|
629
1189
|
return Ok(undefined);
|
|
630
1190
|
}
|
|
@@ -633,61 +1193,13 @@ export class ServiceExecutor {
|
|
|
633
1193
|
*/
|
|
634
1194
|
async restart(config, sessionId, workspaceDir, preferredPort) {
|
|
635
1195
|
const entry = this.services.get(config.type);
|
|
636
|
-
if (entry && (entry.status === 'starting' || entry.status === 'ready'
|
|
1196
|
+
if (entry && (entry.status === 'starting' || entry.status === 'ready')) {
|
|
637
1197
|
const stopResult = await this.stop(config.type, sessionId);
|
|
638
1198
|
if (!stopResult.ok)
|
|
639
1199
|
return stopResult;
|
|
640
1200
|
}
|
|
641
1201
|
return this.start(config, sessionId, workspaceDir, preferredPort);
|
|
642
1202
|
}
|
|
643
|
-
/**
|
|
644
|
-
* Pause a running service (SIGSTOP).
|
|
645
|
-
*/
|
|
646
|
-
async pause(serviceType, sessionId) {
|
|
647
|
-
const entry = this.services.get(serviceType);
|
|
648
|
-
if (!entry) {
|
|
649
|
-
return Err({ message: `Service '${serviceType}' not found`, recoverable: false });
|
|
650
|
-
}
|
|
651
|
-
if (entry.status !== 'ready') {
|
|
652
|
-
return Err({ message: `Service '${serviceType}' is ${entry.status}, cannot pause`, recoverable: false });
|
|
653
|
-
}
|
|
654
|
-
try {
|
|
655
|
-
process.kill(entry.pid, 'SIGSTOP');
|
|
656
|
-
}
|
|
657
|
-
catch {
|
|
658
|
-
return Err({ message: `Failed to pause service '${serviceType}'`, recoverable: false });
|
|
659
|
-
}
|
|
660
|
-
entry.status = 'paused';
|
|
661
|
-
this.notifyStatusChanged(sessionId, serviceType, 'paused');
|
|
662
|
-
this.logger.info('Service paused', { serviceType });
|
|
663
|
-
return Ok(undefined);
|
|
664
|
-
}
|
|
665
|
-
/**
|
|
666
|
-
* Resume a paused service (SIGCONT).
|
|
667
|
-
*/
|
|
668
|
-
async resume(config, sessionId, _workspaceDir) {
|
|
669
|
-
const entry = this.services.get(config.type);
|
|
670
|
-
if (!entry) {
|
|
671
|
-
return Err({ message: `Service '${config.type}' not found`, recoverable: false });
|
|
672
|
-
}
|
|
673
|
-
if (entry.status !== 'paused') {
|
|
674
|
-
return Err({ message: `Service '${config.type}' is ${entry.status}, cannot resume`, recoverable: false });
|
|
675
|
-
}
|
|
676
|
-
try {
|
|
677
|
-
process.kill(entry.pid, 'SIGCONT');
|
|
678
|
-
}
|
|
679
|
-
catch {
|
|
680
|
-
return Err({ message: `Failed to resume service '${config.type}'`, recoverable: false });
|
|
681
|
-
}
|
|
682
|
-
entry.status = 'ready';
|
|
683
|
-
this.notifyStatusChanged(sessionId, config.type, 'ready', {
|
|
684
|
-
port: entry.port,
|
|
685
|
-
cwd: entry.cwd,
|
|
686
|
-
command: entry.command,
|
|
687
|
-
});
|
|
688
|
-
this.logger.info('Service resumed', { serviceType: config.type });
|
|
689
|
-
return Ok(undefined);
|
|
690
|
-
}
|
|
691
1203
|
/**
|
|
692
1204
|
* Get recent log lines for a service.
|
|
693
1205
|
*/
|
|
@@ -711,67 +1223,5 @@ export class ServiceExecutor {
|
|
|
711
1223
|
const status = this.getStatus(serviceType);
|
|
712
1224
|
return status === 'starting' || status === 'ready';
|
|
713
1225
|
}
|
|
714
|
-
/**
|
|
715
|
-
* Shutdown all services and release all ports back to pool.
|
|
716
|
-
* Called on session close.
|
|
717
|
-
*/
|
|
718
|
-
async shutdown() {
|
|
719
|
-
// Nothing queued may spawn after the executor is gone.
|
|
720
|
-
for (const serviceType of [...this.restartTimers.keys()])
|
|
721
|
-
this.cancelPendingRestart(serviceType);
|
|
722
|
-
this.restartRetries.clear();
|
|
723
|
-
const promises = [];
|
|
724
|
-
for (const [serviceType, entry] of this.services) {
|
|
725
|
-
if (entry.status === 'starting' || entry.status === 'ready' || entry.status === 'paused') {
|
|
726
|
-
const gracefulStopMs = entry.config.gracefulStopMs ?? 5000;
|
|
727
|
-
const killPromise = new Promise((resolve) => {
|
|
728
|
-
try {
|
|
729
|
-
process.kill(-entry.pid, 'SIGTERM');
|
|
730
|
-
}
|
|
731
|
-
catch {
|
|
732
|
-
resolve();
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
const killTimeout = setTimeout(() => {
|
|
736
|
-
try {
|
|
737
|
-
process.kill(-entry.pid, 'SIGKILL');
|
|
738
|
-
}
|
|
739
|
-
catch {
|
|
740
|
-
// Already gone
|
|
741
|
-
}
|
|
742
|
-
resolve();
|
|
743
|
-
}, gracefulStopMs);
|
|
744
|
-
const checkInterval = setInterval(() => {
|
|
745
|
-
try {
|
|
746
|
-
process.kill(entry.pid, 0);
|
|
747
|
-
}
|
|
748
|
-
catch {
|
|
749
|
-
clearInterval(checkInterval);
|
|
750
|
-
clearTimeout(killTimeout);
|
|
751
|
-
resolve();
|
|
752
|
-
}
|
|
753
|
-
}, 200);
|
|
754
|
-
});
|
|
755
|
-
promises.push(killPromise);
|
|
756
|
-
entry.status = 'stopping';
|
|
757
|
-
this.logger.info('Shutting down service', { serviceType });
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
await Promise.all(promises);
|
|
761
|
-
this.services.clear();
|
|
762
|
-
// Drain all waiters with error
|
|
763
|
-
for (const [serviceType, pending] of this.waiters) {
|
|
764
|
-
for (const waiter of pending) {
|
|
765
|
-
clearTimeout(waiter.timer);
|
|
766
|
-
waiter.resolve(Err({ message: `Service '${serviceType}' shut down`, recoverable: false }));
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
this.waiters.clear();
|
|
770
|
-
// Release all allocated ports back to pool
|
|
771
|
-
for (const port of this.allocatedPorts.values()) {
|
|
772
|
-
this.portPool.release(port);
|
|
773
|
-
}
|
|
774
|
-
this.allocatedPorts.clear();
|
|
775
|
-
}
|
|
776
1226
|
}
|
|
777
1227
|
//# sourceMappingURL=service.js.map
|