@sema-agent/server 7.132.0 → 7.133.0
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/MIGRATION.md +266 -0
- package/README.md +8 -9
- package/README.zh-CN.md +7 -6
- package/USAGE.md +174 -55
- package/deploy/sema-up/README.md +19 -1
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +5 -2
- package/deploy/sema-up/chart/templates/server.yaml +5 -2
- package/deploy/sema-up/stack/compose.yaml +3 -0
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -2
- package/dist/adoption/plan.d.ts +3 -2
- package/dist/adoption/plan.js +7 -5
- package/dist/adoption/runner.js +3 -10
- package/dist/adoption/sql.d.ts +3 -2
- package/dist/adoption/sql.js +3 -5
- package/dist/adoption/wire.d.ts +4 -1
- package/dist/adoption/wire.js +1 -0
- package/dist/approval-ask-audit-store.d.ts +31 -13
- package/dist/approval-ask-audit-store.js +272 -74
- package/dist/approval-ask-machine.d.ts +15 -0
- package/dist/approval-ask-machine.js +6 -0
- package/dist/approval-ask-retention.d.ts +136 -0
- package/dist/approval-ask-retention.js +102 -0
- package/dist/approval-card.d.ts +79 -19
- package/dist/approval-card.js +44 -13
- package/dist/approval-deny-reasons.d.ts +1 -1
- package/dist/approval.d.ts +9 -0
- package/dist/approval.js +24 -3
- package/dist/audit.d.ts +27 -10
- package/dist/audit.js +12 -16
- package/dist/bake-runner/main.js +3 -2
- package/dist/boot/deferred-sandbox-path-env.js +1 -1
- package/dist/boot/execution-env.js +9 -7
- package/dist/boot/execution-lane.js +11 -0
- package/dist/boot/leader.js +2 -0
- package/dist/boot/memory-boundary.js +2 -2
- package/dist/boot/memory-consolidation.d.ts +5 -4
- package/dist/boot/memory-consolidation.js +7 -6
- package/dist/boot/org-delivery.d.ts +28 -0
- package/dist/boot/org-delivery.js +20 -0
- package/dist/boot/reapers.d.ts +3 -0
- package/dist/boot/reapers.js +21 -1
- package/dist/boot/resolve-spec.d.ts +3 -0
- package/dist/boot/resolve-spec.js +60 -81
- package/dist/boot/runner-deps.js +1 -0
- package/dist/boot/session-faces.d.ts +1 -0
- package/dist/boot/session-faces.js +4 -2
- package/dist/boot/stage-02-web-admin-pull.js +4 -1
- package/dist/boot/stage-06-runners.d.ts +2 -0
- package/dist/boot/stage-06-runners.js +6 -3
- package/dist/boot/stage-07-capability-layer.d.ts +2 -0
- package/dist/boot/stage-07-capability-layer.js +24 -3
- package/dist/boot/stage-08-reapers.d.ts +2 -0
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +3 -0
- package/dist/boot/stage-10-http-server.d.ts +3 -0
- package/dist/boot/stage-10-http-server.js +5 -6
- package/dist/boot/stores.js +2 -2
- package/dist/boot/web-admin.d.ts +4 -0
- package/dist/boot/web-admin.js +151 -63
- package/dist/boot/workflow-orchestration.d.ts +2 -0
- package/dist/boot/workflow-orchestration.js +2 -1
- package/dist/brain.js +8 -3
- package/dist/breaker-key.d.ts +6 -0
- package/dist/breaker-key.js +29 -0
- package/dist/caller-text-echo.d.ts +9 -6
- package/dist/caller-text-echo.js +8 -11
- package/dist/capabilities/center-plugins.d.ts +7 -0
- package/dist/capabilities/center-plugins.js +1 -1
- package/dist/capabilities/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/capabilities/execution-lane.js +2 -2
- package/dist/capabilities/hands-lane.d.ts +21 -0
- package/dist/capabilities/hands-lane.js +10 -0
- package/dist/capabilities/mcp-probe.d.ts +36 -4
- package/dist/capabilities/mcp-probe.js +27 -4
- package/dist/capabilities/scenarios.d.ts +63 -7
- package/dist/capabilities/scenarios.js +39 -7
- package/dist/capability-facts.d.ts +34 -4
- package/dist/capability-facts.js +28 -2
- package/dist/center-config-tier.d.ts +69 -0
- package/dist/center-config-tier.js +42 -0
- package/dist/checkpoint-identity.d.ts +16 -0
- package/dist/checkpoint-identity.js +19 -0
- package/dist/config-catalog.d.ts +17 -10
- package/dist/config-catalog.js +43 -28
- package/dist/config-d-migration.d.ts +3 -1
- package/dist/config-d-migration.js +2 -2
- package/dist/config-dir-trust.d.ts +114 -0
- package/dist/config-dir-trust.js +388 -0
- package/dist/config-invariants.d.ts +18 -5
- package/dist/config-invariants.js +35 -3
- package/dist/config-provider.d.ts +6 -1
- package/dist/config-provider.js +6 -2
- package/dist/config-types.d.ts +71 -14
- package/dist/config-types.js +8 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +121 -59
- package/dist/delegated-session-owner.d.ts +185 -0
- package/dist/delegated-session-owner.js +119 -0
- package/dist/device-protocol.d.ts +5 -3
- package/dist/device-protocol.js +1 -1
- package/dist/device-ws-hub.js +21 -2
- package/dist/device-ws-protocol.d.ts +70 -6
- package/dist/device-ws-protocol.js +50 -23
- package/dist/elicitation.js +3 -2
- package/dist/entry-problems.d.ts +34 -0
- package/dist/entry-problems.js +14 -0
- package/dist/execution-lane-caps.d.ts +49 -10
- package/dist/execution-lane-caps.js +43 -4
- package/dist/field-conflict.d.ts +43 -0
- package/dist/field-conflict.js +20 -0
- package/dist/fleet/fleet-bus.d.ts +80 -6
- package/dist/fleet/fleet-bus.js +41 -9
- package/dist/hooks/classic-stations.d.ts +10 -5
- package/dist/hooks/classic-stations.js +4 -4
- package/dist/hooks/command-spawn.d.ts +57 -0
- package/dist/hooks/command-spawn.js +139 -0
- package/dist/hooks/hook-llm.js +2 -2
- package/dist/hooks/hook-runner.d.ts +127 -25
- package/dist/hooks/hook-runner.js +668 -287
- package/dist/hooks/hook-source.d.ts +44 -0
- package/dist/hooks/hook-source.js +40 -0
- package/dist/http/active-run-conflict.d.ts +5 -13
- package/dist/http/active-run-conflict.js +17 -27
- package/dist/http/admission.js +103 -60
- package/dist/http/approval-sweeps.js +55 -18
- package/dist/http/caller-trust.d.ts +77 -0
- package/dist/http/caller-trust.js +143 -0
- package/dist/http/checkpoint-ctx.d.ts +16 -16
- package/dist/http/checkpoint-ctx.js +13 -10
- package/dist/http/claimed-run.d.ts +4 -3
- package/dist/http/device-placement.d.ts +2 -4
- package/dist/http/device-placement.js +2 -1
- package/dist/http/dispatch.js +10 -4
- package/dist/http/durable-run-leg.d.ts +4 -32
- package/dist/http/durable-run-leg.js +34 -10
- package/dist/http/owner-access.d.ts +56 -0
- package/dist/http/owner-access.js +42 -0
- package/dist/http/principal-gate.d.ts +48 -23
- package/dist/http/principal-gate.js +39 -12
- package/dist/http/readiness.d.ts +11 -5
- package/dist/http/readiness.js +10 -3
- package/dist/http/request-guards.d.ts +16 -23
- package/dist/http/request-guards.js +30 -15
- package/dist/http/resume-ctx-read.d.ts +2 -5
- package/dist/http/resume-ctx-read.js +7 -4
- package/dist/http/resume-legs.d.ts +3 -4
- package/dist/http/resume-legs.js +126 -69
- package/dist/http/retired-service-deps.d.ts +43 -0
- package/dist/http/retired-service-deps.js +18 -0
- package/dist/http/route-ctx.d.ts +44 -28
- package/dist/http/route-ctx.js +4 -0
- package/dist/http/route-table.js +2 -0
- package/dist/http/routes/a2a-serve.js +3 -4
- package/dist/http/routes/admin-config-refresh.js +1 -1
- package/dist/http/routes/admin-drain.js +1 -1
- package/dist/http/routes/admin-memory-optout.js +5 -1
- package/dist/http/routes/adoption.js +2 -9
- package/dist/http/routes/approvals-assistant.js +49 -48
- package/dist/http/routes/background.js +3 -2
- package/dist/http/routes/capabilities.js +20 -4
- package/dist/http/routes/config-catalog.d.ts +3 -3
- package/dist/http/routes/config-catalog.js +1 -1
- package/dist/http/routes/devices.js +3 -17
- package/dist/http/routes/diagnostics.js +5 -1
- package/dist/http/routes/images.js +6 -7
- package/dist/http/routes/leader.js +1 -8
- package/dist/http/routes/mcp-reconnect.d.ts +1 -1
- package/dist/http/routes/mcp-reconnect.js +3 -2
- package/dist/http/routes/memory-bundle.js +1 -1
- package/dist/http/routes/memory-compliance.js +1 -1
- package/dist/http/routes/memory-consolidation.js +1 -1
- package/dist/http/routes/memory-origin.js +1 -1
- package/dist/http/routes/memory-policy.d.ts +1 -1
- package/dist/http/routes/memory-policy.js +3 -2
- package/dist/http/routes/notify-wake.js +8 -21
- package/dist/http/routes/observability.js +2 -2
- package/dist/http/routes/principal-inbox.d.ts +39 -0
- package/dist/http/routes/principal-inbox.js +76 -0
- package/dist/http/routes/retention-ops.js +2 -9
- package/dist/http/routes/rules.js +5 -18
- package/dist/http/routes/runs.d.ts +5 -0
- package/dist/http/routes/runs.js +33 -106
- package/dist/http/routes/session-sync.js +15 -28
- package/dist/http/routes/sessions.js +30 -40
- package/dist/http/routes/side-query.js +6 -4
- package/dist/http/routes/tasks.js +50 -14
- package/dist/http/routes/workflows.d.ts +351 -0
- package/dist/http/routes/workflows.js +153 -119
- package/dist/http/send.d.ts +24 -3
- package/dist/http/send.js +14 -2
- package/dist/http/server.d.ts +17 -18
- package/dist/http/server.js +15 -13
- package/dist/http/stored-body-replay.js +17 -4
- package/dist/http/submit-ctx.js +4 -3
- package/dist/http/submit-replay.d.ts +4 -3
- package/dist/http/submit-replay.js +5 -4
- package/dist/http/wire-types.d.ts +10 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +0 -1
- package/dist/lane-home-probe.d.ts +78 -9
- package/dist/lane-home-probe.js +78 -12
- package/dist/leader/endpoint.d.ts +4 -1
- package/dist/leader/endpoint.js +11 -10
- package/dist/limit-refusal.d.ts +17 -5
- package/dist/limit-refusal.js +4 -1
- package/dist/mcp-assertion-header.d.ts +31 -10
- package/dist/mcp-assertion-header.js +10 -3
- package/dist/mcp-tool-faces.js +1 -3
- package/dist/memory-project-layer.d.ts +37 -0
- package/dist/memory-project-layer.js +46 -0
- package/dist/memory-scope.d.ts +23 -16
- package/dist/memory-scope.js +24 -8
- package/dist/memory-sync-client.d.ts +27 -13
- package/dist/memory-sync-client.js +107 -18
- package/dist/model-entry-refusal.d.ts +9 -1
- package/dist/model-entry-refusal.js +6 -1
- package/dist/model-plane-face.d.ts +51 -0
- package/dist/model-plane-face.js +24 -0
- package/dist/model-provider.d.ts +5 -0
- package/dist/model-provider.js +4 -2
- package/dist/model-route-endpoint.d.ts +8 -11
- package/dist/model-route-endpoint.js +4 -7
- package/dist/model-route-env.d.ts +22 -0
- package/dist/model-route-env.js +2 -0
- package/dist/model-route-gap.d.ts +60 -30
- package/dist/model-route-gap.js +21 -15
- package/dist/observability/corrupt-read-seat.d.ts +7 -0
- package/dist/observability/corrupt-read-seat.js +9 -0
- package/dist/observability/err-text.d.ts +6 -0
- package/dist/observability/err-text.js +18 -4
- package/dist/observability/fail-open.d.ts +35 -9
- package/dist/observability/fail-open.js +35 -9
- package/dist/observability/metrics.js +1 -0
- package/dist/observability/prompt-manifest.d.ts +3 -17
- package/dist/observability/prompt-manifest.js +45 -42
- package/dist/observability/tool-trace.js +9 -9
- package/dist/operator-ask.d.ts +10 -0
- package/dist/operator-ask.js +3 -0
- package/dist/orchestration/workflow-agent-session-index.d.ts +12 -12
- package/dist/orchestration/workflow-agent-session-index.js +9 -13
- package/dist/orchestration/workflow-completion-inbox.d.ts +42 -15
- package/dist/orchestration/workflow-completion-inbox.js +13 -0
- package/dist/orchestration/workflow-notify-journal.d.ts +13 -32
- package/dist/orchestration/workflow-notify-journal.js +27 -34
- package/dist/orchestration/workflow-park-decision.d.ts +89 -0
- package/dist/orchestration/workflow-park-decision.js +118 -0
- package/dist/org-delivery-gate.d.ts +81 -0
- package/dist/org-delivery-gate.js +59 -0
- package/dist/panel-tool-names.d.ts +23 -0
- package/dist/panel-tool-names.js +8 -0
- package/dist/parked-decide.d.ts +39 -6
- package/dist/parked-decide.js +98 -51
- package/dist/parked-revive-surface.d.ts +9 -2
- package/dist/parked-revive-surface.js +11 -8
- package/dist/permission-rule-list.d.ts +1 -1
- package/dist/permission-rule-list.js +2 -2
- package/dist/plugins/approval-ask-store-file.d.ts +16 -1
- package/dist/plugins/approval-ask-store-file.js +21 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +18 -1
- package/dist/plugins/approval-ask-store-memory.js +34 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +25 -0
- package/dist/plugins/approval-ask-store-sql.js +37 -1
- package/dist/plugins/blob-backend.d.ts +38 -25
- package/dist/plugins/blob-backend.js +29 -19
- package/dist/plugins/breaker-state-sql.d.ts +2 -2
- package/dist/plugins/breaker-state-sql.js +4 -4
- package/dist/plugins/caching-session-store.d.ts +3 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +14 -0
- package/dist/plugins/checkpoint-store-sql.js +20 -2
- package/dist/plugins/file-history-store-sql.d.ts +5 -2
- package/dist/plugins/file-history-store-sql.js +3 -1
- package/dist/plugins/file-run-store.d.ts +5 -13
- package/dist/plugins/file-run-store.js +9 -0
- package/dist/plugins/fork-routing-session-store.d.ts +9 -1
- package/dist/plugins/fork-routing-session-store.js +12 -1
- package/dist/plugins/host-platform.d.ts +2 -0
- package/dist/plugins/host-platform.js +2 -1
- package/dist/plugins/local-checkpoint-store.d.ts +2 -0
- package/dist/plugins/local-checkpoint-store.js +6 -1
- package/dist/plugins/local-session-store.d.ts +1 -1
- package/dist/plugins/local-session-window.d.ts +39 -0
- package/dist/plugins/local-session-window.js +54 -0
- package/dist/plugins/mailbox-recipient-lifecycle.d.ts +3 -2
- package/dist/plugins/mailbox-store-sql.d.ts +30 -48
- package/dist/plugins/mailbox-store-sql.js +77 -52
- package/dist/plugins/memory-embedder-fingerprint.js +2 -1
- package/dist/plugins/memory-run-store.d.ts +5 -13
- package/dist/plugins/memory-run-store.js +8 -0
- package/dist/plugins/one-time-migrations.d.ts +110 -5
- package/dist/plugins/one-time-migrations.js +320 -8
- package/dist/plugins/org-delivery-quota-store.d.ts +123 -0
- package/dist/plugins/org-delivery-quota-store.js +228 -0
- package/dist/plugins/pg-pool.js +39 -7
- package/dist/plugins/pg-session-storage.d.ts +17 -4
- package/dist/plugins/pg-session-storage.js +29 -6
- package/dist/plugins/remote-env-adb.d.ts +6 -12
- package/dist/plugins/remote-env-adb.js +12 -2
- package/dist/plugins/remote-env-k8s.d.ts +8 -12
- package/dist/plugins/remote-env-k8s.js +23 -5
- package/dist/plugins/remote-env-local-docker.d.ts +10 -15
- package/dist/plugins/remote-env-local-docker.js +12 -9
- package/dist/plugins/remote-env-ssh.d.ts +10 -16
- package/dist/plugins/remote-env-ssh.js +17 -11
- package/dist/plugins/run-store-sql.d.ts +16 -14
- package/dist/plugins/run-store-sql.js +22 -2
- package/dist/plugins/schema-indexes.js +2 -0
- package/dist/plugins/send-user-file.js +2 -2
- package/dist/plugins/session-epoch-anchor.d.ts +32 -6
- package/dist/plugins/session-epoch-anchor.js +2 -3
- package/dist/plugins/session-store.d.ts +18 -8
- package/dist/plugins/session-store.js +15 -4
- package/dist/plugins/sql-driver.d.ts +74 -11
- package/dist/plugins/sql-driver.js +210 -12
- package/dist/plugins/sql-errors.d.ts +23 -0
- package/dist/plugins/sql-errors.js +21 -0
- package/dist/plugins/store-backend.d.ts +36 -1
- package/dist/plugins/store-backend.js +40 -14
- package/dist/plugins/store-contracts.d.ts +42 -0
- package/dist/plugins/store-contracts.js +1 -0
- package/dist/plugins/tidb-pool.d.ts +6 -1
- package/dist/plugins/tidb-pool.js +44 -10
- package/dist/plugins/tidb-session-storage.d.ts +3 -2
- package/dist/plugins/tidb-session-storage.js +2 -2
- package/dist/plugins/tidb-session-store.d.ts +24 -4
- package/dist/plugins/tidb-session-store.js +28 -4
- package/dist/plugins/workflow-journal-store-sql.d.ts +73 -19
- package/dist/plugins/workflow-journal-store-sql.js +19 -6
- package/dist/plugins/workflow-run-store-sql.d.ts +30 -8
- package/dist/plugins/workflow-run-store-sql.js +50 -9
- package/dist/posture-source.d.ts +18 -10
- package/dist/posture-source.js +4 -1
- package/dist/project-identity.d.ts +45 -6
- package/dist/project-identity.js +162 -8
- package/dist/project-layer-key.d.ts +28 -0
- package/dist/project-layer-key.js +29 -0
- package/dist/question.js +3 -2
- package/dist/read-deny-entry.d.ts +25 -0
- package/dist/read-deny-entry.js +12 -0
- package/dist/read-face-posture.d.ts +37 -11
- package/dist/read-face-posture.js +32 -3
- package/dist/request-key-closure.d.ts +7 -0
- package/dist/request-key-closure.js +9 -1
- package/dist/request-value-closure.d.ts +5 -0
- package/dist/request-value-closure.js +11 -3
- package/dist/rule-entry-list.d.ts +26 -2
- package/dist/rule-entry-list.js +72 -12
- package/dist/rules-consent.js +3 -4
- package/dist/run-acceptance.d.ts +19 -3
- package/dist/run-acceptance.js +13 -1
- package/dist/run-local.js +36 -6
- package/dist/runs.d.ts +10 -9
- package/dist/runs.js +21 -3
- package/dist/runtime-caps-resolver.d.ts +16 -6
- package/dist/runtime-caps-resolver.js +18 -5
- package/dist/sandbox-pkg-source.d.ts +19 -1
- package/dist/sandbox-pkg-source.js +29 -7
- package/dist/security.d.ts +87 -9
- package/dist/security.js +52 -9
- package/dist/session-error-wire.d.ts +19 -2
- package/dist/session-error-wire.js +5 -1
- package/dist/session-titled-frame.d.ts +33 -0
- package/dist/session-titled-frame.js +4 -0
- package/dist/session-titler.d.ts +9 -1
- package/dist/session-titler.js +15 -4
- package/dist/spec-fields.js +21 -2
- package/dist/sql-engine-posture.d.ts +3 -1
- package/dist/task-cwd.d.ts +1 -1
- package/dist/task-mcp.d.ts +3 -3
- package/dist/task-mcp.js +7 -66
- package/dist/task-settings.d.ts +89 -88
- package/dist/task-settings.js +71 -63
- package/dist/text-bidi.d.ts +0 -4
- package/dist/text-bidi.js +1 -3
- package/dist/tighten-only-seats.d.ts +150 -0
- package/dist/tighten-only-seats.js +196 -0
- package/dist/tool-approval.d.ts +303 -87
- package/dist/tool-approval.js +623 -239
- package/dist/trace/core-keyset-guard.d.ts +104 -22
- package/dist/trace/exit-seat.d.ts +28 -0
- package/dist/trace/exit-seat.js +76 -0
- package/dist/trace/ledger-events.d.ts +3 -2
- package/dist/trace/project.d.ts +80 -12
- package/dist/trace/project.js +41 -28
- package/dist/trace/projection-table.d.ts +100 -0
- package/dist/trace/projection-table.js +130 -0
- package/dist/trace/redact.d.ts +54 -0
- package/dist/trace/redact.js +778 -152
- package/dist/trace/self-minted-wire.d.ts +345 -1
- package/dist/trace/self-minted-wire.js +117 -1
- package/dist/trace/wire-projection-faces.d.ts +298 -2
- package/dist/trace/wire-projection-faces.js +154 -13
- package/dist/url-refusal.js +2 -0
- package/dist/web-admin-client/apply-effective.d.ts +12 -0
- package/dist/web-admin-client/apply-effective.js +56 -5
- package/dist/web-admin-client/facade.d.ts +1 -1
- package/dist/web-admin-client/facade.js +1 -1
- package/dist/web-admin-client/read-face.js +5 -9
- package/dist/web-admin-client/skills-mcp.js +4 -5
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +57 -9
- package/package.json +7 -3
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Pool as MysqlPool } from "mysql2/promise";
|
|
2
|
+
import type { Pool as PgPool } from "pg";
|
|
3
|
+
import { type SqlDriver } from "./sql-driver.js";
|
|
4
|
+
/** 发件人行表(单数形;`test/schema-naming-invariants.test.ts` 的闭集词表钉着)。 */
|
|
5
|
+
export declare const ORG_DELIVERY_SENDER_QUOTA_TABLE = "org_delivery_sender_quota";
|
|
6
|
+
/** 收件人行表。 */
|
|
7
|
+
export declare const ORG_DELIVERY_RECIPIENT_QUOTA_TABLE = "org_delivery_recipient_quota";
|
|
8
|
+
/** 对偶行表。 */
|
|
9
|
+
export declare const ORG_DELIVERY_PAIR_QUOTA_TABLE = "org_delivery_pair_quota";
|
|
10
|
+
/** 门用的那份额度。门持有它(唯一的数字源在 `org-delivery-gate.ts`),店只按它数。 */
|
|
11
|
+
export interface OrgDeliveryQuotaPolicy {
|
|
12
|
+
/** 固定窗宽(毫秒)。 */
|
|
13
|
+
readonly windowMs: number;
|
|
14
|
+
/** 一个发件人在一个窗里能做的投递尝试数上界(成没成都算)。 */
|
|
15
|
+
readonly attemptsPerSender: number;
|
|
16
|
+
/** 同一个发件人在一个窗里能投给同一个收件人的封数上界。 */
|
|
17
|
+
readonly perPair: number;
|
|
18
|
+
/** 一个收件人在一个窗里能收的封数上界(全部发件人合计)。 */
|
|
19
|
+
readonly perRecipient: number;
|
|
20
|
+
}
|
|
21
|
+
export interface OrgDeliveryQuotaStore {
|
|
22
|
+
/**
|
|
23
|
+
* 原子地把这位发件人的一次投递**尝试**算进店钟当前窗:未满 ⇒ +1、答 `true`;已满 ⇒ 什么都不记、答 `false`。
|
|
24
|
+
* 店读不出 / 写不进 ⇒ **抛**(调用方按失败处置,绝不当成「放行」)。
|
|
25
|
+
*/
|
|
26
|
+
admitAttempt(fromScope: string, policy: OrgDeliveryQuotaPolicy): Promise<boolean>;
|
|
27
|
+
/**
|
|
28
|
+
* 原子地把一封 (fromScope → toScope) 的信算进**店钟当前窗**的两只计数器:两界都未满 ⇒ 两只各 +1、答 `true`;
|
|
29
|
+
* 任一界已满 ⇒ 什么都不记、答 `false`。店读不出 / 写不进 ⇒ **抛**。
|
|
30
|
+
*/
|
|
31
|
+
take(fromScope: string, toScope: string, policy: OrgDeliveryQuotaPolicy): Promise<boolean>;
|
|
32
|
+
}
|
|
33
|
+
/** 窗起点(一处算法,三条腿共用)。 */
|
|
34
|
+
export declare function quotaWindowStart(nowMs: number, windowMs: number): number;
|
|
35
|
+
/** 店里记着的那个窗(`heldStart`)的计数此刻还算不算数 —— 头注那条律的**唯一**写法:此刻的窗,或此刻的下一个窗。 */
|
|
36
|
+
export declare function heldWindowCounts(heldStart: number, at: number, windowMs: number): boolean;
|
|
37
|
+
/** 一位发件人的当前窗(`windowMs` = 开窗那一次的窗宽:窗何时结束是条目自己的事实,File 腿据它丢已过窗的条目)。 */
|
|
38
|
+
interface SenderWindow {
|
|
39
|
+
windowStart: number;
|
|
40
|
+
windowMs: number;
|
|
41
|
+
attempts: number;
|
|
42
|
+
}
|
|
43
|
+
/** 一位收件人的当前窗。 */
|
|
44
|
+
interface RecipientWindow {
|
|
45
|
+
windowStart: number;
|
|
46
|
+
windowMs: number;
|
|
47
|
+
total: number;
|
|
48
|
+
/** fromScope → 本窗已投封数。 */
|
|
49
|
+
pairs: Map<string, number>;
|
|
50
|
+
}
|
|
51
|
+
/** 两张窗表(Memory 腿的全部状态;File 腿整份落盘的就是它)。 */
|
|
52
|
+
interface QuotaWindows {
|
|
53
|
+
senders: Map<string, SenderWindow>;
|
|
54
|
+
recipients: Map<string, RecipientWindow>;
|
|
55
|
+
}
|
|
56
|
+
export declare class MemoryOrgDeliveryQuotaStore implements OrgDeliveryQuotaStore {
|
|
57
|
+
protected readonly nowMs: () => number;
|
|
58
|
+
protected state: QuotaWindows;
|
|
59
|
+
/** @param nowMs 店钟(单进程腿:店 = 这个进程,缺省本进程墙钟)。 */
|
|
60
|
+
constructor(nowMs?: () => number);
|
|
61
|
+
admitAttempt(fromScope: string, policy: OrgDeliveryQuotaPolicy): Promise<boolean>;
|
|
62
|
+
take(fromScope: string, toScope: string, policy: OrgDeliveryQuotaPolicy): Promise<boolean>;
|
|
63
|
+
/**
|
|
64
|
+
* 落新的整份状态,只留窗还没结束的条目(按条目自己的窗宽判;已结束的发件人 / 收件人整个丢掉 —— 有界:量 = 本窗活跃的主体数)。
|
|
65
|
+
* 丢掉的条目按头注那条律本来就不再算数(过去的窗),所以剪枝不改任何判决;剪枝写在**这里**而不是 File 腿里,Memory 与 File
|
|
66
|
+
* 因此对同一串记账逐笔同判(墙钟回拨的那一形也同判)。File 腿覆写 {@link persist}:先落盘、再换内存(反过来的话,一次写盘失败
|
|
67
|
+
* 会留下「内存记了、盘上没有」)。
|
|
68
|
+
*/
|
|
69
|
+
protected commit(next: QuotaWindows): void;
|
|
70
|
+
/** Memory 腿无盘可落。 */
|
|
71
|
+
protected persist(_next: QuotaWindows): void;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* local 车道的配额店:Memory 腿的同一份算法 + 每次记账后**整份原子改写**一个 JSON 快照(`atomicWriteFile`:
|
|
75
|
+
* 同盘临时文件 + rename,崩溃只会留下旧的或新的整份,不会留半份)。
|
|
76
|
+
*
|
|
77
|
+
* 为什么是整份快照而不是追加日志:快照只装「当前窗」(写入时顺手丢掉已过窗的发件人 / 收件人),量 = 本窗活跃的主体数,
|
|
78
|
+
* 每次记账改写一次的代价与它同阶;追加日志则要再带一套压实与断尾规则。记账频次本来就被本店自己界住。
|
|
79
|
+
*
|
|
80
|
+
* 🔴 读不出的快照 ⇒ **拒用**(fail-closed,响亮):每一次记账都抛,出路在拒句里(移开文件 = 本窗计数清零)。
|
|
81
|
+
* 跳过坏文件当成「没记过」= 一次位腐就把整个配额窗放开,那是放行方向的静默降级。
|
|
82
|
+
*/
|
|
83
|
+
export declare class FileOrgDeliveryQuotaStore extends MemoryOrgDeliveryQuotaStore {
|
|
84
|
+
private readonly path;
|
|
85
|
+
private readonly tmpDir;
|
|
86
|
+
/** 快照读不出时的拒句(在场 ⇒ 店拒用)。 */
|
|
87
|
+
private readonly unusable;
|
|
88
|
+
constructor(root: string, opts?: {
|
|
89
|
+
nowMs?: () => number;
|
|
90
|
+
warn?: (event: string, fields: Record<string, unknown>) => void;
|
|
91
|
+
});
|
|
92
|
+
admitAttempt(fromScope: string, policy: OrgDeliveryQuotaPolicy): Promise<boolean>;
|
|
93
|
+
take(fromScope: string, toScope: string, policy: OrgDeliveryQuotaPolicy): Promise<boolean>;
|
|
94
|
+
protected persist(next: QuotaWindows): void;
|
|
95
|
+
}
|
|
96
|
+
export declare class SqlOrgDeliveryQuotaStore implements OrgDeliveryQuotaStore {
|
|
97
|
+
private readonly db;
|
|
98
|
+
constructor(db: SqlDriver);
|
|
99
|
+
private q;
|
|
100
|
+
admitAttempt(fromScope: string, policy: OrgDeliveryQuotaPolicy): Promise<boolean>;
|
|
101
|
+
take(fromScope: string, toScope: string, policy: OrgDeliveryQuotaPolicy): Promise<boolean>;
|
|
102
|
+
/**
|
|
103
|
+
* 一个记账事务:库钟先读(池上一条值语句,不在事务里留普通 SELECT),判过 ⇒ 提交,判拒 ⇒ 回滚(零残留)。
|
|
104
|
+
* 🔴 **引擎判了死锁 ⇒ 整事务重来**(有界,`isDeadlockError` 是全仓唯一的可重试锁故障判据):被挑中的一方已整事务回滚、锁全放,
|
|
105
|
+
* 重来时读-判-写全部重做,语义等价。真 InnoDB(REPEATABLE READ)上这条路是真的:窗前进时按收件人前缀删旧对偶行要取范围 / 间隙锁,
|
|
106
|
+
* 两位相邻收件人同时翻窗、各自往两人之间的间隙里插新对偶行 ⇒ 互等(TiDB 无间隙锁、PG 读已提交不取间隙锁)。谁当牺牲者是引擎的任意选择,
|
|
107
|
+
* 抛给调用方等于把一次内部仲裁当成投递失败。锁等待超时**不**重来(延迟信号,照抛)。
|
|
108
|
+
*/
|
|
109
|
+
private inTx;
|
|
110
|
+
private inTxOnce;
|
|
111
|
+
private admitIn;
|
|
112
|
+
private takeIn;
|
|
113
|
+
}
|
|
114
|
+
/** MySQL 协议腿的构造绑定。 */
|
|
115
|
+
export declare class TiDBOrgDeliveryQuotaStore extends SqlOrgDeliveryQuotaStore {
|
|
116
|
+
constructor(pool: MysqlPool);
|
|
117
|
+
}
|
|
118
|
+
/** PostgreSQL 腿的构造绑定。 */
|
|
119
|
+
export declare class PgOrgDeliveryQuotaStore extends SqlOrgDeliveryQuotaStore {
|
|
120
|
+
constructor(pool: PgPool);
|
|
121
|
+
}
|
|
122
|
+
export {};
|
|
123
|
+
//# sourceMappingURL=org-delivery-quota-store.d.ts.map
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { atomicWriteFile, ensureDir } from "@sema-agent/core";
|
|
4
|
+
import { mysqlDriver, pgDriver, sqlDbNowMs } from "./sql-driver.js";
|
|
5
|
+
import { isDeadlockError } from "./sql-errors.js";
|
|
6
|
+
import { alignedWindow } from "../observability/cost-quota.js";
|
|
7
|
+
export const ORG_DELIVERY_SENDER_QUOTA_TABLE = "org_delivery_sender_quota";
|
|
8
|
+
export const ORG_DELIVERY_RECIPIENT_QUOTA_TABLE = "org_delivery_recipient_quota";
|
|
9
|
+
export const ORG_DELIVERY_PAIR_QUOTA_TABLE = "org_delivery_pair_quota";
|
|
10
|
+
export function quotaWindowStart(nowMs, windowMs) {
|
|
11
|
+
return alignedWindow(nowMs, windowMs).bucket * windowMs;
|
|
12
|
+
}
|
|
13
|
+
export function heldWindowCounts(heldStart, at, windowMs) {
|
|
14
|
+
return heldStart === at || heldStart === at + windowMs;
|
|
15
|
+
}
|
|
16
|
+
export class MemoryOrgDeliveryQuotaStore {
|
|
17
|
+
nowMs;
|
|
18
|
+
state = { senders: new Map(), recipients: new Map() };
|
|
19
|
+
constructor(nowMs = Date.now) {
|
|
20
|
+
this.nowMs = nowMs;
|
|
21
|
+
}
|
|
22
|
+
async admitAttempt(fromScope, policy) {
|
|
23
|
+
const at = quotaWindowStart(this.nowMs(), policy.windowMs);
|
|
24
|
+
const held = this.state.senders.get(fromScope);
|
|
25
|
+
const current = held !== undefined && heldWindowCounts(held.windowStart, at, policy.windowMs) ? held : { windowStart: at, windowMs: policy.windowMs, attempts: 0 };
|
|
26
|
+
if (current.attempts >= policy.attemptsPerSender)
|
|
27
|
+
return false;
|
|
28
|
+
const senders = new Map(this.state.senders).set(fromScope, { ...current, attempts: current.attempts + 1 });
|
|
29
|
+
this.commit({ senders, recipients: this.state.recipients });
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
async take(fromScope, toScope, policy) {
|
|
33
|
+
const at = quotaWindowStart(this.nowMs(), policy.windowMs);
|
|
34
|
+
const held = this.state.recipients.get(toScope);
|
|
35
|
+
const current = held !== undefined && heldWindowCounts(held.windowStart, at, policy.windowMs) ? held : { windowStart: at, windowMs: policy.windowMs, total: 0, pairs: new Map() };
|
|
36
|
+
const pair = current.pairs.get(fromScope) ?? 0;
|
|
37
|
+
if (current.total >= policy.perRecipient || pair >= policy.perPair)
|
|
38
|
+
return false;
|
|
39
|
+
const next = { ...current, total: current.total + 1, pairs: new Map(current.pairs).set(fromScope, pair + 1) };
|
|
40
|
+
this.commit({ senders: this.state.senders, recipients: new Map(this.state.recipients).set(toScope, next) });
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
commit(next) {
|
|
44
|
+
const now = this.nowMs();
|
|
45
|
+
const live = (m) => new Map([...m].filter(([, w]) => w.windowStart + w.windowMs > now));
|
|
46
|
+
const pruned = { senders: live(next.senders), recipients: live(next.recipients) };
|
|
47
|
+
this.persist(pruned);
|
|
48
|
+
this.state = pruned;
|
|
49
|
+
}
|
|
50
|
+
persist(_next) { }
|
|
51
|
+
}
|
|
52
|
+
const isCount = (x) => typeof x === "number" && Number.isInteger(x) && x >= 0;
|
|
53
|
+
const isWidth = (x) => isCount(x) && x > 0;
|
|
54
|
+
const isPlain = (x) => x !== null && typeof x === "object" && !Array.isArray(x) && Object.getPrototypeOf(x) === Object.prototype;
|
|
55
|
+
function readSnapshot(raw) {
|
|
56
|
+
if (!isPlain(raw) || raw.v !== 1 || !isPlain(raw.senders) || !isPlain(raw.recipients))
|
|
57
|
+
return { ok: false, problem: "expected { v: 1, senders: {…}, recipients: {…} }" };
|
|
58
|
+
const senders = new Map();
|
|
59
|
+
for (const [from, entry] of Object.entries(raw.senders)) {
|
|
60
|
+
if (!isPlain(entry) || !isCount(entry.w) || !isWidth(entry.ms) || !isCount(entry.n))
|
|
61
|
+
return { ok: false, problem: `sender ${JSON.stringify(from)} is not { w, ms, n }` };
|
|
62
|
+
senders.set(from, { windowStart: entry.w, windowMs: entry.ms, attempts: entry.n });
|
|
63
|
+
}
|
|
64
|
+
const recipients = new Map();
|
|
65
|
+
for (const [to, entry] of Object.entries(raw.recipients)) {
|
|
66
|
+
if (!isPlain(entry) || !isCount(entry.w) || !isWidth(entry.ms) || !isCount(entry.n) || !isPlain(entry.pairs))
|
|
67
|
+
return { ok: false, problem: `recipient ${JSON.stringify(to)} is not { w, ms, n, pairs }` };
|
|
68
|
+
const pairs = new Map();
|
|
69
|
+
for (const [from, n] of Object.entries(entry.pairs)) {
|
|
70
|
+
if (!isCount(n))
|
|
71
|
+
return { ok: false, problem: `pair ${JSON.stringify([to, from])} is not a count` };
|
|
72
|
+
pairs.set(from, n);
|
|
73
|
+
}
|
|
74
|
+
recipients.set(to, { windowStart: entry.w, windowMs: entry.ms, total: entry.n, pairs });
|
|
75
|
+
}
|
|
76
|
+
return { ok: true, state: { senders, recipients } };
|
|
77
|
+
}
|
|
78
|
+
export class FileOrgDeliveryQuotaStore extends MemoryOrgDeliveryQuotaStore {
|
|
79
|
+
path;
|
|
80
|
+
tmpDir;
|
|
81
|
+
unusable;
|
|
82
|
+
constructor(root, opts) {
|
|
83
|
+
super(opts?.nowMs);
|
|
84
|
+
const dir = join(root, "org-delivery-quota");
|
|
85
|
+
ensureDir(dir);
|
|
86
|
+
this.tmpDir = join(dir, ".tmp");
|
|
87
|
+
ensureDir(this.tmpDir);
|
|
88
|
+
this.path = join(dir, "windows.json");
|
|
89
|
+
if (!existsSync(this.path))
|
|
90
|
+
return;
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = readSnapshot(JSON.parse(readFileSync(this.path, "utf8")));
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
parsed = { ok: false, problem: err instanceof Error ? err.message : String(err) };
|
|
97
|
+
}
|
|
98
|
+
if (!parsed.ok) {
|
|
99
|
+
this.unusable = `the cross-principal delivery quota file ${this.path} cannot be read (${parsed.problem}) — every delivery is refused until an operator moves it away (moving it resets the current window's counts)`;
|
|
100
|
+
(opts?.warn ?? ((event, fields) => console.warn(event, fields)))("org_delivery_quota_corrupt_read", { path: this.path, problem: parsed.problem });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.state = parsed.state;
|
|
104
|
+
}
|
|
105
|
+
async admitAttempt(fromScope, policy) {
|
|
106
|
+
if (this.unusable !== undefined)
|
|
107
|
+
throw new Error(this.unusable);
|
|
108
|
+
return super.admitAttempt(fromScope, policy);
|
|
109
|
+
}
|
|
110
|
+
async take(fromScope, toScope, policy) {
|
|
111
|
+
if (this.unusable !== undefined)
|
|
112
|
+
throw new Error(this.unusable);
|
|
113
|
+
return super.take(fromScope, toScope, policy);
|
|
114
|
+
}
|
|
115
|
+
persist(next) {
|
|
116
|
+
const { senders, recipients } = next;
|
|
117
|
+
const snapshot = {
|
|
118
|
+
v: 1,
|
|
119
|
+
senders: Object.fromEntries([...senders].map(([from, w]) => [from, { w: w.windowStart, ms: w.windowMs, n: w.attempts }])),
|
|
120
|
+
recipients: Object.fromEntries([...recipients].map(([to, w]) => [to, { w: w.windowStart, ms: w.windowMs, n: w.total, pairs: Object.fromEntries(w.pairs) }])),
|
|
121
|
+
};
|
|
122
|
+
atomicWriteFile(this.tmpDir, this.path, `${JSON.stringify(snapshot)}\n`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const DEADLOCK_RETRY_LIMIT = 3;
|
|
126
|
+
export class SqlOrgDeliveryQuotaStore {
|
|
127
|
+
db;
|
|
128
|
+
constructor(db) {
|
|
129
|
+
this.db = db;
|
|
130
|
+
}
|
|
131
|
+
q(tidb, pg) {
|
|
132
|
+
return this.db.dialect === "tidb" ? tidb : pg;
|
|
133
|
+
}
|
|
134
|
+
async admitAttempt(fromScope, policy) {
|
|
135
|
+
return this.inTx(ORG_DELIVERY_SENDER_QUOTA_TABLE, policy, (conn, at) => this.admitIn(conn, fromScope, at, policy));
|
|
136
|
+
}
|
|
137
|
+
async take(fromScope, toScope, policy) {
|
|
138
|
+
return this.inTx(ORG_DELIVERY_RECIPIENT_QUOTA_TABLE, policy, (conn, at) => this.takeIn(conn, fromScope, toScope, at, policy));
|
|
139
|
+
}
|
|
140
|
+
async inTx(table, policy, body) {
|
|
141
|
+
for (let tries = 0;; tries++) {
|
|
142
|
+
try {
|
|
143
|
+
return await this.inTxOnce(table, policy, body);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
if (tries < DEADLOCK_RETRY_LIMIT && isDeadlockError(this.db.dialect, err))
|
|
147
|
+
continue;
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async inTxOnce(table, policy, body) {
|
|
153
|
+
const at = quotaWindowStart(await sqlDbNowMs(this.db, table), policy.windowMs);
|
|
154
|
+
const conn = await this.db.connect();
|
|
155
|
+
try {
|
|
156
|
+
await conn.begin();
|
|
157
|
+
const granted = await body(conn, at);
|
|
158
|
+
if (granted)
|
|
159
|
+
await conn.commit();
|
|
160
|
+
else
|
|
161
|
+
await conn.rollback();
|
|
162
|
+
return granted;
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
await conn.rollback().catch((rollbackErr) => {
|
|
166
|
+
throw new AggregateError([err, rollbackErr], `${table}: the quota transaction failed and its rollback failed too`);
|
|
167
|
+
});
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
conn.release();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async admitIn(conn, fromScope, at, policy) {
|
|
175
|
+
const S = ORG_DELIVERY_SENDER_QUOTA_TABLE;
|
|
176
|
+
await conn.query(this.q(`INSERT INTO ${S} (from_scope, window_start_ms, attempts) VALUES (?, ?, 0) ON DUPLICATE KEY UPDATE from_scope = from_scope`, `INSERT INTO ${S} (from_scope, window_start_ms, attempts) VALUES ($1, $2, 0) ON CONFLICT (from_scope) DO NOTHING`), [fromScope, at]);
|
|
177
|
+
const { rows } = await conn.query(this.q(`SELECT window_start_ms, attempts FROM ${S} WHERE from_scope = ? FOR UPDATE`, `SELECT window_start_ms, attempts FROM ${S} WHERE from_scope = $1 FOR UPDATE`), [fromScope]);
|
|
178
|
+
const held = rows[0];
|
|
179
|
+
if (held === undefined)
|
|
180
|
+
throw new Error(`${S}: the sender row for ${JSON.stringify(fromScope)} vanished inside its own transaction`);
|
|
181
|
+
const heldStart = Number(held.window_start_ms);
|
|
182
|
+
const counts = heldWindowCounts(heldStart, at, policy.windowMs);
|
|
183
|
+
const windowStart = counts ? heldStart : at;
|
|
184
|
+
const attempts = counts ? Number(held.attempts) : 0;
|
|
185
|
+
if (attempts >= policy.attemptsPerSender)
|
|
186
|
+
return false;
|
|
187
|
+
await conn.query(this.q(`UPDATE ${S} SET window_start_ms = ?, attempts = ? WHERE from_scope = ?`, `UPDATE ${S} SET window_start_ms = $1, attempts = $2 WHERE from_scope = $3`), [windowStart, attempts + 1, fromScope]);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
async takeIn(conn, fromScope, toScope, at, policy) {
|
|
191
|
+
const R = ORG_DELIVERY_RECIPIENT_QUOTA_TABLE;
|
|
192
|
+
const P = ORG_DELIVERY_PAIR_QUOTA_TABLE;
|
|
193
|
+
await conn.query(this.q(`INSERT INTO ${R} (to_scope, window_start_ms, delivered) VALUES (?, ?, 0) ON DUPLICATE KEY UPDATE to_scope = to_scope`, `INSERT INTO ${R} (to_scope, window_start_ms, delivered) VALUES ($1, $2, 0) ON CONFLICT (to_scope) DO NOTHING`), [toScope, at]);
|
|
194
|
+
const { rows: rr } = await conn.query(this.q(`SELECT window_start_ms, delivered FROM ${R} WHERE to_scope = ? FOR UPDATE`, `SELECT window_start_ms, delivered FROM ${R} WHERE to_scope = $1 FOR UPDATE`), [toScope]);
|
|
195
|
+
const held = rr[0];
|
|
196
|
+
if (held === undefined)
|
|
197
|
+
throw new Error(`${R}: the recipient row for ${JSON.stringify(toScope)} vanished inside its own transaction`);
|
|
198
|
+
const heldStart = Number(held.window_start_ms);
|
|
199
|
+
const rolls = !heldWindowCounts(heldStart, at, policy.windowMs);
|
|
200
|
+
const windowStart = rolls ? at : heldStart;
|
|
201
|
+
const total = rolls ? 0 : Number(held.delivered);
|
|
202
|
+
if (rolls) {
|
|
203
|
+
await conn.query(this.q(`DELETE FROM ${P} WHERE to_scope = ? AND window_start_ms <> ?`, `DELETE FROM ${P} WHERE to_scope = $1 AND window_start_ms <> $2`), [toScope, windowStart]);
|
|
204
|
+
}
|
|
205
|
+
await conn.query(this.q(`INSERT INTO ${P} (to_scope, from_scope, window_start_ms, delivered) VALUES (?, ?, ?, 0) ON DUPLICATE KEY UPDATE to_scope = to_scope`, `INSERT INTO ${P} (to_scope, from_scope, window_start_ms, delivered) VALUES ($1, $2, $3, 0) ON CONFLICT (to_scope, from_scope) DO NOTHING`), [toScope, fromScope, windowStart]);
|
|
206
|
+
const { rows: pr } = await conn.query(this.q(`SELECT window_start_ms, delivered FROM ${P} WHERE to_scope = ? AND from_scope = ? FOR UPDATE`, `SELECT window_start_ms, delivered FROM ${P} WHERE to_scope = $1 AND from_scope = $2 FOR UPDATE`), [toScope, fromScope]);
|
|
207
|
+
const pairRow = pr[0];
|
|
208
|
+
if (pairRow === undefined)
|
|
209
|
+
throw new Error(`${P}: the pair row for ${JSON.stringify([toScope, fromScope])} vanished inside its own transaction`);
|
|
210
|
+
const pair = Number(pairRow.window_start_ms) === windowStart ? Number(pairRow.delivered) : 0;
|
|
211
|
+
if (total >= policy.perRecipient || pair >= policy.perPair)
|
|
212
|
+
return false;
|
|
213
|
+
await conn.query(this.q(`UPDATE ${R} SET window_start_ms = ?, delivered = ? WHERE to_scope = ?`, `UPDATE ${R} SET window_start_ms = $1, delivered = $2 WHERE to_scope = $3`), [windowStart, total + 1, toScope]);
|
|
214
|
+
await conn.query(this.q(`UPDATE ${P} SET window_start_ms = ?, delivered = ? WHERE to_scope = ? AND from_scope = ?`, `UPDATE ${P} SET window_start_ms = $1, delivered = $2 WHERE to_scope = $3 AND from_scope = $4`), [windowStart, pair + 1, toScope, fromScope]);
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export class TiDBOrgDeliveryQuotaStore extends SqlOrgDeliveryQuotaStore {
|
|
219
|
+
constructor(pool) {
|
|
220
|
+
super(mysqlDriver(pool));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
export class PgOrgDeliveryQuotaStore extends SqlOrgDeliveryQuotaStore {
|
|
224
|
+
constructor(pool) {
|
|
225
|
+
super(pgDriver(pool));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=org-delivery-quota-store.js.map
|
package/dist/plugins/pg-pool.js
CHANGED
|
@@ -19,6 +19,7 @@ import { ensurePgRetentionSchema } from "./retention-store-sql.js";
|
|
|
19
19
|
import { ensureIndexes } from "./ensure-index.js";
|
|
20
20
|
import { SESSION_RULES_COLUMN } from "./session-rules-column.js";
|
|
21
21
|
import { CENTRAL_SCHEMA_INDEXES } from "./schema-indexes.js";
|
|
22
|
+
import { PRINCIPAL_MAX_LENGTH } from "../security.js";
|
|
22
23
|
export const PG_SCHEMA_STATEMENTS = [
|
|
23
24
|
`CREATE TABLE IF NOT EXISTS task_run (
|
|
24
25
|
task_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
@@ -140,6 +141,25 @@ export const PG_SCHEMA_STATEMENTS = [
|
|
|
140
141
|
consumed_at_ms BIGINT NOT NULL,
|
|
141
142
|
expires_at_ms BIGINT NOT NULL,
|
|
142
143
|
PRIMARY KEY (session_id, nonce)
|
|
144
|
+
)`,
|
|
145
|
+
`CREATE TABLE IF NOT EXISTS org_delivery_sender_quota (
|
|
146
|
+
from_scope VARCHAR(${PRINCIPAL_MAX_LENGTH}) COLLATE "C" NOT NULL,
|
|
147
|
+
window_start_ms BIGINT NOT NULL,
|
|
148
|
+
attempts INT NOT NULL,
|
|
149
|
+
PRIMARY KEY (from_scope)
|
|
150
|
+
)`,
|
|
151
|
+
`CREATE TABLE IF NOT EXISTS org_delivery_recipient_quota (
|
|
152
|
+
to_scope VARCHAR(${PRINCIPAL_MAX_LENGTH}) COLLATE "C" NOT NULL,
|
|
153
|
+
window_start_ms BIGINT NOT NULL,
|
|
154
|
+
delivered INT NOT NULL,
|
|
155
|
+
PRIMARY KEY (to_scope)
|
|
156
|
+
)`,
|
|
157
|
+
`CREATE TABLE IF NOT EXISTS org_delivery_pair_quota (
|
|
158
|
+
to_scope VARCHAR(${PRINCIPAL_MAX_LENGTH}) COLLATE "C" NOT NULL,
|
|
159
|
+
from_scope VARCHAR(${PRINCIPAL_MAX_LENGTH}) COLLATE "C" NOT NULL,
|
|
160
|
+
window_start_ms BIGINT NOT NULL,
|
|
161
|
+
delivered INT NOT NULL,
|
|
162
|
+
PRIMARY KEY (to_scope, from_scope)
|
|
143
163
|
)`,
|
|
144
164
|
`CREATE TABLE IF NOT EXISTS user_input_acceptance (
|
|
145
165
|
accept_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
@@ -263,17 +283,19 @@ export const PG_SCHEMA_STATEMENTS = [
|
|
|
263
283
|
)`,
|
|
264
284
|
`CREATE TABLE IF NOT EXISTS workflow_journal (
|
|
265
285
|
run_id VARCHAR(191) COLLATE "C" NOT NULL,
|
|
286
|
+
incarnation VARCHAR(64) COLLATE "C" NOT NULL,
|
|
266
287
|
ordinal INT NOT NULL,
|
|
267
|
-
-- SVC-2 / CORE-9 audit BLOCKER: cross-tenant resume isolation (the WorkflowJournalStore seam takes a scope
|
|
268
|
-
-- param). NOT NULL DEFAULT '' — a row written without a scope lands in the '' bucket, which is
|
|
269
|
-
-- CROSS-SCOPE-ORPHANED but harmless: a real resume always filters \`WHERE scope = <caller>\`, so '' rows are
|
|
270
|
-
-- never handed to a tenant. Keep the DEFAULT: it is what makes a scope-less write land in that dead bucket
|
|
271
|
-
-- instead of failing or leaking into a real tenant's scope.
|
|
272
|
-
scope VARCHAR(190) COLLATE "C" NOT NULL DEFAULT '',
|
|
273
288
|
call_key VARCHAR(255) COLLATE "C" NOT NULL,
|
|
274
289
|
result TEXT COLLATE "C" NOT NULL,
|
|
275
290
|
created_at_ms BIGINT NOT NULL,
|
|
276
|
-
PRIMARY KEY (run_id, ordinal)
|
|
291
|
+
PRIMARY KEY (run_id, incarnation, ordinal)
|
|
292
|
+
)`,
|
|
293
|
+
`CREATE TABLE IF NOT EXISTS workflow_journal_owner (
|
|
294
|
+
run_id VARCHAR(191) COLLATE "C" NOT NULL,
|
|
295
|
+
scope VARCHAR(190) COLLATE "C" NOT NULL,
|
|
296
|
+
incarnation VARCHAR(64) COLLATE "C" NOT NULL,
|
|
297
|
+
appended_at_ms BIGINT NOT NULL,
|
|
298
|
+
PRIMARY KEY (run_id)
|
|
277
299
|
)`,
|
|
278
300
|
`CREATE TABLE IF NOT EXISTS workflow_resume_claim (
|
|
279
301
|
source_run_id VARCHAR(191) COLLATE "C" NOT NULL,
|
|
@@ -298,6 +320,16 @@ export const PG_SCHEMA_STATEMENTS = [
|
|
|
298
320
|
agent_key VARCHAR(191) COLLATE "C" NOT NULL,
|
|
299
321
|
scope VARCHAR(190) COLLATE "C" NOT NULL,
|
|
300
322
|
PRIMARY KEY (session_id, run_id)
|
|
323
|
+
)`,
|
|
324
|
+
`CREATE TABLE IF NOT EXISTS workflow_park_decision (
|
|
325
|
+
token VARCHAR(120) COLLATE "C" NOT NULL,
|
|
326
|
+
run_id VARCHAR(191) COLLATE "C" NOT NULL,
|
|
327
|
+
host_session_id VARCHAR(190) COLLATE "C" NOT NULL,
|
|
328
|
+
outcome TEXT COLLATE "C" NOT NULL,
|
|
329
|
+
fingerprint VARCHAR(64) COLLATE "C" NOT NULL,
|
|
330
|
+
delivered_task_id VARCHAR(191) COLLATE "C" NOT NULL,
|
|
331
|
+
accepted_at_ms BIGINT NOT NULL,
|
|
332
|
+
PRIMARY KEY (token)
|
|
301
333
|
)`,
|
|
302
334
|
`CREATE TABLE IF NOT EXISTS workflow_completion_inbox (
|
|
303
335
|
seq BIGSERIAL,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { BaseSessionStorage, type AcquiredSession, type PlacedSessionRow, type SessionMetadata, type
|
|
1
|
+
import { BaseSessionStorage, type AcquiredSession, type PlacedSessionRow, type SessionMetadata, type SessionPlacementRecord, type SessionStore, type SessionTreeEntry, type SessionWriteOptions } from "@sema-agent/core";
|
|
2
2
|
import type { Pool, PoolClient } from "pg";
|
|
3
3
|
import type { SessionSummary } from "./store-contracts.js";
|
|
4
4
|
import { type SessionRowPatch } from "../session-row.js";
|
|
5
5
|
import { type PlacementAcquireOpts } from "./session-placement.js";
|
|
6
|
+
import { type OrphanOwnerLog } from "../delegated-session-owner.js";
|
|
6
7
|
import { type StagingHandle } from "../session-sync-kernel.js";
|
|
7
8
|
import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
|
|
8
9
|
import { type SessionFace } from "../session-incarnation.js";
|
|
@@ -65,12 +66,24 @@ export declare class PgSessionStore implements SessionStore {
|
|
|
65
66
|
};
|
|
66
67
|
/** In-flight acquisitions keyed by id, so concurrent acquire(sameId) in one process share one. */
|
|
67
68
|
private readonly pending;
|
|
68
|
-
|
|
69
|
+
/** 委派子代孤行那一行 warn 的去处(TiDB 孪生同格)。 */
|
|
70
|
+
private readonly log;
|
|
71
|
+
constructor(pool: Pool, opts?: {
|
|
72
|
+
readonly log?: OrphanOwnerLog;
|
|
73
|
+
});
|
|
69
74
|
acquire(sessionId?: string, opts?: PlacementAcquireOpts): Promise<AcquiredSession>;
|
|
70
75
|
private load;
|
|
71
|
-
/** Idempotently create a session_meta row with an owner. Existing owner is never overwritten.
|
|
76
|
+
/** Idempotently create a session_meta row with an owner (core `SessionStore.register`). Existing owner is never overwritten. */
|
|
77
|
+
register(sessionId: string, owner: string | null): Promise<void>;
|
|
78
|
+
/** acquire 的建行(唯一一处;S-1770 委派继承律,只住在 `createPlacedSessionRow`,本店交既有读写口 —— TiDB 孪生同名方法同注)。 */
|
|
79
|
+
private createOnAcquire;
|
|
80
|
+
/** S-1770 委派继承律的认领口(TiDB 孪生 {@link TiDBSessionStore.claimOwnerless} 同义;句 = `CLAIM_OWNERLESS_PLACED_SQL`,规则在 `delegated-session-owner.ts`)。 */
|
|
81
|
+
claimOwnerless(sessionId: string, incarnation: string, owner: string): Promise<boolean>;
|
|
82
|
+
/** 若干来源行的会话面(属主 + 化身)一次读回(TiDB 孪生同义;化身经与 `sessionFaceOf` 同一只解码,空 ⇒ 响亮拒)。 */
|
|
83
|
+
private sourceFaces;
|
|
84
|
+
/** The one session_meta INSERT. Existing owner is never overwritten.
|
|
72
85
|
* [ref]:`placement` 与行同一条 INSERT;`DO NOTHING` 臂 ⇒ 已存在行的元组永不被重写(first-write 不可变)。 */
|
|
73
|
-
|
|
86
|
+
private insertMeta;
|
|
74
87
|
/** [ref] 义务 6 的廉价探针(TiDB 孪生 {@link TiDBSessionStore.placementOf} 同义)。 */
|
|
75
88
|
placementOf(sessionId: string): Promise<SessionPlacementRecord | undefined>;
|
|
76
89
|
/** [ref] 义务 5 —— placed 分区枚举(年龄锚/上界/tupleIncomplete 三条读法与 TiDB 孪生逐字同义,
|
|
@@ -9,6 +9,8 @@ import { pgSafeJsonStringify } from "./pg-safe-json.js";
|
|
|
9
9
|
import { toIso } from "./sql-row-helpers.js";
|
|
10
10
|
import { assertPlacementAdmission, LIST_PLACED_BATCH, PLACEMENT_SELECT_COLUMNS, placedRowFromRow, placementFromRow, placementInsertValues, SQL_SESSION_PLACEMENTS, } from "./session-placement.js";
|
|
11
11
|
import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
|
|
12
|
+
import { CLAIM_OWNERLESS_PLACED_SQL, claimOwnerlessParams, createPlacedSessionRow } from "../delegated-session-owner.js";
|
|
13
|
+
import { createLogger } from "../observability/logger.js";
|
|
12
14
|
import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, } from "../session-sync-kernel.js";
|
|
13
15
|
import { isPgUniqueViolation } from "./sql-errors.js";
|
|
14
16
|
import { BASELINE_IO_DEADLINE_MS, decodeInstructionSource, encodeInstructionSource } from "../instruction-source-notice.js";
|
|
@@ -83,10 +85,10 @@ export class PgSessionStorage extends BaseSessionStorage {
|
|
|
83
85
|
this.floorSeq = floorSeq;
|
|
84
86
|
}
|
|
85
87
|
async getEpochAnchor() {
|
|
86
|
-
return scanEpochAnchor(this.
|
|
88
|
+
return scanEpochAnchor(this.floorSeq, async (beforeSeq) => {
|
|
87
89
|
const res = await this.pool.query(`SELECT seq, ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 AND seq < $2 AND type IN ('prompt_epoch','compaction') ORDER BY seq DESC LIMIT 100`, [this.sessionId, beforeSeq]);
|
|
88
90
|
return res.rows;
|
|
89
|
-
});
|
|
91
|
+
}, (row) => decodeSessionEventPayload(row.payload, this.sessionId, row.seq));
|
|
90
92
|
}
|
|
91
93
|
static async wake(pool, sessionId) {
|
|
92
94
|
const metaRes = await pool.query(`SELECT created_at, leaf_id, leaf_seq, incarnation, ${PLACEMENT_SELECT_COLUMNS} FROM session_meta WHERE session_id = $1`, [sessionId]);
|
|
@@ -175,13 +177,15 @@ export class PgSessionStore {
|
|
|
175
177
|
retention = MANAGED_RETENTION;
|
|
176
178
|
placements = SQL_SESSION_PLACEMENTS;
|
|
177
179
|
pending = new Map();
|
|
178
|
-
|
|
180
|
+
log;
|
|
181
|
+
constructor(pool, opts = {}) {
|
|
179
182
|
this.pool = pool;
|
|
183
|
+
this.log = opts.log ?? createLogger();
|
|
180
184
|
}
|
|
181
185
|
async acquire(sessionId, opts) {
|
|
182
186
|
if (!sessionId) {
|
|
183
187
|
const id = uuidv7();
|
|
184
|
-
await this.
|
|
188
|
+
await this.createOnAcquire(id, opts?.placement);
|
|
185
189
|
return this.load(id, false, opts);
|
|
186
190
|
}
|
|
187
191
|
if (opts?.requireExisting)
|
|
@@ -204,7 +208,7 @@ export class PgSessionStore {
|
|
|
204
208
|
}
|
|
205
209
|
if (requireExisting)
|
|
206
210
|
throw new SessionError("not_found", `session ${sessionId} not found`);
|
|
207
|
-
await this.
|
|
211
|
+
await this.createOnAcquire(sessionId, opts?.placement);
|
|
208
212
|
storage = await PgSessionStorage.wake(this.pool, sessionId);
|
|
209
213
|
if (!storage) {
|
|
210
214
|
throw new SessionError("storage", `session ${sessionId} vanished between register and wake (a concurrent purge won the race) — retry`);
|
|
@@ -212,7 +216,26 @@ export class PgSessionStore {
|
|
|
212
216
|
assertPlacementAdmission(sessionId, (await storage.getMetadata()).placement, opts);
|
|
213
217
|
return { session: new StoredSession(storage), sessionId };
|
|
214
218
|
}
|
|
215
|
-
async register(sessionId, owner
|
|
219
|
+
async register(sessionId, owner) {
|
|
220
|
+
await this.insertMeta(sessionId, owner, undefined);
|
|
221
|
+
}
|
|
222
|
+
async createOnAcquire(sessionId, placement) {
|
|
223
|
+
if (placement === undefined)
|
|
224
|
+
return this.insertMeta(sessionId, null, undefined);
|
|
225
|
+
await createPlacedSessionRow(sessionId, placement, {
|
|
226
|
+
readFaces: (ids) => this.sourceFaces(ids),
|
|
227
|
+
insert: (owner) => this.insertMeta(sessionId, owner, placement),
|
|
228
|
+
readOwner: () => this.ownerOf(sessionId),
|
|
229
|
+
}, this.log);
|
|
230
|
+
}
|
|
231
|
+
async claimOwnerless(sessionId, incarnation, owner) {
|
|
232
|
+
return ((await this.pool.query(CLAIM_OWNERLESS_PLACED_SQL.pg, [...claimOwnerlessParams(sessionId, incarnation, owner)])).rowCount ?? 0) > 0;
|
|
233
|
+
}
|
|
234
|
+
async sourceFaces(ids) {
|
|
235
|
+
const res = await this.pool.query(`SELECT session_id, owner, incarnation FROM session_meta WHERE session_id IN (${ids.map((_, i) => `$${i + 1}`).join(", ")})`, [...ids]);
|
|
236
|
+
return new Map(res.rows.map((r) => [String(r.session_id), sessionFaceFromRow(String(r.session_id), r.owner ?? null, r.incarnation)]));
|
|
237
|
+
}
|
|
238
|
+
async insertMeta(sessionId, owner, placement) {
|
|
216
239
|
const now = new Date();
|
|
217
240
|
await this.pool.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation, " +
|
|
218
241
|
"placement_kind, placement_scope, placement_parent_session_id, placement_root_session_id, placement_handle, placed_at) " +
|
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { type LaneHomeConfig } from "../lane-home-probe.js";
|
|
2
3
|
import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory } from "@sema-agent/core";
|
|
3
4
|
type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
|
|
4
|
-
export interface AdbEnvConfig {
|
|
5
|
+
export interface AdbEnvConfig extends LaneHomeConfig {
|
|
5
6
|
/** Device serial or `host:port` (TCP). `host:port` triggers `adb connect` on connect()/reconnect(). */
|
|
6
7
|
serial: string;
|
|
7
8
|
/** Path to the adb binary. Default "adb" (must be on PATH in the runner image). */
|
|
8
9
|
adbPath?: string;
|
|
9
|
-
/**
|
|
10
|
-
* S-213④ / core [ref] —— 本执行环境的 **home 目录**(`ExecutionEnv.homeDir` 座,绝对路径,本环境自己的
|
|
11
|
-
* 命名空间里)。`~/…` 形权限规则(`Edit(~/.ssh/**)`)对**跑在本 env 里的调用**就以它为基。
|
|
12
|
-
* 缺席 ⇒ 本 env 声明「我没有 home」,core 对 `~/` 规则判 `unreadable`(fail-closed 的 ask),
|
|
13
|
-
* 绝不回落引擎进程自己的 home(那在远端腿上是另一个用户的另一个目录 —— 静默守错目录正是 [ref] 的病)。
|
|
14
|
-
* 值由 `boot/execution-env.ts` 从**唯一属主** `execution-lane-caps.ts` 的 `executionLaneHomeDir` 递进来;
|
|
15
|
-
* adapter 不自算(两处各算一份 = 422 门与引擎对同一条规则给相反答案)。
|
|
16
|
-
*/
|
|
17
|
-
homeDir?: string;
|
|
18
10
|
/** On-device workspace root; relative paths resolve against it. Default "/data/local/tmp". */
|
|
19
11
|
mountPath?: string;
|
|
20
12
|
commandTimeoutMs?: number;
|
|
@@ -86,8 +78,10 @@ export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
|
86
78
|
private destroyed;
|
|
87
79
|
/** 在飞的那一次懒连接(单飞记忆)——见 {@link ensureConnected}。settle 即清,**不缓存败诺**。 */
|
|
88
80
|
private connectPromise?;
|
|
89
|
-
/** core `ExecutionEnv.homeDir`([ref])
|
|
90
|
-
|
|
81
|
+
/** core `ExecutionEnv.homeDir`([ref]):设备 shell 里执行用户的 home —— 声明胜,否则首连时 `adb shell` 实测的 `$HOME`(Android 常为 `/` 或 `/data/local/tmp` 一类,读到什么填什么,绝对即可),见 {@link LaneHomeSeat}。读时才取(没声明时连上之前缺席)。 */
|
|
82
|
+
get homeDir(): string | undefined;
|
|
83
|
+
/** 家目录的座(声明胜 · 否则首连时实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。本车道**不对证**声明:Android 没有 POSIX 登录家目录,声明是运维的断言,照单全收。 */
|
|
84
|
+
private readonly homeSeat;
|
|
91
85
|
/** core 7.49.0 #1554:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在 Android 设备的 shell 里(设备自己的 env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
|
|
92
86
|
* 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
|
|
93
87
|
readonly cdPath: "unknown";
|
|
@@ -3,6 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { hostSubprocessEnv } from "../server-secret-env.js";
|
|
6
|
+
import { LANE_HOME_PROBE_COMMAND, LaneHomeSeat } from "../lane-home-probe.js";
|
|
6
7
|
import { buildCwdEnvCommand, createCwdGuardedCall, shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
|
|
7
8
|
import { signalNumber } from "./host-platform.js";
|
|
8
9
|
import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
|
|
@@ -31,7 +32,10 @@ export class RemoteAdbExecutionEnv {
|
|
|
31
32
|
connected = false;
|
|
32
33
|
destroyed = false;
|
|
33
34
|
connectPromise;
|
|
34
|
-
homeDir
|
|
35
|
+
get homeDir() {
|
|
36
|
+
return this.homeSeat.home;
|
|
37
|
+
}
|
|
38
|
+
homeSeat;
|
|
35
39
|
cdPath = "unknown";
|
|
36
40
|
cfg;
|
|
37
41
|
spawnFn;
|
|
@@ -46,7 +50,7 @@ export class RemoteAdbExecutionEnv {
|
|
|
46
50
|
retry: { maxAttempts: config.retry?.maxAttempts ?? 3, backoffMs: config.retry?.backoffMs ?? 800 },
|
|
47
51
|
};
|
|
48
52
|
this.cwd = this.cfg.mountPath;
|
|
49
|
-
this.
|
|
53
|
+
this.homeSeat = new LaneHomeSeat({ lane: PROVIDER, declared: config.homeDir, verifiesDeclared: false, onAbsent: config.onHomeAbsent });
|
|
50
54
|
this.spawnFn = deps?.spawn ?? spawn;
|
|
51
55
|
this.injectedConnectOnce = deps?.connectOnce;
|
|
52
56
|
}
|
|
@@ -117,6 +121,12 @@ export class RemoteAdbExecutionEnv {
|
|
|
117
121
|
error: new RemoteExecutionError("unsupported", `device ${this.cfg.serial} does not advertise the 'shell_v2' feature, so this adapter cannot establish that its command round-trip carries a real exit code with stderr kept separate — the two properties every filesystem judgment on this lane depends on to tell failure from success. Refusing to connect rather than run on an unverifiable transport: attach a device that supports adb shell protocol v2.`),
|
|
118
122
|
};
|
|
119
123
|
}
|
|
124
|
+
if (this.homeSeat.asks) {
|
|
125
|
+
const home = await this.adb(this.deviceShellArgs(LANE_HOME_PROBE_COMMAND), { signal, timeoutMs: this.cfg.commandTimeoutMs });
|
|
126
|
+
if (!home.ok && home.error.code === "aborted")
|
|
127
|
+
return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted while reading the device's home") };
|
|
128
|
+
this.homeSeat.settle(home.ok && home.value.exitCode === 0 ? { ok: true, stdout: home.value.stdout } : { ok: false, reason: home.ok ? `exit ${home.value.exitCode}` : home.error.message });
|
|
129
|
+
}
|
|
120
130
|
this.connected = true;
|
|
121
131
|
return ok(this.workspaceHandle());
|
|
122
132
|
}
|