@cmetech/otto 1.1.1 → 1.2.5
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/coworker/persona-commands.d.ts +1 -0
- package/dist/coworker/persona-commands.js +5 -0
- package/dist/coworker/persona-commands.test.d.ts +1 -0
- package/dist/coworker/persona-commands.test.js +45 -0
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/coworker-artifacts/artifacts-command.js +31 -0
- package/dist/resources/extensions/coworker-artifacts/artifacts-singleton.js +17 -0
- package/dist/resources/extensions/coworker-artifacts/extension-manifest.json +13 -0
- package/dist/resources/extensions/coworker-artifacts/index.js +125 -0
- package/dist/resources/extensions/coworker-artifacts/list-tool.js +27 -0
- package/dist/resources/extensions/coworker-artifacts/open-tool.js +25 -0
- package/dist/resources/extensions/coworker-memory/extension-manifest.json +13 -0
- package/dist/resources/extensions/coworker-memory/index.js +219 -0
- package/dist/resources/extensions/coworker-memory/memorize-tool.js +10 -0
- package/dist/resources/extensions/coworker-memory/memory-command.js +157 -0
- package/dist/resources/extensions/coworker-memory/memory-singleton.js +55 -0
- package/dist/resources/extensions/coworker-memory/recall-tool.js +18 -0
- package/dist/resources/extensions/coworker-memory/session-hooks.js +45 -0
- package/dist/resources/extensions/coworker-scratchpad/attach-banners.js +53 -0
- package/dist/resources/extensions/coworker-scratchpad/extension-manifest.json +13 -0
- package/dist/resources/extensions/coworker-scratchpad/format-age.js +9 -0
- package/dist/resources/extensions/coworker-scratchpad/helpers.js +38 -0
- package/dist/resources/extensions/coworker-scratchpad/index.js +199 -0
- package/dist/resources/extensions/coworker-scratchpad/mime-bundle.js +20 -0
- package/dist/resources/extensions/coworker-scratchpad/scratchpad-tool.js +118 -0
- package/dist/resources/extensions/coworker-scratchpad/session-sidecar.js +60 -0
- package/dist/resources/extensions/coworker-scratchpad/sp-command.js +597 -0
- package/dist/resources/extensions/coworker-scratchpad/workspace-pointer.js +41 -0
- package/dist/resources/extensions/coworker-scratchpad/workspace-root.js +17 -0
- package/dist/resources/extensions/coworker-vault/audit-command.js +35 -0
- package/dist/resources/extensions/coworker-vault/connect-command.js +42 -0
- package/dist/resources/extensions/coworker-vault/datasource-command.js +50 -0
- package/dist/resources/extensions/coworker-vault/extension-manifest.json +12 -0
- package/dist/resources/extensions/coworker-vault/index.js +171 -0
- package/dist/resources/extensions/coworker-vault/test-helpers.js +86 -0
- package/dist/resources/extensions/coworker-vault/vault-singleton.js +24 -0
- package/dist/resources/extensions/otto/commands/release-notes/_data.js +83 -0
- package/dist/resources/extensions/otto/commands/release-notes/command.js +15 -4
- package/dist/resources/extensions/otto/index.js +31 -6
- package/dist/resources/extensions/shared/coworker-paths.js +8 -0
- package/dist/resources/extensions/slash-commands/{audit.js → audit-codebase.js} +4 -4
- package/dist/resources/extensions/slash-commands/extension-manifest.json +1 -1
- package/dist/resources/extensions/slash-commands/index.js +2 -2
- package/dist/resources/extensions/subagent/index.js +8 -1
- package/dist/resources/extensions/subagent/launch.js +37 -5
- package/dist/resources/extensions/subagent/run-store.js +1 -0
- package/dist/resources/extensions/workflow/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/workflow/bootstrap/register-hooks.js +10 -0
- package/dist/resources/extensions/workflow/health-widget-core.js +1 -1
- package/dist/resources/extensions/workflow/persona-status.js +87 -0
- package/package.json +26 -10
- package/packages/contracts/package.json +1 -1
- package/packages/coworker-artifacts/dist/artifact-store.d.ts +25 -0
- package/packages/coworker-artifacts/dist/artifact-store.js +187 -0
- package/packages/coworker-artifacts/dist/dir-snapshot.d.ts +7 -0
- package/packages/coworker-artifacts/dist/dir-snapshot.js +54 -0
- package/packages/coworker-artifacts/dist/errors.d.ts +18 -0
- package/packages/coworker-artifacts/dist/errors.js +37 -0
- package/packages/coworker-artifacts/dist/index.d.ts +7 -0
- package/packages/coworker-artifacts/dist/index.js +7 -0
- package/packages/coworker-artifacts/dist/readme-renderer.d.ts +5 -0
- package/packages/coworker-artifacts/dist/readme-renderer.js +47 -0
- package/packages/coworker-artifacts/dist/resolve-uri.d.ts +3 -0
- package/packages/coworker-artifacts/dist/resolve-uri.js +29 -0
- package/packages/coworker-artifacts/dist/slug.d.ts +4 -0
- package/packages/coworker-artifacts/dist/slug.js +32 -0
- package/packages/coworker-artifacts/dist/types.d.ts +52 -0
- package/packages/coworker-artifacts/dist/types.js +1 -0
- package/packages/coworker-artifacts/package.json +20 -0
- package/packages/coworker-artifacts/src/artifact-store.test.ts +188 -0
- package/packages/coworker-artifacts/src/artifact-store.ts +206 -0
- package/packages/coworker-artifacts/src/artifacts-integration.test.ts +109 -0
- package/packages/coworker-artifacts/src/dir-snapshot.test.ts +71 -0
- package/packages/coworker-artifacts/src/dir-snapshot.ts +52 -0
- package/packages/coworker-artifacts/src/errors.test.ts +37 -0
- package/packages/coworker-artifacts/src/errors.ts +28 -0
- package/packages/coworker-artifacts/src/index.test.ts +22 -0
- package/packages/coworker-artifacts/src/index.ts +7 -0
- package/packages/coworker-artifacts/src/readme-renderer.test.ts +72 -0
- package/packages/coworker-artifacts/src/readme-renderer.ts +56 -0
- package/packages/coworker-artifacts/src/resolve-uri.test.ts +46 -0
- package/packages/coworker-artifacts/src/resolve-uri.ts +29 -0
- package/packages/coworker-artifacts/src/slug.test.ts +47 -0
- package/packages/coworker-artifacts/src/slug.ts +31 -0
- package/packages/coworker-artifacts/src/types.ts +61 -0
- package/packages/coworker-artifacts/tsconfig.json +15 -0
- package/packages/coworker-artifacts/tsconfig.publish.json +4 -0
- package/packages/coworker-memory/dist/context-injection.d.ts +9 -0
- package/packages/coworker-memory/dist/context-injection.js +41 -0
- package/packages/coworker-memory/dist/errors.d.ts +25 -0
- package/packages/coworker-memory/dist/errors.js +51 -0
- package/packages/coworker-memory/dist/index.d.ts +12 -0
- package/packages/coworker-memory/dist/index.js +12 -0
- package/packages/coworker-memory/dist/layer-a-store.d.ts +16 -0
- package/packages/coworker-memory/dist/layer-a-store.js +78 -0
- package/packages/coworker-memory/dist/local-sqlite-backend.d.ts +28 -0
- package/packages/coworker-memory/dist/local-sqlite-backend.js +167 -0
- package/packages/coworker-memory/dist/memory-backend.d.ts +14 -0
- package/packages/coworker-memory/dist/memory-backend.js +1 -0
- package/packages/coworker-memory/dist/memory-recorder.d.ts +50 -0
- package/packages/coworker-memory/dist/memory-recorder.js +69 -0
- package/packages/coworker-memory/dist/migrations/001-init.sql +38 -0
- package/packages/coworker-memory/dist/migrations/002-artifact-kind.sql +50 -0
- package/packages/coworker-memory/dist/paste-detector.d.ts +5 -0
- package/packages/coworker-memory/dist/paste-detector.js +14 -0
- package/packages/coworker-memory/dist/persona-seed.d.ts +10 -0
- package/packages/coworker-memory/dist/persona-seed.js +38 -0
- package/packages/coworker-memory/dist/recall-formatter.d.ts +2 -0
- package/packages/coworker-memory/dist/recall-formatter.js +14 -0
- package/packages/coworker-memory/dist/scope-resolver.d.ts +9 -0
- package/packages/coworker-memory/dist/scope-resolver.js +10 -0
- package/packages/coworker-memory/dist/types.d.ts +51 -0
- package/packages/coworker-memory/dist/types.js +2 -0
- package/packages/coworker-memory/dist/workspace-id.d.ts +3 -0
- package/packages/coworker-memory/dist/workspace-id.js +54 -0
- package/packages/coworker-memory/package.json +35 -0
- package/packages/coworker-memory/src/activator-integration.test.ts +141 -0
- package/packages/coworker-memory/src/context-injection.test.ts +72 -0
- package/packages/coworker-memory/src/context-injection.ts +57 -0
- package/packages/coworker-memory/src/errors.test.ts +45 -0
- package/packages/coworker-memory/src/errors.ts +42 -0
- package/packages/coworker-memory/src/index.test.ts +21 -0
- package/packages/coworker-memory/src/index.ts +12 -0
- package/packages/coworker-memory/src/layer-a-store.test.ts +85 -0
- package/packages/coworker-memory/src/layer-a-store.ts +88 -0
- package/packages/coworker-memory/src/local-sqlite-backend.test.ts +110 -0
- package/packages/coworker-memory/src/local-sqlite-backend.ts +185 -0
- package/packages/coworker-memory/src/memory-backend.ts +10 -0
- package/packages/coworker-memory/src/memory-integration.test.ts +89 -0
- package/packages/coworker-memory/src/memory-recorder.test.ts +101 -0
- package/packages/coworker-memory/src/memory-recorder.ts +95 -0
- package/packages/coworker-memory/src/migrations/001-init.sql +38 -0
- package/packages/coworker-memory/src/migrations/002-artifact-kind.sql +50 -0
- package/packages/coworker-memory/src/paste-detector.test.ts +23 -0
- package/packages/coworker-memory/src/paste-detector.ts +18 -0
- package/packages/coworker-memory/src/persona-seed.test.ts +57 -0
- package/packages/coworker-memory/src/persona-seed.ts +46 -0
- package/packages/coworker-memory/src/recall-formatter.test.ts +34 -0
- package/packages/coworker-memory/src/recall-formatter.ts +15 -0
- package/packages/coworker-memory/src/scope-resolver.test.ts +23 -0
- package/packages/coworker-memory/src/scope-resolver.ts +18 -0
- package/packages/coworker-memory/src/types.ts +61 -0
- package/packages/coworker-memory/src/workspace-id.test.ts +48 -0
- package/packages/coworker-memory/src/workspace-id.ts +56 -0
- package/packages/coworker-memory/tsconfig.json +15 -0
- package/packages/coworker-memory/tsconfig.publish.json +4 -0
- package/packages/coworker-persona/dist/commands.d.ts +7 -0
- package/packages/coworker-persona/dist/commands.js +35 -0
- package/packages/coworker-persona/dist/defaults/manifest.yaml +12 -0
- package/packages/coworker-persona/dist/defaults/steering/identity.md +3 -0
- package/packages/coworker-persona/dist/index.d.ts +3 -0
- package/packages/coworker-persona/dist/index.js +3 -0
- package/packages/coworker-persona/dist/manifest.d.ts +24 -0
- package/packages/coworker-persona/dist/manifest.js +21 -0
- package/packages/coworker-persona/dist/registry.d.ts +22 -0
- package/packages/coworker-persona/dist/registry.js +142 -0
- package/packages/coworker-persona/package.json +28 -0
- package/packages/coworker-persona/scripts/copy-defaults.cjs +17 -0
- package/packages/coworker-persona/src/commands.ts +47 -0
- package/packages/coworker-persona/src/defaults/manifest.yaml +12 -0
- package/packages/coworker-persona/src/defaults/steering/identity.md +3 -0
- package/packages/coworker-persona/src/index.ts +3 -0
- package/packages/coworker-persona/src/manifest.test.ts +67 -0
- package/packages/coworker-persona/src/manifest.ts +49 -0
- package/packages/coworker-persona/src/registry.test.ts +89 -0
- package/packages/coworker-persona/src/registry.ts +147 -0
- package/packages/coworker-persona/tsconfig.json +15 -0
- package/packages/coworker-persona/tsconfig.publish.json +4 -0
- package/packages/coworker-scratchpad/dist/cell-archive.d.ts +39 -0
- package/packages/coworker-scratchpad/dist/cell-archive.js +77 -0
- package/packages/coworker-scratchpad/dist/cell-tree.d.ts +14 -0
- package/packages/coworker-scratchpad/dist/cell-tree.js +72 -0
- package/packages/coworker-scratchpad/dist/child-process-runtime.d.ts +129 -0
- package/packages/coworker-scratchpad/dist/child-process-runtime.js +427 -0
- package/packages/coworker-scratchpad/dist/collector-registry.d.ts +12 -0
- package/packages/coworker-scratchpad/dist/collector-registry.js +29 -0
- package/packages/coworker-scratchpad/dist/detect-kind.d.ts +3 -0
- package/packages/coworker-scratchpad/dist/detect-kind.js +19 -0
- package/packages/coworker-scratchpad/dist/file-collector.d.ts +15 -0
- package/packages/coworker-scratchpad/dist/file-collector.js +99 -0
- package/packages/coworker-scratchpad/dist/index.d.ts +13 -0
- package/packages/coworker-scratchpad/dist/index.js +13 -0
- package/packages/coworker-scratchpad/dist/kernel-bindings.d.ts +49 -0
- package/packages/coworker-scratchpad/dist/kernel-bindings.js +220 -0
- package/packages/coworker-scratchpad/dist/kernel-entry.d.ts +1 -0
- package/packages/coworker-scratchpad/dist/kernel-entry.js +355 -0
- package/packages/coworker-scratchpad/dist/kernel-protocol.d.ts +171 -0
- package/packages/coworker-scratchpad/dist/kernel-protocol.js +48 -0
- package/packages/coworker-scratchpad/dist/kernel-spawn.d.ts +3 -0
- package/packages/coworker-scratchpad/dist/kernel-spawn.js +54 -0
- package/packages/coworker-scratchpad/dist/namespace-codec.d.ts +22 -0
- package/packages/coworker-scratchpad/dist/namespace-codec.js +61 -0
- package/packages/coworker-scratchpad/dist/scratchpad-lock.d.ts +24 -0
- package/packages/coworker-scratchpad/dist/scratchpad-lock.js +86 -0
- package/packages/coworker-scratchpad/dist/scratchpad-manager.d.ts +193 -0
- package/packages/coworker-scratchpad/dist/scratchpad-manager.js +866 -0
- package/packages/coworker-scratchpad/dist/staleness-banner.d.ts +12 -0
- package/packages/coworker-scratchpad/dist/staleness-banner.js +27 -0
- package/packages/coworker-scratchpad/package.json +31 -0
- package/packages/coworker-scratchpad/src/cell-archive.test.ts +150 -0
- package/packages/coworker-scratchpad/src/cell-archive.ts +97 -0
- package/packages/coworker-scratchpad/src/cell-tree.test.ts +105 -0
- package/packages/coworker-scratchpad/src/cell-tree.ts +90 -0
- package/packages/coworker-scratchpad/src/child-process-runtime.test.ts +413 -0
- package/packages/coworker-scratchpad/src/child-process-runtime.ts +493 -0
- package/packages/coworker-scratchpad/src/collector-registry.test.ts +69 -0
- package/packages/coworker-scratchpad/src/collector-registry.ts +33 -0
- package/packages/coworker-scratchpad/src/detect-kind.test.ts +33 -0
- package/packages/coworker-scratchpad/src/detect-kind.ts +22 -0
- package/packages/coworker-scratchpad/src/file-collector.test.ts +109 -0
- package/packages/coworker-scratchpad/src/file-collector.ts +114 -0
- package/packages/coworker-scratchpad/src/index.ts +74 -0
- package/packages/coworker-scratchpad/src/kernel-bindings.test.ts +188 -0
- package/packages/coworker-scratchpad/src/kernel-bindings.ts +279 -0
- package/packages/coworker-scratchpad/src/kernel-entry.test.ts +123 -0
- package/packages/coworker-scratchpad/src/kernel-entry.ts +390 -0
- package/packages/coworker-scratchpad/src/kernel-protocol.test.ts +105 -0
- package/packages/coworker-scratchpad/src/kernel-protocol.ts +230 -0
- package/packages/coworker-scratchpad/src/kernel-spawn.test.ts +60 -0
- package/packages/coworker-scratchpad/src/kernel-spawn.ts +54 -0
- package/packages/coworker-scratchpad/src/namespace-codec.test.ts +102 -0
- package/packages/coworker-scratchpad/src/namespace-codec.ts +90 -0
- package/packages/coworker-scratchpad/src/scratchpad-lock.test.ts +98 -0
- package/packages/coworker-scratchpad/src/scratchpad-lock.ts +102 -0
- package/packages/coworker-scratchpad/src/scratchpad-manager.test.ts +1343 -0
- package/packages/coworker-scratchpad/src/scratchpad-manager.ts +891 -0
- package/packages/coworker-scratchpad/src/staleness-banner.test.ts +53 -0
- package/packages/coworker-scratchpad/src/staleness-banner.ts +33 -0
- package/packages/coworker-scratchpad/src/vault-integration.test.ts +221 -0
- package/packages/coworker-scratchpad/tsconfig.json +15 -0
- package/packages/coworker-scratchpad/tsconfig.publish.json +4 -0
- package/packages/coworker-types/dist/artifacts.d.ts +31 -0
- package/packages/coworker-types/dist/artifacts.js +2 -0
- package/packages/coworker-types/dist/contracts.d.ts +32 -0
- package/packages/coworker-types/dist/contracts.js +1 -0
- package/packages/coworker-types/dist/index.d.ts +5 -0
- package/packages/coworker-types/dist/index.js +5 -0
- package/packages/coworker-types/dist/memory.d.ts +61 -0
- package/packages/coworker-types/dist/memory.js +3 -0
- package/packages/coworker-types/dist/scratchpad.d.ts +43 -0
- package/packages/coworker-types/dist/scratchpad.js +2 -0
- package/packages/coworker-types/dist/vault.d.ts +34 -0
- package/packages/coworker-types/dist/vault.js +2 -0
- package/packages/coworker-types/package.json +24 -0
- package/packages/coworker-types/src/artifacts.test.ts +52 -0
- package/packages/coworker-types/src/artifacts.ts +35 -0
- package/packages/coworker-types/src/contracts.test.ts +43 -0
- package/packages/coworker-types/src/contracts.ts +36 -0
- package/packages/coworker-types/src/index.ts +5 -0
- package/packages/coworker-types/src/memory.test.ts +50 -0
- package/packages/coworker-types/src/memory.ts +79 -0
- package/packages/coworker-types/src/scratchpad.test.ts +46 -0
- package/packages/coworker-types/src/scratchpad.ts +51 -0
- package/packages/coworker-types/src/smoke.test.ts +34 -0
- package/packages/coworker-types/src/vault.test.ts +49 -0
- package/packages/coworker-types/src/vault.ts +40 -0
- package/packages/coworker-types/tsconfig.json +15 -0
- package/packages/coworker-types/tsconfig.publish.json +4 -0
- package/packages/coworker-utils/dist/audit-log.d.ts +34 -0
- package/packages/coworker-utils/dist/audit-log.js +88 -0
- package/packages/coworker-utils/dist/index.d.ts +6 -0
- package/packages/coworker-utils/dist/index.js +6 -0
- package/packages/coworker-utils/dist/lease.d.ts +7 -0
- package/packages/coworker-utils/dist/lease.js +67 -0
- package/packages/coworker-utils/dist/logger.d.ts +13 -0
- package/packages/coworker-utils/dist/logger.js +26 -0
- package/packages/coworker-utils/dist/migration-runner.d.ts +7 -0
- package/packages/coworker-utils/dist/migration-runner.js +36 -0
- package/packages/coworker-utils/dist/ndjson-channel.d.ts +3 -0
- package/packages/coworker-utils/dist/ndjson-channel.js +38 -0
- package/packages/coworker-utils/dist/secret-scanner.d.ts +10 -0
- package/packages/coworker-utils/dist/secret-scanner.js +42 -0
- package/packages/coworker-utils/package.json +24 -0
- package/packages/coworker-utils/src/audit-log.test.ts +140 -0
- package/packages/coworker-utils/src/audit-log.ts +107 -0
- package/packages/coworker-utils/src/index.ts +6 -0
- package/packages/coworker-utils/src/lease.test.ts +64 -0
- package/packages/coworker-utils/src/lease.ts +76 -0
- package/packages/coworker-utils/src/logger.test.ts +50 -0
- package/packages/coworker-utils/src/logger.ts +45 -0
- package/packages/coworker-utils/src/migration-runner.test.ts +65 -0
- package/packages/coworker-utils/src/migration-runner.ts +50 -0
- package/packages/coworker-utils/src/ndjson-channel.test.ts +76 -0
- package/packages/coworker-utils/src/ndjson-channel.ts +41 -0
- package/packages/coworker-utils/src/secret-scanner.test.ts +61 -0
- package/packages/coworker-utils/src/secret-scanner.ts +56 -0
- package/packages/coworker-utils/tsconfig.json +15 -0
- package/packages/coworker-utils/tsconfig.publish.json +4 -0
- package/packages/coworker-vault/dist/data-vault.d.ts +41 -0
- package/packages/coworker-vault/dist/data-vault.js +223 -0
- package/packages/coworker-vault/dist/engine-registry.d.ts +34 -0
- package/packages/coworker-vault/dist/engine-registry.js +90 -0
- package/packages/coworker-vault/dist/engines/jira.yaml +17 -0
- package/packages/coworker-vault/dist/errors.d.ts +28 -0
- package/packages/coworker-vault/dist/errors.js +57 -0
- package/packages/coworker-vault/dist/index.d.ts +6 -0
- package/packages/coworker-vault/dist/index.js +6 -0
- package/packages/coworker-vault/dist/injector.d.ts +19 -0
- package/packages/coworker-vault/dist/injector.js +77 -0
- package/packages/coworker-vault/dist/types.d.ts +28 -0
- package/packages/coworker-vault/dist/types.js +1 -0
- package/packages/coworker-vault/dist/vault-keep.d.ts +4 -0
- package/packages/coworker-vault/dist/vault-keep.js +21 -0
- package/packages/coworker-vault/package.json +29 -0
- package/packages/coworker-vault/src/data-vault.test.ts +199 -0
- package/packages/coworker-vault/src/data-vault.ts +257 -0
- package/packages/coworker-vault/src/engine-registry.test.ts +120 -0
- package/packages/coworker-vault/src/engine-registry.ts +107 -0
- package/packages/coworker-vault/src/engines/jira.yaml +17 -0
- package/packages/coworker-vault/src/errors.test.ts +58 -0
- package/packages/coworker-vault/src/errors.ts +50 -0
- package/packages/coworker-vault/src/index.test.ts +24 -0
- package/packages/coworker-vault/src/index.ts +6 -0
- package/packages/coworker-vault/src/injector.test.ts +109 -0
- package/packages/coworker-vault/src/injector.ts +98 -0
- package/packages/coworker-vault/src/types.ts +33 -0
- package/packages/coworker-vault/src/vault-keep.test.ts +49 -0
- package/packages/coworker-vault/src/vault-keep.ts +31 -0
- package/packages/coworker-vault/tsconfig.json +15 -0
- package/packages/coworker-vault/tsconfig.publish.json +4 -0
- package/packages/daemon/package.json +3 -3
- package/packages/mcp-server/package.json +3 -3
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/native/package.json +1 -1
- package/packages/native/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +6 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +22 -3
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +11 -0
- package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.d.ts +47 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.js +107 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.d.ts +19 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.js +121 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +17 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +2 -2
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +22 -3
- package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +11 -0
- package/packages/pi-coding-agent/src/modes/rpc/raw-stdout.regression.test.ts +129 -0
- package/packages/pi-coding-agent/src/modes/rpc/raw-stdout.ts +117 -0
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +18 -1
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/packages/rpc-client/tsconfig.tsbuildinfo +1 -1
- package/pkg/package.json +1 -1
- package/scripts/install.js +6 -5
- package/src/resources/extensions/coworker-artifacts/artifacts-command.test.ts +54 -0
- package/src/resources/extensions/coworker-artifacts/artifacts-command.ts +43 -0
- package/src/resources/extensions/coworker-artifacts/artifacts-singleton.test.ts +25 -0
- package/src/resources/extensions/coworker-artifacts/artifacts-singleton.ts +29 -0
- package/src/resources/extensions/coworker-artifacts/extension-manifest.json +13 -0
- package/src/resources/extensions/coworker-artifacts/index.test.ts +46 -0
- package/src/resources/extensions/coworker-artifacts/index.ts +154 -0
- package/src/resources/extensions/coworker-artifacts/list-tool.test.ts +29 -0
- package/src/resources/extensions/coworker-artifacts/list-tool.ts +53 -0
- package/src/resources/extensions/coworker-artifacts/open-tool.test.ts +30 -0
- package/src/resources/extensions/coworker-artifacts/open-tool.ts +43 -0
- package/src/resources/extensions/coworker-memory/extension-manifest.json +13 -0
- package/src/resources/extensions/coworker-memory/index.test.ts +137 -0
- package/src/resources/extensions/coworker-memory/index.ts +257 -0
- package/src/resources/extensions/coworker-memory/memorize-tool.test.ts +41 -0
- package/src/resources/extensions/coworker-memory/memorize-tool.ts +20 -0
- package/src/resources/extensions/coworker-memory/memory-command.test.ts +134 -0
- package/src/resources/extensions/coworker-memory/memory-command.ts +131 -0
- package/src/resources/extensions/coworker-memory/memory-singleton.test.ts +41 -0
- package/src/resources/extensions/coworker-memory/memory-singleton.ts +89 -0
- package/src/resources/extensions/coworker-memory/recall-tool.test.ts +50 -0
- package/src/resources/extensions/coworker-memory/recall-tool.ts +35 -0
- package/src/resources/extensions/coworker-memory/session-hooks.test.ts +77 -0
- package/src/resources/extensions/coworker-memory/session-hooks.ts +61 -0
- package/src/resources/extensions/coworker-scratchpad/attach-banners.test.ts +124 -0
- package/src/resources/extensions/coworker-scratchpad/attach-banners.ts +67 -0
- package/src/resources/extensions/coworker-scratchpad/extension-manifest.json +13 -0
- package/src/resources/extensions/coworker-scratchpad/format-age.test.ts +30 -0
- package/src/resources/extensions/coworker-scratchpad/format-age.ts +6 -0
- package/src/resources/extensions/coworker-scratchpad/helpers.test.ts +93 -0
- package/src/resources/extensions/coworker-scratchpad/helpers.ts +42 -0
- package/src/resources/extensions/coworker-scratchpad/index.test.ts +514 -0
- package/src/resources/extensions/coworker-scratchpad/index.ts +207 -0
- package/src/resources/extensions/coworker-scratchpad/mime-bundle.test.ts +61 -0
- package/src/resources/extensions/coworker-scratchpad/mime-bundle.ts +23 -0
- package/src/resources/extensions/coworker-scratchpad/scratchpad-tool.test.ts +137 -0
- package/src/resources/extensions/coworker-scratchpad/scratchpad-tool.ts +165 -0
- package/src/resources/extensions/coworker-scratchpad/session-sidecar.test.ts +133 -0
- package/src/resources/extensions/coworker-scratchpad/session-sidecar.ts +68 -0
- package/src/resources/extensions/coworker-scratchpad/sp-command.test.ts +836 -0
- package/src/resources/extensions/coworker-scratchpad/sp-command.ts +602 -0
- package/src/resources/extensions/coworker-scratchpad/workspace-pointer.test.ts +74 -0
- package/src/resources/extensions/coworker-scratchpad/workspace-pointer.ts +55 -0
- package/src/resources/extensions/coworker-scratchpad/workspace-root.test.ts +51 -0
- package/src/resources/extensions/coworker-scratchpad/workspace-root.ts +16 -0
- package/src/resources/extensions/coworker-vault/audit-command.test.ts +109 -0
- package/src/resources/extensions/coworker-vault/audit-command.ts +56 -0
- package/src/resources/extensions/coworker-vault/connect-command.test.ts +103 -0
- package/src/resources/extensions/coworker-vault/connect-command.ts +69 -0
- package/src/resources/extensions/coworker-vault/datasource-command.test.ts +80 -0
- package/src/resources/extensions/coworker-vault/datasource-command.ts +81 -0
- package/src/resources/extensions/coworker-vault/extension-manifest.json +12 -0
- package/src/resources/extensions/coworker-vault/index.test.ts +82 -0
- package/src/resources/extensions/coworker-vault/index.ts +181 -0
- package/src/resources/extensions/coworker-vault/test-helpers.ts +120 -0
- package/src/resources/extensions/coworker-vault/vault-singleton.test.ts +27 -0
- package/src/resources/extensions/coworker-vault/vault-singleton.ts +40 -0
- package/src/resources/extensions/otto/commands/release-notes/_data.ts +97 -0
- package/src/resources/extensions/otto/commands/release-notes/command.ts +16 -3
- package/src/resources/extensions/otto/index.ts +29 -6
- package/src/resources/extensions/shared/coworker-paths.test.ts +40 -0
- package/src/resources/extensions/shared/coworker-paths.ts +10 -0
- package/src/resources/extensions/slash-commands/{audit.ts → audit-codebase.ts} +4 -4
- package/src/resources/extensions/slash-commands/extension-manifest.json +1 -1
- package/src/resources/extensions/slash-commands/index.ts +2 -2
- package/src/resources/extensions/subagent/index.ts +9 -0
- package/src/resources/extensions/subagent/launch.test.ts +97 -0
- package/src/resources/extensions/subagent/launch.ts +42 -5
- package/src/resources/extensions/subagent/run-store.ts +3 -1
- package/src/resources/extensions/workflow/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/workflow/bootstrap/register-hooks.ts +10 -0
- package/src/resources/extensions/workflow/health-widget-core.ts +1 -1
- package/src/resources/extensions/workflow/persona-status.ts +109 -0
- package/src/resources/extensions/workflow/tests/auto-recovery.test.ts +34 -0
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { ChildProcessRuntime } from './child-process-runtime.js';
|
|
5
|
+
import { acquireLock, releaseLock } from './scratchpad-lock.js';
|
|
6
|
+
import { CellArchive } from './cell-archive.js';
|
|
7
|
+
import { projectTree, validateLeafId } from './cell-tree.js';
|
|
8
|
+
import { redactForJournal } from './kernel-bindings.js';
|
|
9
|
+
export class ForkKernelHangError extends Error {
|
|
10
|
+
srcName;
|
|
11
|
+
pid;
|
|
12
|
+
constructor(srcName, pid) {
|
|
13
|
+
super(`fork: source kernel for '${srcName}' (pid ${pid}) did not exit after SIGTERM + SIGKILL. Destination may be partially populated; clean up with /sp remove <dst>.`);
|
|
14
|
+
this.srcName = srcName;
|
|
15
|
+
this.pid = pid;
|
|
16
|
+
this.name = 'ForkKernelHangError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const DEFAULT_MAX_LIVE = 8;
|
|
20
|
+
const DEFAULT_IDLE_MS = 600_000;
|
|
21
|
+
const DEFAULT_SWEEP_MS = 30_000;
|
|
22
|
+
const META_SCHEMA_VERSION = 4;
|
|
23
|
+
const MAX_RECOVERY_NOTES = 20;
|
|
24
|
+
const FORK_EXIT_TIMEOUT_MS = 5000;
|
|
25
|
+
function raceWithTimeout(p, ms, label) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
let settled = false;
|
|
28
|
+
const timer = setTimeout(() => {
|
|
29
|
+
if (settled)
|
|
30
|
+
return;
|
|
31
|
+
settled = true;
|
|
32
|
+
reject(new Error(`timeout: ${label}`));
|
|
33
|
+
}, ms);
|
|
34
|
+
timer.unref();
|
|
35
|
+
p.then((v) => { if (settled)
|
|
36
|
+
return; settled = true; clearTimeout(timer); resolve(v); }, (e) => { if (settled)
|
|
37
|
+
return; settled = true; clearTimeout(timer); reject(e); });
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export class ScratchpadManager {
|
|
41
|
+
entries = new Map();
|
|
42
|
+
workspace;
|
|
43
|
+
root;
|
|
44
|
+
maxLive;
|
|
45
|
+
idleMs;
|
|
46
|
+
sessionId;
|
|
47
|
+
now;
|
|
48
|
+
runtimeOptions;
|
|
49
|
+
forkExitTimeoutMs;
|
|
50
|
+
injector;
|
|
51
|
+
audit;
|
|
52
|
+
onDataLoad;
|
|
53
|
+
onArtifactCreate;
|
|
54
|
+
getArtifactStore;
|
|
55
|
+
disposed = false;
|
|
56
|
+
sweepTimer = null;
|
|
57
|
+
constructor(options) {
|
|
58
|
+
this.workspace = options.workspace;
|
|
59
|
+
this.root = options.root ?? join(homedir(), '.otto', 'scratchpads');
|
|
60
|
+
this.maxLive = options.maxLiveKernels ?? DEFAULT_MAX_LIVE;
|
|
61
|
+
this.idleMs = options.idleMs ?? DEFAULT_IDLE_MS;
|
|
62
|
+
this.now = options.now ?? Date.now;
|
|
63
|
+
this.runtimeOptions = options.runtimeOptions ?? {};
|
|
64
|
+
this.sessionId = options.sessionId;
|
|
65
|
+
this.forkExitTimeoutMs = options.forkExitTimeoutMs ?? FORK_EXIT_TIMEOUT_MS;
|
|
66
|
+
this.injector = options.injector;
|
|
67
|
+
this.audit = options.audit;
|
|
68
|
+
this.onDataLoad = options.onDataLoad;
|
|
69
|
+
this.onArtifactCreate = options.onArtifactCreate;
|
|
70
|
+
this.getArtifactStore = options.getArtifactStore;
|
|
71
|
+
this.sweepTimer = setInterval(() => { void this.evictIdle(); }, options.sweepIntervalMs ?? DEFAULT_SWEEP_MS);
|
|
72
|
+
this.sweepTimer.unref();
|
|
73
|
+
}
|
|
74
|
+
dirFor(name) {
|
|
75
|
+
return join(this.root, name);
|
|
76
|
+
}
|
|
77
|
+
metaPath(name) {
|
|
78
|
+
return join(this.dirFor(name), 'meta.json');
|
|
79
|
+
}
|
|
80
|
+
existsOnDisk(name) {
|
|
81
|
+
return existsSync(this.metaPath(name));
|
|
82
|
+
}
|
|
83
|
+
payloadSize(dir) {
|
|
84
|
+
let total = 0;
|
|
85
|
+
for (const f of ['kernel.db', 'kernel.db.wal', 'namespace.json', 'cells.jsonl']) {
|
|
86
|
+
try {
|
|
87
|
+
total += statSync(join(dir, f)).size;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// not present -> skip (no-op contribution)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return total;
|
|
94
|
+
}
|
|
95
|
+
writeMetaAtomic(path, payload) {
|
|
96
|
+
const tmp = `${path}.tmp`;
|
|
97
|
+
writeFileSync(tmp, JSON.stringify(payload, null, 2));
|
|
98
|
+
renameSync(tmp, path);
|
|
99
|
+
}
|
|
100
|
+
writeMeta(name, initialBindings) {
|
|
101
|
+
const dir = this.dirFor(name);
|
|
102
|
+
const path = this.metaPath(name);
|
|
103
|
+
mkdirSync(dir, { recursive: true });
|
|
104
|
+
const nowIso = new Date(this.now()).toISOString();
|
|
105
|
+
let created_at = nowIso;
|
|
106
|
+
let attached_sessions = [];
|
|
107
|
+
// Phase 2 Task 12: bindings persistence + v3→v4 migration.
|
|
108
|
+
// - On first write (no prev), use the passed-in initialBindings (default []).
|
|
109
|
+
// - On subsequent writes, preserve whatever's on disk (migrating v3 → []).
|
|
110
|
+
let bindings = Array.isArray(initialBindings) ? [...initialBindings] : [];
|
|
111
|
+
const prevExtras = {};
|
|
112
|
+
if (existsSync(path)) {
|
|
113
|
+
try {
|
|
114
|
+
const prev = JSON.parse(readFileSync(path, 'utf8'));
|
|
115
|
+
if (typeof prev.created_at === 'string')
|
|
116
|
+
created_at = prev.created_at;
|
|
117
|
+
if (Array.isArray(prev.attached_sessions))
|
|
118
|
+
attached_sessions = prev.attached_sessions;
|
|
119
|
+
// v3 → v4 migration: bindings field is missing on v3; default to [].
|
|
120
|
+
bindings = Array.isArray(prev.bindings) ? prev.bindings : [];
|
|
121
|
+
for (const k of [
|
|
122
|
+
'last_snapshot_cell_id', 'last_snapshot_at', 'namespace_skipped', 'recovery_notes',
|
|
123
|
+
'cell_leaf_id', 'kernel_at_cell_id', 'recovery_notes_seen_at',
|
|
124
|
+
]) {
|
|
125
|
+
if (k in prev)
|
|
126
|
+
prevExtras[k] = prev[k];
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(prevExtras.recovery_notes)) {
|
|
129
|
+
const rn = prevExtras.recovery_notes;
|
|
130
|
+
prevExtras.recovery_notes = rn.slice(Math.max(0, rn.length - MAX_RECOVERY_NOTES));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// corrupt meta -> drop extras + rewrite fresh
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (this.sessionId && !attached_sessions.includes(this.sessionId)) {
|
|
138
|
+
attached_sessions.push(this.sessionId);
|
|
139
|
+
}
|
|
140
|
+
const archive = this.entries.get(name)?.archive;
|
|
141
|
+
if (archive && archive.leafId !== null) {
|
|
142
|
+
prevExtras.cell_leaf_id = archive.leafId;
|
|
143
|
+
}
|
|
144
|
+
const liveEntry = this.entries.get(name);
|
|
145
|
+
if (liveEntry && liveEntry.kernelAtCellId !== null) {
|
|
146
|
+
prevExtras.kernel_at_cell_id = liveEntry.kernelAtCellId;
|
|
147
|
+
}
|
|
148
|
+
const meta = {
|
|
149
|
+
name,
|
|
150
|
+
created_at,
|
|
151
|
+
last_used: nowIso,
|
|
152
|
+
attached_sessions,
|
|
153
|
+
bindings,
|
|
154
|
+
size_bytes: this.payloadSize(dir),
|
|
155
|
+
schema_version: META_SCHEMA_VERSION,
|
|
156
|
+
...prevExtras,
|
|
157
|
+
kernel_db: { present: existsSync(join(dir, 'kernel.db')), path: 'kernel.db' },
|
|
158
|
+
namespace: { present: existsSync(join(dir, 'namespace.json')), schema_version: 1 },
|
|
159
|
+
};
|
|
160
|
+
this.writeMetaAtomic(path, meta);
|
|
161
|
+
}
|
|
162
|
+
appendRecoveryNotes(name, notes) {
|
|
163
|
+
if (notes.length === 0)
|
|
164
|
+
return;
|
|
165
|
+
const path = this.metaPath(name);
|
|
166
|
+
if (!existsSync(path))
|
|
167
|
+
return; // no meta yet; nothing to attach notes to
|
|
168
|
+
let cur = {};
|
|
169
|
+
try {
|
|
170
|
+
cur = JSON.parse(readFileSync(path, 'utf8'));
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// corrupt meta -> do NOT rewrite as a fragment that destroys other fields.
|
|
174
|
+
// The next successful writeMeta call will re-establish a coherent shape.
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const prior = Array.isArray(cur.recovery_notes) ? cur.recovery_notes : [];
|
|
178
|
+
const stamped = notes.map((n) => ({ at: new Date(this.now()).toISOString(), ...n }));
|
|
179
|
+
const merged = [...prior, ...stamped];
|
|
180
|
+
cur.recovery_notes = merged.slice(Math.max(0, merged.length - MAX_RECOVERY_NOTES));
|
|
181
|
+
this.writeMetaAtomic(path, cur);
|
|
182
|
+
}
|
|
183
|
+
applySnapshotToMeta(name, entry, res) {
|
|
184
|
+
const path = this.metaPath(name);
|
|
185
|
+
if (!existsSync(path))
|
|
186
|
+
return;
|
|
187
|
+
let cur = {};
|
|
188
|
+
try {
|
|
189
|
+
cur = JSON.parse(readFileSync(path, 'utf8'));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
cur.last_snapshot_cell_id = entry.archive.lastId;
|
|
195
|
+
cur.last_snapshot_at = res.snapshotted_at;
|
|
196
|
+
cur.namespace_skipped = res.skipped;
|
|
197
|
+
cur.namespace = { present: true, schema_version: 1 };
|
|
198
|
+
cur.kernel_db = { present: existsSync(join(this.dirFor(name), 'kernel.db')), path: 'kernel.db' };
|
|
199
|
+
this.writeMetaAtomic(path, cur);
|
|
200
|
+
}
|
|
201
|
+
async snapshotThenDispose(name, entry) {
|
|
202
|
+
const rt = entry.runtime;
|
|
203
|
+
if (!rt)
|
|
204
|
+
return;
|
|
205
|
+
if (rt.hasActiveCell) {
|
|
206
|
+
// An active cell would block the snapshot indefinitely until cellTimeoutMs fires
|
|
207
|
+
// (kernel processes one NDJSON frame at a time). Skip the snapshot and dispose
|
|
208
|
+
// straight away; the next attach will see cells-since-snapshot divergence.
|
|
209
|
+
this.appendRecoveryNotes(name, [{ kind: 'snapshot-failed', message: 'skipped: active cell would block snapshot' }]);
|
|
210
|
+
await rt.dispose();
|
|
211
|
+
if (entry.runtime === rt)
|
|
212
|
+
entry.runtime = null;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const res = await rt.snapshot();
|
|
216
|
+
if (res.ok) {
|
|
217
|
+
this.applySnapshotToMeta(name, entry, res);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
this.appendRecoveryNotes(name, [{ kind: 'snapshot-failed', message: res.error.message }]);
|
|
221
|
+
}
|
|
222
|
+
await rt.dispose();
|
|
223
|
+
// Only null the field if no concurrent caller has already replaced or cleared it.
|
|
224
|
+
if (entry.runtime === rt)
|
|
225
|
+
entry.runtime = null;
|
|
226
|
+
}
|
|
227
|
+
ingestRecoveryNotesOnAttach(name, entry) {
|
|
228
|
+
const notes = [...entry.runtime.recoveryNotes];
|
|
229
|
+
// Divergence: compare archive.lastId to last_snapshot_cell_id on disk.
|
|
230
|
+
const path = this.metaPath(name);
|
|
231
|
+
if (existsSync(path)) {
|
|
232
|
+
try {
|
|
233
|
+
const cur = JSON.parse(readFileSync(path, 'utf8'));
|
|
234
|
+
const last = cur.last_snapshot_cell_id;
|
|
235
|
+
const archiveId = entry.archive.lastId;
|
|
236
|
+
if (typeof last === 'number' && typeof archiveId === 'number' && archiveId > last) {
|
|
237
|
+
notes.push({ kind: 'cells-since-snapshot', n: archiveId - last });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// ignore; covered by the namespace-corrupt note path
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
this.appendRecoveryNotes(name, notes);
|
|
245
|
+
}
|
|
246
|
+
restoreLeafOnAttach(name, entry) {
|
|
247
|
+
const path = this.metaPath(name);
|
|
248
|
+
if (!existsSync(path))
|
|
249
|
+
return;
|
|
250
|
+
try {
|
|
251
|
+
const cur = JSON.parse(readFileSync(path, 'utf8'));
|
|
252
|
+
const persisted = cur.cell_leaf_id;
|
|
253
|
+
if (typeof persisted === 'number' && entry.archive.leafId !== persisted) {
|
|
254
|
+
entry.archive.setLeaf(persisted);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// ignore; leaf falls back to file-max (the constructor's default).
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
restoreKernelAtCellIdOnAttach(name, entry) {
|
|
262
|
+
// Cold restore: kernel was hydrated from namespace.json, which was written at
|
|
263
|
+
// last_snapshot_cell_id. That's where the in-VM state lives.
|
|
264
|
+
const path = this.metaPath(name);
|
|
265
|
+
if (!existsSync(path)) {
|
|
266
|
+
entry.kernelAtCellId = null;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const cur = JSON.parse(readFileSync(path, 'utf8'));
|
|
271
|
+
const last = cur.last_snapshot_cell_id;
|
|
272
|
+
entry.kernelAtCellId = typeof last === 'number' ? last : null;
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
entry.kernelAtCellId = null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
warmCount() {
|
|
279
|
+
let n = 0;
|
|
280
|
+
for (const e of this.entries.values())
|
|
281
|
+
if (e.runtime !== null)
|
|
282
|
+
n++;
|
|
283
|
+
return n;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Phase 2 Task 13: read meta.bindings from disk so each fresh spawn picks up
|
|
287
|
+
* the current binding set (bindings can change between attaches via /sp use).
|
|
288
|
+
* Returns [] if meta.json doesn't exist or doesn't have a v4 bindings array.
|
|
289
|
+
*
|
|
290
|
+
* Phase 2 Task 16: also surfaced as a public read for /sp list rendering and
|
|
291
|
+
* staleness-banner emission. Stays a thin read; callers that need to mutate
|
|
292
|
+
* use addBinding / removeBinding (which atomically RMW meta.json).
|
|
293
|
+
*/
|
|
294
|
+
readBindings(name) {
|
|
295
|
+
const path = this.metaPath(name);
|
|
296
|
+
if (!existsSync(path))
|
|
297
|
+
return [];
|
|
298
|
+
try {
|
|
299
|
+
const cur = JSON.parse(readFileSync(path, 'utf8'));
|
|
300
|
+
return Array.isArray(cur.bindings) ? cur.bindings : [];
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Phase 2 Task 16: append a binding ref (e.g. 'jira:prod') to meta.bindings.
|
|
308
|
+
* Idempotent — adding a ref already in the list is a no-op (the meta.json
|
|
309
|
+
* write still happens so callers can detect "added" vs "noop" only via the
|
|
310
|
+
* returned tuple). Atomically rewrites meta.json via writeMetaAtomic, so
|
|
311
|
+
* concurrent writers cannot interleave. Caller is responsible for validating
|
|
312
|
+
* `ref` (sp-command uses LocalDataVault.parseRef before invoking).
|
|
313
|
+
*/
|
|
314
|
+
async addBinding(name, ref) {
|
|
315
|
+
this.assertNotDisposed();
|
|
316
|
+
if (!this.existsOnDisk(name))
|
|
317
|
+
throw new Error(`scratchpad not found: ${name}`);
|
|
318
|
+
const path = this.metaPath(name);
|
|
319
|
+
let cur = {};
|
|
320
|
+
try {
|
|
321
|
+
cur = JSON.parse(readFileSync(path, 'utf8'));
|
|
322
|
+
}
|
|
323
|
+
catch { /* corrupt -> overwrite */ }
|
|
324
|
+
const bindings = Array.isArray(cur.bindings) ? [...cur.bindings] : [];
|
|
325
|
+
if (bindings.includes(ref)) {
|
|
326
|
+
return { added: false };
|
|
327
|
+
}
|
|
328
|
+
bindings.push(ref);
|
|
329
|
+
cur.bindings = bindings;
|
|
330
|
+
if (typeof cur.schema_version !== 'number')
|
|
331
|
+
cur.schema_version = META_SCHEMA_VERSION;
|
|
332
|
+
this.writeMetaAtomic(path, cur);
|
|
333
|
+
return { added: true };
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Phase 2 Task 16: remove a binding ref from meta.bindings. Returns whether
|
|
337
|
+
* a removal happened so callers can emit "no such binding" if needed.
|
|
338
|
+
*/
|
|
339
|
+
async removeBinding(name, ref) {
|
|
340
|
+
this.assertNotDisposed();
|
|
341
|
+
if (!this.existsOnDisk(name))
|
|
342
|
+
throw new Error(`scratchpad not found: ${name}`);
|
|
343
|
+
const path = this.metaPath(name);
|
|
344
|
+
let cur = {};
|
|
345
|
+
try {
|
|
346
|
+
cur = JSON.parse(readFileSync(path, 'utf8'));
|
|
347
|
+
}
|
|
348
|
+
catch { /* corrupt -> overwrite */ }
|
|
349
|
+
const bindings = Array.isArray(cur.bindings) ? [...cur.bindings] : [];
|
|
350
|
+
const idx = bindings.indexOf(ref);
|
|
351
|
+
if (idx < 0) {
|
|
352
|
+
return { removed: false };
|
|
353
|
+
}
|
|
354
|
+
bindings.splice(idx, 1);
|
|
355
|
+
cur.bindings = bindings;
|
|
356
|
+
if (typeof cur.schema_version !== 'number')
|
|
357
|
+
cur.schema_version = META_SCHEMA_VERSION;
|
|
358
|
+
this.writeMetaAtomic(path, cur);
|
|
359
|
+
return { removed: true };
|
|
360
|
+
}
|
|
361
|
+
async spawnRuntime(name) {
|
|
362
|
+
// Phase 3 Task 19: bridge the manager-level onDataLoad (which receives
|
|
363
|
+
// the scratchpad name) to the runtime-level onDataLoad (which doesn't).
|
|
364
|
+
// Closure-bound to `name` here so each spawned runtime tags its drawers
|
|
365
|
+
// with the correct scratchpad even when the manager is shared. If both
|
|
366
|
+
// the manager and runtimeOptions supply an onDataLoad, the manager's
|
|
367
|
+
// wins — runtimeOptions.onDataLoad never had a way to know the name.
|
|
368
|
+
const fanout = this.onDataLoad;
|
|
369
|
+
const onDataLoad = fanout
|
|
370
|
+
? (drawer) => fanout(drawer, name)
|
|
371
|
+
: this.runtimeOptions.onDataLoad;
|
|
372
|
+
// Phase 4 Task 10: mirror the onDataLoad fan-out pattern for artifact_create.
|
|
373
|
+
// Closure-binds the scratchpad name so multi-pad sessions tag drawers correctly.
|
|
374
|
+
const fanArtifactCreate = this.onArtifactCreate;
|
|
375
|
+
const onArtifactCreate = fanArtifactCreate
|
|
376
|
+
? (drawer) => fanArtifactCreate(drawer, name)
|
|
377
|
+
: this.runtimeOptions.onArtifactCreate;
|
|
378
|
+
// Phase 4 Task 10: parent-side RPC handlers backed by the ArtifactStore.
|
|
379
|
+
// Both handlers look up the store lazily through getArtifactStore() so the
|
|
380
|
+
// host can toggle availability without re-spawning the kernel. When the
|
|
381
|
+
// store is unavailable we throw — the runtime converts the throw into an
|
|
382
|
+
// `ok:false` response frame so the awaiting cell rejects cleanly.
|
|
383
|
+
const getStore = this.getArtifactStore;
|
|
384
|
+
const handleArtifactCreate = getStore
|
|
385
|
+
? async (req) => {
|
|
386
|
+
const store = getStore();
|
|
387
|
+
if (!store)
|
|
388
|
+
throw new Error('artifacts unavailable');
|
|
389
|
+
const handle = await store.create(req.kind, req.name);
|
|
390
|
+
return {
|
|
391
|
+
slug: handle.slug,
|
|
392
|
+
uri: handle.uri,
|
|
393
|
+
primary_path: handle.primaryPath,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
: this.runtimeOptions.handleArtifactCreate;
|
|
397
|
+
const handleArtifactUpdate = getStore
|
|
398
|
+
? async (req) => {
|
|
399
|
+
const store = getStore();
|
|
400
|
+
if (!store)
|
|
401
|
+
throw new Error('artifacts unavailable');
|
|
402
|
+
const handle = await store.get(req.slug);
|
|
403
|
+
if (!handle)
|
|
404
|
+
throw new Error(`unknown artifact: ${req.slug}`);
|
|
405
|
+
return await store.update(handle, req.files);
|
|
406
|
+
}
|
|
407
|
+
: this.runtimeOptions.handleArtifactUpdate;
|
|
408
|
+
const rt = new ChildProcessRuntime({
|
|
409
|
+
workspace: this.workspace,
|
|
410
|
+
scratchpadDir: this.dirFor(name),
|
|
411
|
+
...this.runtimeOptions,
|
|
412
|
+
onDataLoad,
|
|
413
|
+
onArtifactCreate,
|
|
414
|
+
handleArtifactCreate,
|
|
415
|
+
handleArtifactUpdate,
|
|
416
|
+
// Phase 2 Task 13: env-injection wiring. Injector + bindings are read
|
|
417
|
+
// fresh on every spawn so cold-restarts and re-attaches see the latest
|
|
418
|
+
// bindings list. scratchpadName + sessionId stamp the injector's audit
|
|
419
|
+
// records so /audit can attribute each inject to a scratchpad+session.
|
|
420
|
+
injector: this.injector,
|
|
421
|
+
bindings: this.readBindings(name),
|
|
422
|
+
scratchpadName: name,
|
|
423
|
+
sessionId: this.sessionId ?? '',
|
|
424
|
+
});
|
|
425
|
+
await rt.start();
|
|
426
|
+
return rt;
|
|
427
|
+
}
|
|
428
|
+
async evictLruIfNeeded() {
|
|
429
|
+
while (this.warmCount() >= this.maxLive) {
|
|
430
|
+
let victim = null;
|
|
431
|
+
for (const e of this.entries.values()) {
|
|
432
|
+
if (e.runtime === null)
|
|
433
|
+
continue; // already cold
|
|
434
|
+
if (e.runtime.hasActiveCell)
|
|
435
|
+
continue; // never evict a busy kernel
|
|
436
|
+
if (victim === null || e.lastUsedAt < victim.lastUsedAt)
|
|
437
|
+
victim = e;
|
|
438
|
+
}
|
|
439
|
+
if (victim === null)
|
|
440
|
+
break; // every warm kernel is busy; pool may momentarily exceed (documented)
|
|
441
|
+
// The map key for the LRU victim is needed for snapshotThenDispose; find it now.
|
|
442
|
+
let victimName = null;
|
|
443
|
+
for (const [n, e] of this.entries) {
|
|
444
|
+
if (e === victim) {
|
|
445
|
+
victimName = n;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (victimName === null)
|
|
450
|
+
break; // defensive; should be impossible
|
|
451
|
+
await this.snapshotThenDispose(victimName, victim);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async create(name, opts = {}) {
|
|
455
|
+
this.assertNotDisposed();
|
|
456
|
+
if (this.entries.has(name) || this.existsOnDisk(name)) {
|
|
457
|
+
throw new Error(`scratchpad ${name} already exists`);
|
|
458
|
+
}
|
|
459
|
+
return this.attachUnmanaged(name, opts);
|
|
460
|
+
}
|
|
461
|
+
async getOrAttach(name, opts = {}) {
|
|
462
|
+
this.assertNotDisposed();
|
|
463
|
+
const existing = this.entries.get(name);
|
|
464
|
+
if (existing) {
|
|
465
|
+
existing.lastUsedAt = this.now();
|
|
466
|
+
if (existing.runtime)
|
|
467
|
+
return existing.runtime;
|
|
468
|
+
await this.evictLruIfNeeded();
|
|
469
|
+
existing.runtime = await this.spawnRuntime(name); // cold -> warm; namespace restored from disk (1d2)
|
|
470
|
+
this.ingestRecoveryNotesOnAttach(name, existing);
|
|
471
|
+
this.restoreLeafOnAttach(name, existing);
|
|
472
|
+
this.restoreKernelAtCellIdOnAttach(name, existing);
|
|
473
|
+
return existing.runtime;
|
|
474
|
+
}
|
|
475
|
+
return this.attachUnmanaged(name, opts);
|
|
476
|
+
}
|
|
477
|
+
async runCell(name, code, opts = {}) {
|
|
478
|
+
this.assertNotDisposed();
|
|
479
|
+
const runtime = await this.getOrAttach(name, opts);
|
|
480
|
+
const entry = this.entries.get(name);
|
|
481
|
+
entry.lastUsedAt = this.now();
|
|
482
|
+
// Phase 2 Task 14: forecast the id the archive will assign to this cell so
|
|
483
|
+
// any secret-scanner audit records carry the same cell_id the journal entry
|
|
484
|
+
// will get. archive.lastId may be null for an empty archive (id 1 is next).
|
|
485
|
+
const nextCellId = (entry.archive.lastId ?? 0) + 1;
|
|
486
|
+
try {
|
|
487
|
+
const result = await runtime.runCell(code);
|
|
488
|
+
// Phase 2 Task 14: live TUI output is UPSTREAM — `result` flows back to
|
|
489
|
+
// the tool unchanged for display. Only the journal copy of stdout is
|
|
490
|
+
// passed through redactForJournal. When no audit is plumbed, redaction
|
|
491
|
+
// is a pass-through (backward compat).
|
|
492
|
+
const journalStdout = this.redactStdout(result.stdout, name, nextCellId);
|
|
493
|
+
entry.archive.append({ code, ok: true, value: result.value, stdout: journalStdout });
|
|
494
|
+
entry.kernelAtCellId = entry.archive.lastId;
|
|
495
|
+
this.writeMeta(name);
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
const e = err;
|
|
500
|
+
try {
|
|
501
|
+
// Phase 2 Task 14: redact the error message too — cell exceptions can
|
|
502
|
+
// embed user data in `e.message`. stdout is empty in this branch.
|
|
503
|
+
const journalErrMsg = this.redactStdout(e.message, name, nextCellId);
|
|
504
|
+
entry.archive.append({ code, ok: false, error: { name: e.name, message: journalErrMsg }, stdout: '' });
|
|
505
|
+
entry.kernelAtCellId = entry.archive.lastId;
|
|
506
|
+
this.writeMeta(name);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
// recording the failure must never mask the original cell error
|
|
510
|
+
}
|
|
511
|
+
throw err;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Phase 2 Task 14: redact known-secret patterns from a cell-output string
|
|
516
|
+
* before journaling. No-op (pass-through) when no AuditLog is configured —
|
|
517
|
+
* the manager was constructed in test/legacy mode without vault wiring.
|
|
518
|
+
*/
|
|
519
|
+
redactStdout(raw, scratchpadName, cellId) {
|
|
520
|
+
if (!this.audit)
|
|
521
|
+
return raw;
|
|
522
|
+
return redactForJournal(raw, {
|
|
523
|
+
audit: this.audit,
|
|
524
|
+
sessionId: this.sessionId ?? '',
|
|
525
|
+
scratchpadName,
|
|
526
|
+
pid: process.pid,
|
|
527
|
+
cellId: String(cellId),
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Task D: Release a warm kernel's process+memory while preserving on-disk state
|
|
532
|
+
* (kernel.db, namespace.json, cells.jsonl, meta.json, lock.json). Cold-restart
|
|
533
|
+
* happens on the next attach.
|
|
534
|
+
*
|
|
535
|
+
* Without --force: refuses if a cell is mid-execution. With --force: cancels the
|
|
536
|
+
* active cell via runtime.cancel() (SIGINT → SIGTERM → SIGKILL escalation handled
|
|
537
|
+
* internally by ChildProcessRuntime). Post-cancel the kernel is dead, so we skip
|
|
538
|
+
* the snapshot — the next attach replays from cells.jsonl.
|
|
539
|
+
*/
|
|
540
|
+
async evict(name, opts = {}) {
|
|
541
|
+
this.assertNotDisposed();
|
|
542
|
+
const entry = this.entries.get(name);
|
|
543
|
+
if (!entry || !entry.runtime) {
|
|
544
|
+
throw new Error(`scratchpad ${name} is not warm (already cold)`);
|
|
545
|
+
}
|
|
546
|
+
if (entry.runtime.hasActiveCell) {
|
|
547
|
+
if (!opts.force) {
|
|
548
|
+
throw new Error(`cannot evict ${name}: cell is running (use --force to interrupt)`);
|
|
549
|
+
}
|
|
550
|
+
// --force: cancel via existing SIGINT → SIGTERM → SIGKILL escalation in
|
|
551
|
+
// ChildProcessRuntime.cancel(). After cancel resolves the runtime is dead,
|
|
552
|
+
// so snapshotThenDispose would fail. Skip the snapshot and flip the entry
|
|
553
|
+
// to cold (runtime=null) like snapshotThenDispose would have. The session
|
|
554
|
+
// lock remains held so /sp list still shows the (cold) entry and the next
|
|
555
|
+
// attach cold-restarts from cells.jsonl.
|
|
556
|
+
const rt = entry.runtime;
|
|
557
|
+
await rt.cancel();
|
|
558
|
+
try {
|
|
559
|
+
await rt.dispose();
|
|
560
|
+
}
|
|
561
|
+
catch { /* already dead — best-effort cleanup */ }
|
|
562
|
+
if (entry.runtime === rt)
|
|
563
|
+
entry.runtime = null;
|
|
564
|
+
return { interrupted: true };
|
|
565
|
+
}
|
|
566
|
+
await this.snapshotThenDispose(name, entry);
|
|
567
|
+
return { interrupted: false };
|
|
568
|
+
}
|
|
569
|
+
async setLeaf(name, id) {
|
|
570
|
+
this.assertNotDisposed();
|
|
571
|
+
// Verify the scratchpad exists on disk (works for both warm and cold).
|
|
572
|
+
if (!this.existsOnDisk(name))
|
|
573
|
+
throw new Error(`scratchpad not found: ${name}`);
|
|
574
|
+
// Build a tree from the on-disk cells.jsonl so validation works even when cold.
|
|
575
|
+
const cells = [];
|
|
576
|
+
const cellsPath = join(this.dirFor(name), 'cells.jsonl');
|
|
577
|
+
if (existsSync(cellsPath)) {
|
|
578
|
+
for (const line of readFileSync(cellsPath, 'utf8').split('\n')) {
|
|
579
|
+
if (!line.trim())
|
|
580
|
+
continue;
|
|
581
|
+
try {
|
|
582
|
+
const obj = JSON.parse(line);
|
|
583
|
+
if (typeof obj.id === 'number')
|
|
584
|
+
cells.push(obj);
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
// header or trailing-corrupt line -> skip
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const tree = projectTree(cells);
|
|
592
|
+
validateLeafId(tree, id);
|
|
593
|
+
// Warm path: update the live archive too.
|
|
594
|
+
const entry = this.entries.get(name);
|
|
595
|
+
if (entry)
|
|
596
|
+
entry.archive.setLeaf(id);
|
|
597
|
+
// Direct meta update so cold scratchpads persist the leaf.
|
|
598
|
+
const path = this.metaPath(name);
|
|
599
|
+
let cur = {};
|
|
600
|
+
try {
|
|
601
|
+
cur = JSON.parse(readFileSync(path, 'utf8'));
|
|
602
|
+
}
|
|
603
|
+
catch { /* fall through */ }
|
|
604
|
+
cur.cell_leaf_id = id;
|
|
605
|
+
// Phase 2 Task 12: bumping schema_version here doubles as a migration step
|
|
606
|
+
// for cold-only flows; ensure bindings exists so the v4 invariant holds.
|
|
607
|
+
if (!Array.isArray(cur.bindings))
|
|
608
|
+
cur.bindings = [];
|
|
609
|
+
cur.schema_version = META_SCHEMA_VERSION;
|
|
610
|
+
this.writeMetaAtomic(path, cur);
|
|
611
|
+
}
|
|
612
|
+
async fork(srcName, dstName) {
|
|
613
|
+
this.assertNotDisposed();
|
|
614
|
+
if (this.entries.has(dstName) || this.existsOnDisk(dstName)) {
|
|
615
|
+
throw new Error(`scratchpad ${dstName} already exists`);
|
|
616
|
+
}
|
|
617
|
+
if (!this.existsOnDisk(srcName)) {
|
|
618
|
+
throw new Error(`scratchpad not found: ${srcName}`);
|
|
619
|
+
}
|
|
620
|
+
// Auto-evict src to release the DuckDB kernel.db handle before we copy.
|
|
621
|
+
// Capture the raw child process reference before disposal so we can await its
|
|
622
|
+
// full exit — ensuring DuckDB flushes and closes kernel.db before copyFileSync.
|
|
623
|
+
const srcEntry = this.entries.get(srcName);
|
|
624
|
+
if (srcEntry && srcEntry.runtime) {
|
|
625
|
+
const rawChild = srcEntry.runtime.child;
|
|
626
|
+
await this.snapshotThenDispose(srcName, srcEntry);
|
|
627
|
+
if (rawChild && rawChild.exitCode === null) {
|
|
628
|
+
const exitPromise = new Promise((resolve) => rawChild.once('exit', () => resolve()));
|
|
629
|
+
try {
|
|
630
|
+
await raceWithTimeout(exitPromise, this.forkExitTimeoutMs, 'exit-after-SIGTERM');
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
rawChild.kill('SIGKILL');
|
|
634
|
+
const exitPromise2 = new Promise((resolve) => rawChild.once('exit', () => resolve()));
|
|
635
|
+
try {
|
|
636
|
+
await raceWithTimeout(exitPromise2, this.forkExitTimeoutMs, 'exit-after-SIGKILL');
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
throw new ForkKernelHangError(srcName, rawChild.pid ?? -1);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const srcDir = this.dirFor(srcName);
|
|
645
|
+
const dstDir = this.dirFor(dstName);
|
|
646
|
+
mkdirSync(dstDir, { recursive: true });
|
|
647
|
+
for (const file of ['kernel.db', 'kernel.db.wal', 'namespace.json', 'cells.jsonl']) {
|
|
648
|
+
if (existsSync(join(srcDir, file))) {
|
|
649
|
+
copyFileSync(join(srcDir, file), join(dstDir, file));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// Build dst meta inheriting selected fields from src.
|
|
653
|
+
let srcMeta = {};
|
|
654
|
+
try {
|
|
655
|
+
srcMeta = JSON.parse(readFileSync(join(srcDir, 'meta.json'), 'utf8'));
|
|
656
|
+
}
|
|
657
|
+
catch { /* leave empty */ }
|
|
658
|
+
const nowIso = new Date(this.now()).toISOString();
|
|
659
|
+
const dstMeta = {
|
|
660
|
+
name: dstName,
|
|
661
|
+
created_at: nowIso,
|
|
662
|
+
last_used: nowIso,
|
|
663
|
+
attached_sessions: this.sessionId ? [this.sessionId] : [],
|
|
664
|
+
// Phase 2 Task 16: fork inherits src's bindings so the forked scratchpad
|
|
665
|
+
// spawns its kernel with the same OTTO_DS_* env block. Users can
|
|
666
|
+
// /sp unuse on dst afterwards if they want a different binding shape.
|
|
667
|
+
bindings: Array.isArray(srcMeta.bindings) ? [...srcMeta.bindings] : [],
|
|
668
|
+
size_bytes: this.payloadSize(dstDir),
|
|
669
|
+
schema_version: META_SCHEMA_VERSION,
|
|
670
|
+
cell_leaf_id: typeof srcMeta.cell_leaf_id === 'number' ? srcMeta.cell_leaf_id : null,
|
|
671
|
+
last_snapshot_cell_id: typeof srcMeta.last_snapshot_cell_id === 'number' ? srcMeta.last_snapshot_cell_id : null,
|
|
672
|
+
last_snapshot_at: typeof srcMeta.last_snapshot_at === 'string' ? srcMeta.last_snapshot_at : null,
|
|
673
|
+
kernel_at_cell_id: typeof srcMeta.kernel_at_cell_id === 'number'
|
|
674
|
+
? srcMeta.kernel_at_cell_id
|
|
675
|
+
: (typeof srcMeta.last_snapshot_cell_id === 'number' ? srcMeta.last_snapshot_cell_id : null),
|
|
676
|
+
namespace_skipped: [],
|
|
677
|
+
recovery_notes: [],
|
|
678
|
+
kernel_db: { present: existsSync(join(dstDir, 'kernel.db')), path: 'kernel.db' },
|
|
679
|
+
namespace: { present: existsSync(join(dstDir, 'namespace.json')), schema_version: 1 },
|
|
680
|
+
};
|
|
681
|
+
this.writeMetaAtomic(join(dstDir, 'meta.json'), dstMeta);
|
|
682
|
+
// Claim the new scratchpad for this session by acquiring its lock and registering
|
|
683
|
+
// a cold entry so getOrAttach can re-warm without re-acquiring the lock.
|
|
684
|
+
const dstLock = acquireLock(dstDir, { now: this.now });
|
|
685
|
+
const dstEntry = {
|
|
686
|
+
runtime: null,
|
|
687
|
+
lock: dstLock,
|
|
688
|
+
lastUsedAt: this.now(),
|
|
689
|
+
archive: new CellArchive(dstDir, this.now),
|
|
690
|
+
kernelAtCellId: dstMeta.kernel_at_cell_id,
|
|
691
|
+
};
|
|
692
|
+
this.entries.set(dstName, dstEntry);
|
|
693
|
+
}
|
|
694
|
+
async clearHistory(name) {
|
|
695
|
+
this.assertNotDisposed();
|
|
696
|
+
const entry = this.entries.get(name);
|
|
697
|
+
if (entry?.runtime?.hasActiveCell) {
|
|
698
|
+
throw new Error('cannot clear history while a cell is running');
|
|
699
|
+
}
|
|
700
|
+
if (entry?.archive) {
|
|
701
|
+
entry.archive.reset();
|
|
702
|
+
entry.kernelAtCellId = null;
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
// Cold path: construct a temp archive solely to reuse its truncation logic.
|
|
706
|
+
const tmpArchive = new CellArchive(this.dirFor(name), this.now);
|
|
707
|
+
tmpArchive.reset();
|
|
708
|
+
}
|
|
709
|
+
// Direct meta read-modify-write; we explicitly do NOT route through writeMeta
|
|
710
|
+
// because writeMeta pulls cell_leaf_id from the live archive — which is exactly
|
|
711
|
+
// what we just nulled, but writeMeta would also re-add this.sessionId, which
|
|
712
|
+
// we want preserved untouched here. Safer to read+merge+write directly.
|
|
713
|
+
const path = this.metaPath(name);
|
|
714
|
+
if (existsSync(path)) {
|
|
715
|
+
let cur = {};
|
|
716
|
+
try {
|
|
717
|
+
cur = JSON.parse(readFileSync(path, 'utf8'));
|
|
718
|
+
}
|
|
719
|
+
catch { /* drop */ }
|
|
720
|
+
cur.cell_leaf_id = null;
|
|
721
|
+
cur.last_snapshot_cell_id = null;
|
|
722
|
+
cur.last_snapshot_at = null;
|
|
723
|
+
cur.kernel_at_cell_id = null;
|
|
724
|
+
this.writeMetaAtomic(path, cur);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
async save(name) {
|
|
728
|
+
this.assertNotDisposed();
|
|
729
|
+
const entry = this.entries.get(name);
|
|
730
|
+
if (!entry || !entry.runtime) {
|
|
731
|
+
throw new Error(`scratchpad ${name} is not warm — nothing to save`);
|
|
732
|
+
}
|
|
733
|
+
if (entry.runtime.hasActiveCell) {
|
|
734
|
+
this.appendRecoveryNotes(name, [{ kind: 'snapshot-failed', message: 'active cell' }]);
|
|
735
|
+
throw new Error('cannot save while a cell is running');
|
|
736
|
+
}
|
|
737
|
+
const res = await entry.runtime.snapshot();
|
|
738
|
+
if (res.ok) {
|
|
739
|
+
this.applySnapshotToMeta(name, entry, res);
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
this.appendRecoveryNotes(name, [{ kind: 'snapshot-failed', message: res.error.message }]);
|
|
743
|
+
throw new Error(`save failed: ${res.error.message}`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
async detach(name, sessionId) {
|
|
747
|
+
this.assertNotDisposed();
|
|
748
|
+
const path = this.metaPath(name);
|
|
749
|
+
if (!existsSync(path))
|
|
750
|
+
return;
|
|
751
|
+
let cur = {};
|
|
752
|
+
try {
|
|
753
|
+
cur = JSON.parse(readFileSync(path, 'utf8'));
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const arr = Array.isArray(cur.attached_sessions) ? cur.attached_sessions : [];
|
|
759
|
+
const idx = arr.indexOf(sessionId);
|
|
760
|
+
if (idx >= 0) {
|
|
761
|
+
cur.attached_sessions = [...arr.slice(0, idx), ...arr.slice(idx + 1)];
|
|
762
|
+
this.writeMetaAtomic(path, cur);
|
|
763
|
+
}
|
|
764
|
+
// Runtime explicitly NOT disposed. Pool LRU/idle eviction owns cleanup.
|
|
765
|
+
}
|
|
766
|
+
async markRecoveryNotesSeen(name) {
|
|
767
|
+
this.assertNotDisposed();
|
|
768
|
+
const path = this.metaPath(name);
|
|
769
|
+
if (!existsSync(path))
|
|
770
|
+
return;
|
|
771
|
+
let cur = {};
|
|
772
|
+
try {
|
|
773
|
+
cur = JSON.parse(readFileSync(path, 'utf8'));
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
cur.recovery_notes_seen_at = new Date(this.now()).toISOString();
|
|
779
|
+
this.writeMetaAtomic(path, cur);
|
|
780
|
+
}
|
|
781
|
+
async attachUnmanaged(name, opts) {
|
|
782
|
+
const dir = this.dirFor(name);
|
|
783
|
+
const lock = acquireLock(dir, {
|
|
784
|
+
forceTakeover: opts.forceTakeover,
|
|
785
|
+
takeoverReason: opts.takeoverReason,
|
|
786
|
+
now: this.now,
|
|
787
|
+
});
|
|
788
|
+
// Phase 2 Task 12: opts.bindings is only honored on first create; on re-attach
|
|
789
|
+
// the on-disk bindings field wins via prevExtras in writeMeta.
|
|
790
|
+
this.writeMeta(name, opts.bindings);
|
|
791
|
+
await this.evictLruIfNeeded();
|
|
792
|
+
let runtime;
|
|
793
|
+
try {
|
|
794
|
+
runtime = await this.spawnRuntime(name);
|
|
795
|
+
}
|
|
796
|
+
catch (err) {
|
|
797
|
+
releaseLock(dir); // don't leak the lock if spawn fails
|
|
798
|
+
throw err;
|
|
799
|
+
}
|
|
800
|
+
this.writeMeta(name); // refresh: kernel.db is now on disk; payloadSize + kernel_db.present become accurate (Task E / Issue #2)
|
|
801
|
+
const entry = { runtime, lock, lastUsedAt: this.now(), archive: new CellArchive(dir, this.now), kernelAtCellId: null };
|
|
802
|
+
this.entries.set(name, entry);
|
|
803
|
+
this.ingestRecoveryNotesOnAttach(name, entry);
|
|
804
|
+
this.restoreLeafOnAttach(name, entry);
|
|
805
|
+
this.restoreKernelAtCellIdOnAttach(name, entry);
|
|
806
|
+
return runtime;
|
|
807
|
+
}
|
|
808
|
+
list() {
|
|
809
|
+
return [...this.entries].map(([name, e]) => ({
|
|
810
|
+
name,
|
|
811
|
+
live: e.runtime !== null,
|
|
812
|
+
lastUsedAt: e.lastUsedAt,
|
|
813
|
+
hasActiveCell: e.runtime?.hasActiveCell ?? false,
|
|
814
|
+
}));
|
|
815
|
+
}
|
|
816
|
+
async remove(name) {
|
|
817
|
+
const entry = this.entries.get(name);
|
|
818
|
+
if (entry) {
|
|
819
|
+
await entry.runtime?.dispose();
|
|
820
|
+
this.entries.delete(name);
|
|
821
|
+
}
|
|
822
|
+
rmSync(this.dirFor(name), { recursive: true, force: true }); // deletes lock.json + meta.json
|
|
823
|
+
}
|
|
824
|
+
async evictIdle() {
|
|
825
|
+
if (this.disposed)
|
|
826
|
+
return;
|
|
827
|
+
const cutoff = this.now() - this.idleMs;
|
|
828
|
+
for (const e of this.entries.values()) {
|
|
829
|
+
if (e.runtime === null)
|
|
830
|
+
continue;
|
|
831
|
+
if (e.runtime.hasActiveCell)
|
|
832
|
+
continue; // never evict a busy kernel
|
|
833
|
+
if (e.lastUsedAt <= cutoff) {
|
|
834
|
+
// Find the name for this entry to feed snapshotThenDispose.
|
|
835
|
+
let entryName = null;
|
|
836
|
+
for (const [n, ent] of this.entries) {
|
|
837
|
+
if (ent === e) {
|
|
838
|
+
entryName = n;
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (entryName !== null)
|
|
843
|
+
await this.snapshotThenDispose(entryName, e);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async disposeAll() {
|
|
848
|
+
if (this.disposed)
|
|
849
|
+
return;
|
|
850
|
+
this.disposed = true;
|
|
851
|
+
if (this.sweepTimer) {
|
|
852
|
+
clearInterval(this.sweepTimer);
|
|
853
|
+
this.sweepTimer = null;
|
|
854
|
+
}
|
|
855
|
+
for (const [name, e] of this.entries) {
|
|
856
|
+
if (e.runtime)
|
|
857
|
+
await this.snapshotThenDispose(name, e);
|
|
858
|
+
releaseLock(this.dirFor(name)); // release lock; leave meta.json (durable)
|
|
859
|
+
}
|
|
860
|
+
this.entries.clear();
|
|
861
|
+
}
|
|
862
|
+
assertNotDisposed() {
|
|
863
|
+
if (this.disposed)
|
|
864
|
+
throw new Error('scratchpad manager disposed');
|
|
865
|
+
}
|
|
866
|
+
}
|