@jungjaehoon/mama-core 2.4.1 → 4.0.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/README.md +76 -239
- package/db/migrations/030-case-first-memory-substrate.sql +13 -98
- package/db/migrations/034-add-connector-event-scope-columns.sql +9 -13
- package/db/migrations/039-add-connector-event-operator-ingest-seq.sql +9 -95
- package/db/migrations/062-refresh-connector-event-sequences.sql +9 -113
- package/db/migrations/063-refresh-legacy-connector-event-updates.sql +9 -32
- package/db/migrations/067-add-connector-event-source-entity-id.sql +9 -13
- package/db/migrations/069-create-registry-nodes.sql +52 -0
- package/db/migrations/070-record-identity.sql +35 -0
- package/db/migrations/071-service-operation-origins.sql +187 -0
- package/db/migrations/072-observation-versions.sql +29 -0
- package/db/migrations/073-registry-corrections.sql +45 -0
- package/db/migrations/074-work-graph-ref-kinds.sql +39 -0
- package/db/migrations/075-agent-judgments.sql +58 -0
- package/db/migrations/076-scoped-checkpoint-bindings.sql +15 -0
- package/db/migrations/077-legacy-record-kind.sql +6 -0
- package/db/migrations/078-source-ingest-commands.sql +18 -0
- package/db/migrations/079-twin-edge-relations.sql +43 -0
- package/db/migrations/080-restore-duration-days.sql +7 -0
- package/db/migrations/081-mailbox.sql +61 -0
- package/db/migrations/082-commitments-row-id.sql +12 -0
- package/db/migrations/083-observation-is-the-evidence.sql +49 -0
- package/db/migrations/084-scope-kind-is-not-the-cores-to-list.sql +34 -0
- package/db/migrations/085-a-migration-has-a-source.sql +37 -0
- package/db/migrations/086-the-mailbox-is-generic.sql +110 -0
- package/db/migrations/087-mailbox-payload.sql +5 -0
- package/db/migrations/088-native-input-journal.sql +14 -0
- package/db/migrations/089-native-turn-receipt-lookup.sql +3 -0
- package/db/migrations/090-model-run-native-inputs.sql +29 -0
- package/db/migrations/091-native-turn-results.sql +11 -0
- package/db/migrations/092-restore-decision-fts.sql +34 -0
- package/db/migrations/093-native-turn-primary-kind.sql +14 -0
- package/db/migrations/094-commitment-run-provenance.sql +9 -0
- package/db/migrations/095-model-run-native-input-identity.sql +39 -0
- package/db/migrations/096-drop-connector-event-index-fts.sql +9 -0
- package/db/migrations/097-native-input-json-validity.sql +37 -0
- package/db/migrations/098-workflow-memory-kind.sql +105 -0
- package/dist/action-contracts.d.ts +212 -0
- package/dist/action-contracts.js +10 -0
- package/dist/api/catalog.d.ts +118 -0
- package/dist/api/catalog.js +1756 -0
- package/dist/api/dispatch.d.ts +76 -0
- package/dist/api/dispatch.js +372 -0
- package/dist/canonicalize.d.ts +2 -2
- package/dist/canonicalize.js +3 -3
- package/dist/client/client.d.ts +33 -0
- package/dist/client/client.js +123 -0
- package/dist/client/ipc.d.ts +81 -0
- package/dist/client/ipc.js +314 -0
- package/dist/db-adapter/base-adapter.d.ts +6 -66
- package/dist/db-adapter/base-adapter.js +6 -6
- package/dist/db-adapter/index.d.ts +5 -5
- package/dist/db-adapter/index.js +2 -5
- package/dist/db-adapter/node-sqlite-adapter.d.ts +85 -15
- package/dist/db-adapter/node-sqlite-adapter.js +1968 -570
- package/dist/db-manager.d.ts +53 -97
- package/dist/db-manager.js +121 -603
- package/dist/embedding/embedder.d.ts +132 -0
- package/dist/embedding/embedder.js +299 -0
- package/dist/identity/principal-repository.d.ts +2 -2
- package/dist/index.d.ts +43 -58
- package/dist/index.js +97 -145
- package/dist/{context-compile/channel-grant.d.ts → knowledge/access.d.ts} +12 -17
- package/dist/knowledge/access.js +663 -0
- package/dist/knowledge/case-errors.d.ts +31 -0
- package/dist/knowledge/case-errors.js +40 -0
- package/dist/{cases/search-rollup.d.ts → knowledge/case-search-rollup.d.ts} +5 -4
- package/dist/{cases/search-rollup.js → knowledge/case-search-rollup.js} +6 -8
- package/dist/{cases/store.d.ts → knowledge/case-store.d.ts} +3 -6
- package/dist/{cases/store.js → knowledge/case-store.js} +7 -42
- package/dist/{cases/timeline-range.d.ts → knowledge/case-timeline-range.d.ts} +3 -3
- package/dist/{cases/timeline-range.js → knowledge/case-timeline-range.js} +47 -25
- package/dist/{cases/types.d.ts → knowledge/case-types.d.ts} +8 -2
- package/dist/{cases/types.js → knowledge/case-types.js} +1 -1
- package/dist/knowledge/commitments.d.ts +188 -0
- package/dist/knowledge/commitments.js +370 -0
- package/dist/knowledge/decision-edges.d.ts +70 -0
- package/dist/knowledge/decision-edges.js +123 -0
- package/dist/{agent-graph/types.d.ts → knowledge/graph-query.d.ts} +80 -70
- package/dist/knowledge/graph-query.js +1585 -0
- package/dist/knowledge/identity.d.ts +16 -0
- package/dist/knowledge/identity.js +22 -0
- package/dist/knowledge/index.d.ts +47 -0
- package/dist/knowledge/index.js +90 -0
- package/dist/knowledge/judgments.d.ts +118 -0
- package/dist/knowledge/judgments.js +808 -0
- package/dist/knowledge/observations.d.ts +160 -0
- package/dist/knowledge/observations.js +449 -0
- package/dist/knowledge/question-type.d.ts +34 -0
- package/dist/knowledge/question-type.js +70 -0
- package/dist/{search → knowledge}/ranker-features.js +1 -1
- package/dist/{search → knowledge}/search-quality.d.ts +0 -1
- package/dist/knowledge/search.d.ts +38 -0
- package/dist/knowledge/search.js +91 -0
- package/dist/knowledge/source-ingest.d.ts +65 -0
- package/dist/knowledge/source-ingest.js +174 -0
- package/dist/{edges/types.d.ts → knowledge/twin-edge-types.d.ts} +31 -4
- package/dist/{edges/types.js → knowledge/twin-edge-types.js} +13 -2
- package/dist/knowledge/work-dates.d.ts +49 -0
- package/dist/knowledge/work-dates.js +120 -0
- package/dist/mama-api.d.ts +114 -778
- package/dist/mama-api.js +313 -2461
- package/dist/memory/api.d.ts +264 -24
- package/dist/memory/api.js +1633 -843
- package/dist/memory/bootstrap-builder.d.ts +2 -1
- package/dist/memory/bootstrap-builder.js +5 -5
- package/dist/memory/channel-summary-state-store.d.ts +3 -2
- package/dist/memory/channel-summary-state-store.js +53 -51
- package/dist/memory/channel-summary-store.d.ts +5 -3
- package/dist/memory/channel-summary-store.js +3 -8
- package/dist/memory/dashboard-read.d.ts +46 -0
- package/dist/memory/dashboard-read.js +94 -0
- package/dist/memory/event-store.d.ts +3 -3
- package/dist/memory/event-store.js +3 -10
- package/dist/memory/finding-store.d.ts +3 -2
- package/dist/memory/finding-store.js +2 -7
- package/dist/memory/graph-read.d.ts +52 -0
- package/dist/memory/graph-read.js +130 -0
- package/dist/memory/judgment-types.d.ts +394 -0
- package/dist/memory/judgment-types.js +16 -0
- package/dist/memory/provenance-audit.d.ts +5 -2
- package/dist/memory/provenance-audit.js +8 -14
- package/dist/memory/provenance-live.d.ts +141 -0
- package/dist/memory/provenance-live.js +409 -0
- package/dist/memory/provenance-query.d.ts +5 -4
- package/dist/memory/provenance-query.js +21 -29
- package/dist/memory/provenance-resolver.d.ts +191 -0
- package/dist/memory/provenance-resolver.js +155 -0
- package/dist/memory/provenance.d.ts +1 -11
- package/dist/memory/provenance.js +2 -33
- package/dist/memory/recall-sanitize.d.ts +55 -0
- package/dist/memory/recall-sanitize.js +144 -0
- package/dist/memory/secret-filter.d.ts +32 -0
- package/dist/memory/secret-filter.js +126 -0
- package/dist/memory/truth-store.d.ts +2 -1
- package/dist/memory/truth-store.js +1 -4
- package/dist/memory/types.d.ts +74 -24
- package/dist/memory/types.js +75 -5
- package/dist/memory/write-adapters.d.ts +98 -0
- package/dist/memory/write-adapters.js +242 -0
- package/dist/operations/owner-action-effects.d.ts +53 -0
- package/dist/operations/owner-action-effects.js +96 -0
- package/dist/provenance/source-ref.d.ts +1 -1
- package/dist/provenance/source-ref.js +0 -2
- package/dist/registry/corrections.d.ts +31 -0
- package/dist/registry/corrections.js +332 -0
- package/dist/registry/record-identity.d.ts +49 -0
- package/dist/registry/record-identity.js +133 -0
- package/dist/registry/store.d.ts +101 -0
- package/dist/registry/store.js +395 -0
- package/dist/registry/types.d.ts +28 -0
- package/dist/registry/types.js +5 -0
- package/dist/relevance-scorer.js +2 -2
- package/dist/runtime/agent-event-bus.d.ts +50 -0
- package/dist/runtime/agent-event-bus.js +101 -0
- package/dist/runtime/drivers/claude-cli-wrapper.d.ts +155 -0
- package/dist/runtime/drivers/claude-cli-wrapper.js +392 -0
- package/dist/runtime/drivers/cli-arg-redaction.d.ts +2 -0
- package/dist/runtime/drivers/cli-arg-redaction.js +18 -0
- package/dist/runtime/drivers/cli-secret-redaction.d.ts +17 -0
- package/dist/runtime/drivers/cli-secret-redaction.js +126 -0
- package/dist/runtime/drivers/codex-app-server-process.d.ts +277 -0
- package/dist/runtime/drivers/codex-app-server-process.js +2342 -0
- package/dist/runtime/drivers/codex-auxiliary-tools.d.ts +17 -0
- package/dist/runtime/drivers/codex-auxiliary-tools.js +267 -0
- package/dist/runtime/drivers/codex-home.d.ts +20 -0
- package/dist/runtime/drivers/codex-home.js +674 -0
- package/dist/runtime/drivers/codex-thread-registry.d.ts +63 -0
- package/dist/runtime/drivers/codex-thread-registry.js +433 -0
- package/dist/runtime/drivers/persistent-cli-adapter.d.ts +160 -0
- package/dist/runtime/drivers/persistent-cli-adapter.js +396 -0
- package/dist/runtime/drivers/persistent-cli-process.d.ts +424 -0
- package/dist/runtime/drivers/persistent-cli-process.js +1543 -0
- package/dist/runtime/drivers/types.d.ts +456 -0
- package/dist/runtime/drivers/types.js +110 -0
- package/dist/runtime/mailbox.d.ts +223 -0
- package/dist/runtime/mailbox.js +582 -0
- package/dist/runtime/model-run-store.d.ts +28 -0
- package/dist/{model-runs/store.js → runtime/model-run-store.js} +81 -41
- package/dist/{model-runs/types.d.ts → runtime/model-run-types.d.ts} +33 -4
- package/dist/{model-runs/types.js → runtime/model-run-types.js} +1 -1
- package/dist/runtime/native-effect-observer.d.ts +22 -0
- package/dist/runtime/native-effect-observer.js +103 -0
- package/dist/runtime/native-input-journal.d.ts +46 -0
- package/dist/runtime/native-input-journal.js +308 -0
- package/dist/runtime/native-prompt.d.ts +118 -0
- package/dist/runtime/native-prompt.js +496 -0
- package/dist/runtime/native-session.d.ts +10 -0
- package/dist/{agent-graph/index.js → runtime/native-session.js} +9 -6
- package/dist/runtime/native-tool-trace-observer.d.ts +5 -0
- package/dist/runtime/native-tool-trace-observer.js +65 -0
- package/dist/runtime/native-turn.d.ts +272 -0
- package/dist/runtime/native-turn.js +1041 -0
- package/dist/runtime/operations.d.ts +236 -0
- package/dist/runtime/operations.js +565 -0
- package/dist/runtime/post-tool-handler.d.ts +74 -0
- package/dist/runtime/post-tool-handler.js +137 -0
- package/dist/runtime/prompt-layers.d.ts +126 -0
- package/dist/runtime/prompt-layers.js +239 -0
- package/dist/runtime/runtime-process.d.ts +102 -0
- package/dist/runtime/runtime-process.js +206 -0
- package/dist/runtime/runtime.d.ts +278 -0
- package/dist/runtime/runtime.js +563 -0
- package/dist/runtime/session-pool.d.ts +176 -0
- package/dist/runtime/session-pool.js +369 -0
- package/dist/runtime/stimulus-payload.d.ts +5 -0
- package/dist/runtime/stimulus-payload.js +50 -0
- package/dist/runtime/subagent-bridge.d.ts +24 -0
- package/dist/{agent-graph/types.js → runtime/subagent-bridge.js} +1 -1
- package/dist/runtime/text-completion.d.ts +22 -0
- package/dist/{agent-situation/types.js → runtime/text-completion.js} +1 -1
- package/dist/runtime/token-estimator.d.ts +25 -0
- package/dist/runtime/token-estimator.js +75 -0
- package/dist/runtime/tool-trace-store.d.ts +22 -0
- package/dist/{model-runs → runtime}/tool-trace-store.js +46 -27
- package/dist/runtime/trace-summary.d.ts +5 -0
- package/dist/runtime/trace-summary.js +74 -0
- package/dist/runtime/turn-text.d.ts +23 -0
- package/dist/runtime/turn-text.js +41 -0
- package/dist/storage/database.d.ts +67 -0
- package/dist/storage/database.js +158 -0
- package/package.json +20 -34
- package/dist/agent-graph/alias-write.d.ts +0 -3
- package/dist/agent-graph/alias-write.js +0 -234
- package/dist/agent-graph/entity-resolve.d.ts +0 -3
- package/dist/agent-graph/entity-resolve.js +0 -271
- package/dist/agent-graph/errors.d.ts +0 -4
- package/dist/agent-graph/errors.js +0 -11
- package/dist/agent-graph/graph-query.d.ts +0 -5
- package/dist/agent-graph/graph-query.js +0 -360
- package/dist/agent-graph/index.d.ts +0 -6
- package/dist/agent-situation/builder.d.ts +0 -8
- package/dist/agent-situation/builder.js +0 -239
- package/dist/agent-situation/cache-key.d.ts +0 -4
- package/dist/agent-situation/cache-key.js +0 -64
- package/dist/agent-situation/index.d.ts +0 -7
- package/dist/agent-situation/index.js +0 -28
- package/dist/agent-situation/packet-store.d.ts +0 -27
- package/dist/agent-situation/packet-store.js +0 -228
- package/dist/agent-situation/ranking-policy.d.ts +0 -18
- package/dist/agent-situation/ranking-policy.js +0 -75
- package/dist/agent-situation/source-readers.d.ts +0 -52
- package/dist/agent-situation/source-readers.js +0 -300
- package/dist/agent-situation/types.d.ts +0 -195
- package/dist/cases/wiki-page-index.d.ts +0 -37
- package/dist/cases/wiki-page-index.js +0 -201
- package/dist/config-loader.d.ts +0 -68
- package/dist/config-loader.js +0 -239
- package/dist/connectors/event-index.d.ts +0 -24
- package/dist/connectors/event-index.js +0 -245
- package/dist/connectors/raw-query.d.ts +0 -40
- package/dist/connectors/raw-query.js +0 -412
- package/dist/connectors/types.d.ts +0 -117
- package/dist/connectors/types.js +0 -3
- package/dist/context-compile/boundary-defaults.d.ts +0 -12
- package/dist/context-compile/boundary-defaults.js +0 -105
- package/dist/context-compile/channel-grant.js +0 -66
- package/dist/context-compile/compiler-policy.d.ts +0 -39
- package/dist/context-compile/compiler-policy.js +0 -147
- package/dist/context-compile/compiler.d.ts +0 -18
- package/dist/context-compile/compiler.js +0 -404
- package/dist/context-compile/index.d.ts +0 -10
- package/dist/context-compile/index.js +0 -26
- package/dist/context-compile/packet-store.d.ts +0 -8
- package/dist/context-compile/packet-store.js +0 -239
- package/dist/context-compile/ref.d.ts +0 -7
- package/dist/context-compile/ref.js +0 -102
- package/dist/context-compile/source-readers.d.ts +0 -70
- package/dist/context-compile/source-readers.js +0 -840
- package/dist/context-compile/types.d.ts +0 -155
- package/dist/context-compile/types.js +0 -11
- package/dist/context-compile/visibility.d.ts +0 -21
- package/dist/context-compile/visibility.js +0 -224
- package/dist/decision-tracker.d.ts +0 -206
- package/dist/decision-tracker.js +0 -481
- package/dist/edges/ref-validation.d.ts +0 -7
- package/dist/edges/ref-validation.js +0 -321
- package/dist/edges/store.d.ts +0 -8
- package/dist/edges/store.js +0 -129
- package/dist/embedding-client.d.ts +0 -28
- package/dist/embedding-client.js +0 -53
- package/dist/embedding-server/index.d.ts +0 -67
- package/dist/embedding-server/index.js +0 -405
- package/dist/embedding-server/mobile/auth.d.ts +0 -44
- package/dist/embedding-server/mobile/auth.js +0 -150
- package/dist/embedding-server/mobile/daemon.d.ts +0 -129
- package/dist/embedding-server/mobile/daemon.js +0 -313
- package/dist/embedding-server/mobile/output-parser.d.ts +0 -115
- package/dist/embedding-server/mobile/output-parser.js +0 -241
- package/dist/embedding-server/mobile/session-api.d.ts +0 -57
- package/dist/embedding-server/mobile/session-api.js +0 -261
- package/dist/embedding-server/mobile/session-manager.d.ts +0 -138
- package/dist/embedding-server/mobile/session-manager.js +0 -378
- package/dist/embedding-server/mobile/websocket-handler.d.ts +0 -137
- package/dist/embedding-server/mobile/websocket-handler.js +0 -486
- package/dist/embeddings.d.ts +0 -68
- package/dist/embeddings.js +0 -244
- package/dist/entities/audit-metrics.d.ts +0 -68
- package/dist/entities/audit-metrics.js +0 -124
- package/dist/entities/candidate-generator.d.ts +0 -8
- package/dist/entities/candidate-generator.js +0 -24
- package/dist/entities/entity-linked-decision-counts.d.ts +0 -2
- package/dist/entities/entity-linked-decision-counts.js +0 -29
- package/dist/entities/entity-list.d.ts +0 -22
- package/dist/entities/entity-list.js +0 -3
- package/dist/entities/entity-orphan-list.d.ts +0 -17
- package/dist/entities/entity-orphan-list.js +0 -3
- package/dist/entities/entity-search.d.ts +0 -20
- package/dist/entities/entity-search.js +0 -3
- package/dist/entities/errors.d.ts +0 -55
- package/dist/entities/errors.js +0 -100
- package/dist/entities/lineage-store.d.ts +0 -28
- package/dist/entities/lineage-store.js +0 -204
- package/dist/entities/normalization.d.ts +0 -14
- package/dist/entities/normalization.js +0 -63
- package/dist/entities/policy-store.d.ts +0 -8
- package/dist/entities/policy-store.js +0 -75
- package/dist/entities/policy-types.d.ts +0 -72
- package/dist/entities/policy-types.js +0 -59
- package/dist/entities/projection.d.ts +0 -8
- package/dist/entities/projection.js +0 -82
- package/dist/entities/provenance-query.d.ts +0 -32
- package/dist/entities/provenance-query.js +0 -3
- package/dist/entities/read-identity.d.ts +0 -31
- package/dist/entities/read-identity.js +0 -264
- package/dist/entities/recall-bridge.d.ts +0 -5
- package/dist/entities/recall-bridge.js +0 -171
- package/dist/entities/resolution-engine.d.ts +0 -8
- package/dist/entities/resolution-engine.js +0 -3
- package/dist/entities/rollback-preview.d.ts +0 -35
- package/dist/entities/rollback-preview.js +0 -3
- package/dist/entities/source-locator.d.ts +0 -3
- package/dist/entities/source-locator.js +0 -32
- package/dist/entities/store.d.ts +0 -69
- package/dist/entities/store.js +0 -544
- package/dist/entities/types.d.ts +0 -166
- package/dist/entities/types.js +0 -30
- package/dist/memory/extraction-prompt.d.ts +0 -4
- package/dist/memory/extraction-prompt.js +0 -89
- package/dist/memory/scope-backfill.d.ts +0 -14
- package/dist/memory/scope-backfill.js +0 -3
- package/dist/memory/scope-store.d.ts +0 -4
- package/dist/memory/scope-store.js +0 -9
- package/dist/memory-store.d.ts +0 -87
- package/dist/memory-store.js +0 -78
- package/dist/model-runs/store.d.ts +0 -13
- package/dist/model-runs/tool-trace-store.d.ts +0 -8
- package/dist/notification-manager.d.ts +0 -7
- package/dist/notification-manager.js +0 -12
- package/dist/outcome-tracker.d.ts +0 -151
- package/dist/outcome-tracker.js +0 -271
- package/dist/query-intent.d.ts +0 -23
- package/dist/query-intent.js +0 -114
- package/dist/runtime/trusted-provenance.d.ts +0 -3
- package/dist/runtime/trusted-provenance.js +0 -6
- package/dist/search/question-type.d.ts +0 -5
- package/dist/search/question-type.js +0 -41
- package/dist/test-utils.d.ts +0 -80
- package/dist/test-utils.js +0 -218
- package/dist/tier-validator.d.ts +0 -43
- package/dist/tier-validator.js +0 -159
- package/dist/time-formatter.d.ts +0 -25
- package/dist/time-formatter.js +0 -93
- /package/dist/{search → knowledge}/feedback-store.d.ts +0 -0
- /package/dist/{search → knowledge}/feedback-store.js +0 -0
- /package/dist/{search → knowledge}/ranker-features.d.ts +0 -0
- /package/dist/{search → knowledge}/ranker-rescore.d.ts +0 -0
- /package/dist/{search → knowledge}/ranker-rescore.js +0 -0
- /package/dist/{search → knowledge}/ranker-trainer.d.ts +0 -0
- /package/dist/{search → knowledge}/ranker-trainer.js +0 -0
- /package/dist/{search → knowledge}/search-quality.js +0 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Mailbox = exports.MailboxCorruptionError = void 0;
|
|
4
|
+
exports.isStimulusKind = isStimulusKind;
|
|
5
|
+
const stimulus_payload_js_1 = require("./stimulus-payload.js");
|
|
6
|
+
const native_input_journal_js_1 = require("./native-input-journal.js");
|
|
7
|
+
const STIMULUS_KINDS = new Set([
|
|
8
|
+
'owner_message',
|
|
9
|
+
'source_delta',
|
|
10
|
+
'scheduled',
|
|
11
|
+
'native_event',
|
|
12
|
+
]);
|
|
13
|
+
function isStimulusKind(value) {
|
|
14
|
+
return typeof value === 'string' && STIMULUS_KINDS.has(value);
|
|
15
|
+
}
|
|
16
|
+
/** A row the mailbox refused to parse. Parked dead, never silently dropped. */
|
|
17
|
+
class MailboxCorruptionError extends Error {
|
|
18
|
+
rowId;
|
|
19
|
+
code = 'MAILBOX_CORRUPT';
|
|
20
|
+
constructor(rowId, cause) {
|
|
21
|
+
super(`Mailbox row ${rowId} is corrupt`, { cause });
|
|
22
|
+
this.rowId = rowId;
|
|
23
|
+
this.name = 'MailboxCorruptionError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.MailboxCorruptionError = MailboxCorruptionError;
|
|
27
|
+
/**
|
|
28
|
+
* The backoff and retention schedule, carried over unchanged from the intake
|
|
29
|
+
* this generalises: five attempts, then the row parks dead and visible.
|
|
30
|
+
*/
|
|
31
|
+
const MAX_ATTEMPTS = 5;
|
|
32
|
+
const ACKED_RETENTION_MS = 7 * 86_400_000;
|
|
33
|
+
/**
|
|
34
|
+
* Pending rows older than this park as dead so a long provider outage cannot
|
|
35
|
+
* replay months of stale input. Dead rows stay visible in depth().
|
|
36
|
+
*/
|
|
37
|
+
const PENDING_RETENTION_MS = 7 * 86_400_000;
|
|
38
|
+
/**
|
|
39
|
+
* Dedupe horizon. Ref ids must outlive their row or a redelivery after pruning
|
|
40
|
+
* re-processes old input — but not forever either (one TEXT-PK row per ref ever
|
|
41
|
+
* accepted). 30 days is far wider than any redelivery gap a cursor produces.
|
|
42
|
+
*/
|
|
43
|
+
const REF_RETENTION_MS = 30 * 86_400_000;
|
|
44
|
+
const SEEN_CHUNK = 500; // stay under SQLITE_MAX_VARIABLE_NUMBER
|
|
45
|
+
/**
|
|
46
|
+
* Replay deltas intentionally carry one channel's bounded source history. The
|
|
47
|
+
* feeder preflights this same contract before accepting any window so a
|
|
48
|
+
* large channel cannot be silently split or partially delivered.
|
|
49
|
+
*/
|
|
50
|
+
const MAX_REFS = 500;
|
|
51
|
+
function backoffCase(column) {
|
|
52
|
+
return `CASE ${column}
|
|
53
|
+
WHEN 0 THEN 60000
|
|
54
|
+
WHEN 1 THEN 300000
|
|
55
|
+
WHEN 2 THEN 1800000
|
|
56
|
+
WHEN 3 THEN 7200000
|
|
57
|
+
ELSE 43200000
|
|
58
|
+
END`;
|
|
59
|
+
}
|
|
60
|
+
class Mailbox {
|
|
61
|
+
db;
|
|
62
|
+
clock;
|
|
63
|
+
nativeInputs;
|
|
64
|
+
stmtInsertSeen;
|
|
65
|
+
stmtInsert;
|
|
66
|
+
stmtInsertRef;
|
|
67
|
+
stmtRefsFor;
|
|
68
|
+
stmtClaimSelect;
|
|
69
|
+
stmtClaimSelectKind;
|
|
70
|
+
stmtClaimUpdate;
|
|
71
|
+
stmtAck;
|
|
72
|
+
stmtRetry;
|
|
73
|
+
stmtRetryStatus;
|
|
74
|
+
stmtPruneAcked;
|
|
75
|
+
stmtPrunePending;
|
|
76
|
+
stmtPruneSeen;
|
|
77
|
+
stmtReplay;
|
|
78
|
+
stmtDepth;
|
|
79
|
+
stmtPendingCoalesced;
|
|
80
|
+
stmtAppendPreview;
|
|
81
|
+
stmtIdentityPayload;
|
|
82
|
+
constructor(db, clock = () => Date.now()) {
|
|
83
|
+
this.db = db;
|
|
84
|
+
this.clock = clock;
|
|
85
|
+
// Declared by core migrations 086/087. Repeated here for the same reason every
|
|
86
|
+
// core store repeats its own shape: a consumer may open this against a
|
|
87
|
+
// database its own assembly created.
|
|
88
|
+
this.db.exec(`
|
|
89
|
+
CREATE TABLE IF NOT EXISTS mailbox_inputs (
|
|
90
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
91
|
+
stimulus_id TEXT NOT NULL,
|
|
92
|
+
kind TEXT,
|
|
93
|
+
principal_id TEXT NOT NULL,
|
|
94
|
+
channel_key TEXT NOT NULL,
|
|
95
|
+
preview_json TEXT NOT NULL DEFAULT '[]',
|
|
96
|
+
payload_json TEXT,
|
|
97
|
+
reply_to TEXT,
|
|
98
|
+
coalesce_key TEXT,
|
|
99
|
+
occurred_at INTEGER NOT NULL,
|
|
100
|
+
created_at INTEGER NOT NULL,
|
|
101
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
102
|
+
CHECK (status IN ('pending','claimed','acked','dead')),
|
|
103
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
104
|
+
last_error TEXT,
|
|
105
|
+
claimed_at INTEGER,
|
|
106
|
+
acked_at INTEGER,
|
|
107
|
+
retry_after INTEGER
|
|
108
|
+
);
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_mailbox_inputs_status
|
|
110
|
+
ON mailbox_inputs(status, id);
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_mailbox_inputs_coalesce
|
|
112
|
+
ON mailbox_inputs(status, coalesce_key);
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_mailbox_inputs_identity
|
|
114
|
+
ON mailbox_inputs(principal_id, stimulus_id);
|
|
115
|
+
CREATE TABLE IF NOT EXISTS mailbox_input_refs (
|
|
116
|
+
input_id INTEGER NOT NULL REFERENCES mailbox_inputs(id) ON DELETE CASCADE,
|
|
117
|
+
ref_id TEXT NOT NULL,
|
|
118
|
+
observation_ref TEXT,
|
|
119
|
+
PRIMARY KEY (input_id, ref_id)
|
|
120
|
+
);
|
|
121
|
+
CREATE TABLE IF NOT EXISTS mailbox_seen (
|
|
122
|
+
ref_id TEXT PRIMARY KEY,
|
|
123
|
+
seen_at INTEGER NOT NULL DEFAULT 0
|
|
124
|
+
);
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_mailbox_seen_at ON mailbox_seen(seen_at);
|
|
126
|
+
CREATE TABLE IF NOT EXISTS mailbox_schedules (
|
|
127
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
128
|
+
channel_key TEXT NOT NULL,
|
|
129
|
+
due_at INTEGER NOT NULL,
|
|
130
|
+
created_at INTEGER NOT NULL,
|
|
131
|
+
fired_at INTEGER
|
|
132
|
+
);
|
|
133
|
+
CREATE INDEX IF NOT EXISTS idx_mailbox_schedules_due
|
|
134
|
+
ON mailbox_schedules(fired_at, due_at);
|
|
135
|
+
`);
|
|
136
|
+
this.nativeInputs = new native_input_journal_js_1.NativeInputJournal(db, clock);
|
|
137
|
+
// Prepared once: enqueue runs per producer tick and re-preparing per call
|
|
138
|
+
// was measurable on backfill drains.
|
|
139
|
+
this.stmtInsertSeen = this.db.prepare(`INSERT OR IGNORE INTO mailbox_seen (ref_id, seen_at) VALUES (?, ?)`);
|
|
140
|
+
this.stmtInsert = this.db.prepare(`INSERT INTO mailbox_inputs
|
|
141
|
+
(stimulus_id, kind, principal_id, channel_key, preview_json, payload_json, reply_to,
|
|
142
|
+
coalesce_key, occurred_at, created_at)
|
|
143
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
144
|
+
this.stmtInsertRef = this.db.prepare(`INSERT OR IGNORE INTO mailbox_input_refs (input_id, ref_id, observation_ref)
|
|
145
|
+
VALUES (?, ?, ?)`);
|
|
146
|
+
this.stmtRefsFor = this.db.prepare(`SELECT ref_id, observation_ref FROM mailbox_input_refs WHERE input_id = ? ORDER BY rowid ASC`);
|
|
147
|
+
this.stmtClaimSelect = this.db.prepare(`SELECT id, stimulus_id, kind, principal_id, channel_key, preview_json, payload_json, reply_to,
|
|
148
|
+
coalesce_key, occurred_at, created_at, attempts
|
|
149
|
+
FROM mailbox_inputs
|
|
150
|
+
WHERE status = 'pending' AND COALESCE(retry_after, 0) <= ?
|
|
151
|
+
AND NOT EXISTS (SELECT 1 FROM native_input_deliveries n WHERE n.input_id=mailbox_inputs.id AND n.state!='prepared')
|
|
152
|
+
ORDER BY id ASC LIMIT 1`);
|
|
153
|
+
this.stmtClaimSelectKind = this.db.prepare(`SELECT id, stimulus_id, kind, principal_id, channel_key, preview_json, payload_json, reply_to,
|
|
154
|
+
coalesce_key, occurred_at, created_at, attempts
|
|
155
|
+
FROM mailbox_inputs
|
|
156
|
+
WHERE status = 'pending' AND COALESCE(retry_after, 0) <= ? AND kind = ?
|
|
157
|
+
AND NOT EXISTS (SELECT 1 FROM native_input_deliveries n WHERE n.input_id=mailbox_inputs.id AND n.state!='prepared')
|
|
158
|
+
ORDER BY id ASC LIMIT 1`);
|
|
159
|
+
this.stmtClaimUpdate = this.db.prepare(`UPDATE mailbox_inputs SET status = 'claimed', claimed_at = ?
|
|
160
|
+
WHERE id = ? AND status = 'pending'`);
|
|
161
|
+
this.stmtAck = this.db.prepare(`UPDATE mailbox_inputs SET status = 'acked', acked_at = ? WHERE id = ?`);
|
|
162
|
+
this.stmtRetry = this.db.prepare(`UPDATE mailbox_inputs
|
|
163
|
+
SET status = CASE WHEN attempts + 1 >= ${MAX_ATTEMPTS} THEN 'dead' ELSE 'pending' END,
|
|
164
|
+
attempts = attempts + 1, last_error = ?, claimed_at = NULL,
|
|
165
|
+
retry_after = ? + ${backoffCase('attempts')}
|
|
166
|
+
WHERE id = ? AND status = 'claimed'`);
|
|
167
|
+
this.stmtRetryStatus = this.db.prepare(`SELECT status FROM mailbox_inputs WHERE id = ?`);
|
|
168
|
+
this.stmtPruneAcked = this.db.prepare(`DELETE FROM mailbox_inputs WHERE status = 'acked' AND acked_at <= ?
|
|
169
|
+
AND NOT EXISTS (SELECT 1 FROM native_input_deliveries n WHERE n.input_id=mailbox_inputs.id AND n.state IN ('dispatching','accepted','uncertain'))`);
|
|
170
|
+
this.stmtPrunePending = this.db.prepare(`UPDATE mailbox_inputs
|
|
171
|
+
SET status = 'dead', last_error = 'stale_pending_expired'
|
|
172
|
+
WHERE status = 'pending' AND created_at <= ?
|
|
173
|
+
AND NOT EXISTS (SELECT 1 FROM native_input_deliveries n WHERE n.input_id=mailbox_inputs.id AND n.state IN ('dispatching','accepted','uncertain'))`);
|
|
174
|
+
this.stmtPruneSeen = this.db.prepare(`DELETE FROM mailbox_seen WHERE seen_at <= ?
|
|
175
|
+
AND NOT EXISTS (SELECT 1 FROM mailbox_input_refs r JOIN native_input_deliveries n ON n.input_id=r.input_id
|
|
176
|
+
WHERE r.ref_id=mailbox_seen.ref_id AND n.state IN ('dispatching','accepted','uncertain'))`);
|
|
177
|
+
// Same poison cap as retry(): a claim that expires its lease repeatedly
|
|
178
|
+
// (hung run, process death mid-flight) parks dead too, rather than
|
|
179
|
+
// re-pending forever at the head of the queue.
|
|
180
|
+
this.stmtReplay = this.db.prepare(`UPDATE mailbox_inputs
|
|
181
|
+
SET status = CASE WHEN attempts + 1 >= ${MAX_ATTEMPTS} THEN 'dead' ELSE 'pending' END,
|
|
182
|
+
attempts = attempts + 1, claimed_at = NULL,
|
|
183
|
+
retry_after = ? + ${backoffCase('attempts')}
|
|
184
|
+
WHERE status = 'claimed' AND COALESCE(claimed_at, 0) <= ?
|
|
185
|
+
AND NOT EXISTS (SELECT 1 FROM native_input_deliveries n WHERE n.input_id=mailbox_inputs.id AND n.state!='prepared')`);
|
|
186
|
+
// Grouped on the (status, id) index; never scans the acked bulk.
|
|
187
|
+
this.stmtDepth = this.db.prepare(`SELECT status, COUNT(*) AS n FROM mailbox_inputs
|
|
188
|
+
WHERE status IN ('pending','claimed','dead') GROUP BY status`);
|
|
189
|
+
this.stmtPendingCoalesced = this.db.prepare(`SELECT id, preview_json FROM mailbox_inputs
|
|
190
|
+
WHERE status = 'pending' AND coalesce_key = ? AND principal_id = ?
|
|
191
|
+
AND payload_json IS ? AND kind IS ? AND channel_key = ? AND reply_to IS ?
|
|
192
|
+
AND NOT EXISTS (SELECT 1 FROM native_input_deliveries n WHERE n.input_id=mailbox_inputs.id AND n.state!='prepared')
|
|
193
|
+
ORDER BY id ASC LIMIT 1`);
|
|
194
|
+
this.stmtAppendPreview = this.db.prepare(`UPDATE mailbox_inputs SET preview_json = ? WHERE id = ? AND status = 'pending'`);
|
|
195
|
+
this.stmtIdentityPayload = this.db.prepare(`SELECT payload_json FROM mailbox_inputs
|
|
196
|
+
WHERE principal_id = ? AND (stimulus_id = ? OR id IN (
|
|
197
|
+
SELECT input_id FROM mailbox_input_refs WHERE ref_id = ?
|
|
198
|
+
)) ORDER BY id DESC LIMIT 1`);
|
|
199
|
+
}
|
|
200
|
+
now() {
|
|
201
|
+
return this.clock();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Make a stimulus durable. Returns the row id, or `null` when every ref it
|
|
205
|
+
* carries was already accepted.
|
|
206
|
+
*
|
|
207
|
+
* Dedupe is PER REF, not per stimulus shape. A boundary key fails on partial
|
|
208
|
+
* redelivery: [r1] enqueues, the producer's cursor commit fails, the next
|
|
209
|
+
* tick delivers [r1,r2] under a different boundary — and r1 runs twice.
|
|
210
|
+
* A stimulus with no unseen ref is dropped; one with any unseen ref is
|
|
211
|
+
* stored with exactly its unseen refs.
|
|
212
|
+
*/
|
|
213
|
+
enqueue(stimulus) {
|
|
214
|
+
if (!isStimulusKind(stimulus.kind)) {
|
|
215
|
+
throw new Error(`Mailbox: unknown stimulus kind "${String(stimulus.kind)}"`);
|
|
216
|
+
}
|
|
217
|
+
if (stimulus.principalId.trim() === '') {
|
|
218
|
+
throw new Error('Mailbox: a stimulus states whose it is');
|
|
219
|
+
}
|
|
220
|
+
if (stimulus.channelKey.trim() === '') {
|
|
221
|
+
throw new Error('Mailbox: a stimulus states where it came from');
|
|
222
|
+
}
|
|
223
|
+
const payload = (0, stimulus_payload_js_1.encodeStimulusPayload)(stimulus.payload);
|
|
224
|
+
const accepted = this.stmtIdentityPayload.get(stimulus.principalId, stimulus.id, stimulus.id);
|
|
225
|
+
if (accepted && accepted.payload_json !== payload) {
|
|
226
|
+
throw new Error('Mailbox payload conflict for an accepted stimulus identity');
|
|
227
|
+
}
|
|
228
|
+
// With no refs stated the producer's own id is the identity, so a
|
|
229
|
+
// redelivery of the same stimulus dedupes on it rather than opening a
|
|
230
|
+
// second row.
|
|
231
|
+
const stated = stimulus.refs && stimulus.refs.length > 0
|
|
232
|
+
? stimulus.refs.map((ref) => ({ refId: ref.refId, observationRef: ref.observationRef }))
|
|
233
|
+
: [{ refId: stimulus.id, observationRef: null }];
|
|
234
|
+
if (stated.length > MAX_REFS) {
|
|
235
|
+
throw new Error(`Mailbox: a stimulus carries at most ${MAX_REFS} refs`);
|
|
236
|
+
}
|
|
237
|
+
for (const ref of stated) {
|
|
238
|
+
if (typeof ref.refId !== 'string' || ref.refId.trim() === '') {
|
|
239
|
+
throw new Error('Mailbox: a ref states its id');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const seen = new Set();
|
|
243
|
+
const refIds = stated.map((ref) => ref.refId);
|
|
244
|
+
for (let i = 0; i < refIds.length; i += SEEN_CHUNK) {
|
|
245
|
+
const chunk = refIds.slice(i, i + SEEN_CHUNK);
|
|
246
|
+
const rows = this.db
|
|
247
|
+
.prepare(`SELECT ref_id FROM mailbox_seen WHERE ref_id IN (${chunk.map(() => '?').join(',')})`)
|
|
248
|
+
.all(...chunk);
|
|
249
|
+
for (const row of rows)
|
|
250
|
+
seen.add(row.ref_id);
|
|
251
|
+
}
|
|
252
|
+
const fresh = stated.filter((ref) => !seen.has(ref.refId));
|
|
253
|
+
if (fresh.length === 0) {
|
|
254
|
+
return null; // fully redelivered — already durable
|
|
255
|
+
}
|
|
256
|
+
const preview = [...(stimulus.preview ?? [])];
|
|
257
|
+
return this.db.transaction(() => {
|
|
258
|
+
const now = this.now();
|
|
259
|
+
for (const ref of fresh) {
|
|
260
|
+
this.stmtInsertSeen.run(ref.refId, now);
|
|
261
|
+
}
|
|
262
|
+
// Mechanical merge: an existing pending row with the same producer key
|
|
263
|
+
// and principal takes these refs instead of opening a second row. Both
|
|
264
|
+
// rows are still pending, so nothing that was already handed to the loop
|
|
265
|
+
// is touched.
|
|
266
|
+
const key = stimulus.coalesceKey ?? null;
|
|
267
|
+
if (key !== null && key !== '') {
|
|
268
|
+
const open = this.stmtPendingCoalesced.get(key, stimulus.principalId, payload, stimulus.kind, stimulus.channelKey, stimulus.replyTo ?? null);
|
|
269
|
+
if (open) {
|
|
270
|
+
for (const ref of fresh) {
|
|
271
|
+
this.stmtInsertRef.run(open.id, ref.refId, ref.observationRef);
|
|
272
|
+
}
|
|
273
|
+
if (preview.length > 0) {
|
|
274
|
+
const existing = JSON.parse(open.preview_json);
|
|
275
|
+
this.stmtAppendPreview.run(JSON.stringify([...existing, ...preview]), open.id);
|
|
276
|
+
}
|
|
277
|
+
return open.id;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const inserted = this.stmtInsert.run(stimulus.id, stimulus.kind, stimulus.principalId, stimulus.channelKey, JSON.stringify(preview), payload, stimulus.replyTo ?? null, key, stimulus.occurredAt, now);
|
|
281
|
+
const id = Number(inserted.lastInsertRowid);
|
|
282
|
+
for (const ref of fresh) {
|
|
283
|
+
this.stmtInsertRef.run(id, ref.refId, ref.observationRef);
|
|
284
|
+
}
|
|
285
|
+
return id;
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Take the next pending row under a lease. A corrupt row parks dead and the
|
|
290
|
+
* scan continues.
|
|
291
|
+
*
|
|
292
|
+
* `prefer` orders kinds ahead of the rest, oldest-first within each. It is
|
|
293
|
+
* ordering, not meaning: §4.3 forbids a host FIFO in which the person's
|
|
294
|
+
* current request waits behind a mass replay, and the kind a producer stated
|
|
295
|
+
* is the only thing that can say which is which without reading content.
|
|
296
|
+
*/
|
|
297
|
+
claimNext(options = {}) {
|
|
298
|
+
const prefer = options.prefer ?? [];
|
|
299
|
+
const select = () => {
|
|
300
|
+
const now = this.now();
|
|
301
|
+
for (const kind of prefer) {
|
|
302
|
+
const preferred = this.stmtClaimSelectKind.get(now, kind);
|
|
303
|
+
if (preferred)
|
|
304
|
+
return preferred;
|
|
305
|
+
}
|
|
306
|
+
return this.stmtClaimSelect.get(now);
|
|
307
|
+
};
|
|
308
|
+
let row = select();
|
|
309
|
+
while (row) {
|
|
310
|
+
const candidate = row;
|
|
311
|
+
let preview;
|
|
312
|
+
let hydrated;
|
|
313
|
+
try {
|
|
314
|
+
preview = JSON.parse(candidate.preview_json);
|
|
315
|
+
if (!Array.isArray(preview) || preview.some((line) => typeof line !== 'string')) {
|
|
316
|
+
throw new Error('preview_json is invalid');
|
|
317
|
+
}
|
|
318
|
+
hydrated = this.hydrate(candidate, preview);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
const diagnostic = new MailboxCorruptionError(candidate.id, error);
|
|
322
|
+
this.db
|
|
323
|
+
.prepare(`UPDATE mailbox_inputs SET status = 'dead', last_error = ?, claimed_at = NULL
|
|
324
|
+
WHERE id = ? AND status = 'pending'`)
|
|
325
|
+
.run(`${diagnostic.code}:row-${candidate.id}`, candidate.id);
|
|
326
|
+
row = select();
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const claimed = this.stmtClaimUpdate.run(this.now(), candidate.id);
|
|
330
|
+
if (claimed.changes !== 1) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
return { ...hydrated, status: 'claimed' };
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
hydrate(row, preview) {
|
|
338
|
+
const nativeDelivery = this.nativeInputs.get(row.id);
|
|
339
|
+
const refs = this.stmtRefsFor.all(row.id).map((ref) => ({ refId: ref.ref_id, observationRef: ref.observation_ref }));
|
|
340
|
+
return {
|
|
341
|
+
id: row.id,
|
|
342
|
+
stimulusId: row.stimulus_id,
|
|
343
|
+
kind: isStimulusKind(row.kind) ? row.kind : null,
|
|
344
|
+
principalId: row.principal_id,
|
|
345
|
+
channelKey: row.channel_key,
|
|
346
|
+
refs,
|
|
347
|
+
preview,
|
|
348
|
+
...(row.payload_json === null ? {} : { payload: (0, stimulus_payload_js_1.decodeStimulusPayload)(row.payload_json) }),
|
|
349
|
+
replyTo: row.reply_to,
|
|
350
|
+
coalesceKey: row.coalesce_key,
|
|
351
|
+
occurredAt: row.occurred_at,
|
|
352
|
+
createdAt: row.created_at,
|
|
353
|
+
status: 'pending',
|
|
354
|
+
attempts: row.attempts,
|
|
355
|
+
...(nativeDelivery ? { nativeDelivery } : {}),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/** Renew only a live runtime-owned claim; native ACKs remain independent. */
|
|
359
|
+
renewClaim(id, now = this.now()) {
|
|
360
|
+
this.db
|
|
361
|
+
.prepare("UPDATE mailbox_inputs SET claimed_at=? WHERE id=? AND status='claimed'")
|
|
362
|
+
.run(now, id);
|
|
363
|
+
}
|
|
364
|
+
/** Inspect one accepted input without claiming it or changing its lease. */
|
|
365
|
+
readInput(stimulusId, principalId) {
|
|
366
|
+
const row = this.db
|
|
367
|
+
.prepare(`SELECT id, stimulus_id, kind, principal_id, channel_key,
|
|
368
|
+
preview_json, payload_json, reply_to, coalesce_key, occurred_at, created_at, attempts, status
|
|
369
|
+
FROM mailbox_inputs WHERE stimulus_id = ? AND principal_id = ? ORDER BY id DESC LIMIT 1`)
|
|
370
|
+
.get(stimulusId, principalId);
|
|
371
|
+
if (!row)
|
|
372
|
+
return null;
|
|
373
|
+
try {
|
|
374
|
+
const preview = JSON.parse(row.preview_json);
|
|
375
|
+
if (!Array.isArray(preview) || preview.some((line) => typeof line !== 'string')) {
|
|
376
|
+
throw new Error('Invalid input preview');
|
|
377
|
+
}
|
|
378
|
+
return { ...this.hydrate(row, preview), status: row.status };
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
throw new MailboxCorruptionError(row.id, error);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/** Reopen claims that never crossed the native dispatch boundary. */
|
|
385
|
+
recoverOrphanedClaims(now = this.now()) {
|
|
386
|
+
return this.db.transaction(() => {
|
|
387
|
+
const replayed = this.db
|
|
388
|
+
.prepare(`UPDATE mailbox_inputs
|
|
389
|
+
SET status='pending', claimed_at=NULL, retry_after=NULL
|
|
390
|
+
WHERE status='claimed'
|
|
391
|
+
AND NOT EXISTS (
|
|
392
|
+
SELECT 1 FROM native_input_deliveries n
|
|
393
|
+
WHERE n.input_id=mailbox_inputs.id
|
|
394
|
+
AND n.state IN ('dispatching','accepted','uncertain','settled')
|
|
395
|
+
)`)
|
|
396
|
+
.run().changes;
|
|
397
|
+
this.db
|
|
398
|
+
.prepare(`UPDATE mailbox_inputs
|
|
399
|
+
SET status='acked', acked_at=COALESCE(acked_at, ?), claimed_at=NULL
|
|
400
|
+
WHERE status='claimed'
|
|
401
|
+
AND EXISTS (
|
|
402
|
+
SELECT 1 FROM native_input_deliveries n
|
|
403
|
+
WHERE n.input_id=mailbox_inputs.id AND n.state='settled'
|
|
404
|
+
)`)
|
|
405
|
+
.run(now);
|
|
406
|
+
return replayed;
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
/** Identity lookup for transport recovery; never returns another principal's payload. */
|
|
410
|
+
principalsForInput(stimulusId) {
|
|
411
|
+
return this.db
|
|
412
|
+
.prepare('SELECT DISTINCT principal_id FROM mailbox_inputs WHERE stimulus_id = ?')
|
|
413
|
+
.all(stimulusId).map((row) => row.principal_id);
|
|
414
|
+
}
|
|
415
|
+
/** The exact native turn's accepted input cohort, scoped to its principal. */
|
|
416
|
+
readNativeTurnInputs(stimulusId, principalId, options = {}) {
|
|
417
|
+
const primary = this.db
|
|
418
|
+
.prepare('SELECT id FROM mailbox_inputs WHERE stimulus_id=? AND principal_id=? ORDER BY id DESC LIMIT 1')
|
|
419
|
+
.get(stimulusId, principalId);
|
|
420
|
+
if (!primary)
|
|
421
|
+
throw new Error('Native turn input is not visible to this principal');
|
|
422
|
+
const receipt = this.nativeInputs.get(primary.id)?.receipt;
|
|
423
|
+
if (!receipt)
|
|
424
|
+
throw new Error('Native turn input has no accepted receipt');
|
|
425
|
+
const page = this.nativeInputs.listByReceipt(receipt, principalId, options);
|
|
426
|
+
if (page.inputIds.length === 0)
|
|
427
|
+
return { items: [], nextCursor: null };
|
|
428
|
+
const placeholders = page.inputIds.map(() => '?').join(',');
|
|
429
|
+
const rows = this.db
|
|
430
|
+
.prepare(`SELECT id, stimulus_id, kind, channel_key, occurred_at, status
|
|
431
|
+
FROM mailbox_inputs WHERE id IN (${placeholders}) AND principal_id=?`)
|
|
432
|
+
.all(...page.inputIds, principalId);
|
|
433
|
+
if (rows.length !== page.inputIds.length) {
|
|
434
|
+
throw new Error('Native turn input cohort is incomplete');
|
|
435
|
+
}
|
|
436
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
437
|
+
return {
|
|
438
|
+
items: page.inputIds.map((id) => {
|
|
439
|
+
const row = byId.get(id);
|
|
440
|
+
return {
|
|
441
|
+
id,
|
|
442
|
+
stimulusId: row.stimulus_id,
|
|
443
|
+
kind: isStimulusKind(row.kind) ? row.kind : null,
|
|
444
|
+
channelKey: row.channel_key,
|
|
445
|
+
occurredAt: row.occurred_at,
|
|
446
|
+
status: row.status,
|
|
447
|
+
};
|
|
448
|
+
}),
|
|
449
|
+
nextCursor: page.nextCursor,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
/** Inputs needing native/result reconciliation, never a second model execution. */
|
|
453
|
+
unsettledNative(limit, afterId = 0) {
|
|
454
|
+
return this.nativeInputs.pending(limit, afterId).map((id) => {
|
|
455
|
+
const row = this.db
|
|
456
|
+
.prepare('SELECT * FROM mailbox_inputs WHERE id=?')
|
|
457
|
+
.get(id);
|
|
458
|
+
if (!row)
|
|
459
|
+
throw new Error(`Native delivery input ${id} is missing`);
|
|
460
|
+
const preview = JSON.parse(row.preview_json);
|
|
461
|
+
if (!Array.isArray(preview) || preview.some((line) => typeof line !== 'string')) {
|
|
462
|
+
throw new MailboxCorruptionError(id, new Error('Invalid input preview'));
|
|
463
|
+
}
|
|
464
|
+
return { ...this.hydrate(row, preview), status: row.status };
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* The loop took this stimulus.
|
|
469
|
+
*
|
|
470
|
+
* That is all this says. It is not a claim that anything was read, decided
|
|
471
|
+
* or done — §4.4 is explicit that a delivery ACK is not a work receipt, and
|
|
472
|
+
* there is deliberately no field here to record one in.
|
|
473
|
+
*/
|
|
474
|
+
ack(id) {
|
|
475
|
+
this.stmtAck.run(this.now(), id);
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Return a claim to pending, or park it dead after MAX_ATTEMPTS. Returns the
|
|
479
|
+
* resulting status so the caller can be LOUD about a dead row — a permanent
|
|
480
|
+
* loss must never be silent.
|
|
481
|
+
*/
|
|
482
|
+
retry(id, error) {
|
|
483
|
+
const result = this.stmtRetry.run(error.slice(0, 500), this.now(), id);
|
|
484
|
+
if (result.changes !== 1) {
|
|
485
|
+
return 'noop'; // replayStale already flipped it
|
|
486
|
+
}
|
|
487
|
+
const row = this.stmtRetryStatus.get(id);
|
|
488
|
+
return row?.status === 'dead' ? 'dead' : 'pending';
|
|
489
|
+
}
|
|
490
|
+
/** Park a row dead without spending an attempt. */
|
|
491
|
+
quarantine(id, reason) {
|
|
492
|
+
this.db
|
|
493
|
+
.prepare(`UPDATE mailbox_inputs SET status = 'dead', last_error = ?, claimed_at = NULL
|
|
494
|
+
WHERE id = ? AND status IN ('claimed', 'pending')`)
|
|
495
|
+
.run(reason.slice(0, 500), id);
|
|
496
|
+
}
|
|
497
|
+
replayStale(olderThanMs, now = this.now()) {
|
|
498
|
+
return this.replayStaleDetailed(olderThanMs, now).replayed;
|
|
499
|
+
}
|
|
500
|
+
replayStaleDetailed(olderThanMs, now = this.now()) {
|
|
501
|
+
// Housekeeping rides along: acked rows age out, stale pending rows park as
|
|
502
|
+
// dead (visible, bounded), and the dedupe horizon stays wide but finite —
|
|
503
|
+
// no table here grows without bound.
|
|
504
|
+
const stalePending = this.db
|
|
505
|
+
.prepare(`SELECT id, stimulus_id, kind, principal_id, channel_key, preview_json, payload_json, reply_to,
|
|
506
|
+
coalesce_key, occurred_at, created_at, attempts
|
|
507
|
+
FROM mailbox_inputs
|
|
508
|
+
WHERE status = 'pending' AND created_at <= ?
|
|
509
|
+
AND NOT EXISTS (SELECT 1 FROM native_input_deliveries n WHERE n.input_id=mailbox_inputs.id AND n.state IN ('dispatching','accepted','uncertain'))
|
|
510
|
+
ORDER BY id ASC`)
|
|
511
|
+
.all(now - PENDING_RETENTION_MS);
|
|
512
|
+
this.stmtPruneAcked.run(now - ACKED_RETENTION_MS);
|
|
513
|
+
this.stmtPrunePending.run(now - PENDING_RETENTION_MS);
|
|
514
|
+
this.stmtPruneSeen.run(now - REF_RETENTION_MS);
|
|
515
|
+
const cutoff = now - olderThanMs;
|
|
516
|
+
const dying = this.db
|
|
517
|
+
.prepare(`SELECT id, stimulus_id, kind, principal_id, channel_key, preview_json, payload_json, reply_to,
|
|
518
|
+
coalesce_key, occurred_at, created_at, attempts
|
|
519
|
+
FROM mailbox_inputs
|
|
520
|
+
WHERE status = 'claimed' AND attempts + 1 >= ${MAX_ATTEMPTS}
|
|
521
|
+
AND COALESCE(claimed_at, 0) <= ?
|
|
522
|
+
AND NOT EXISTS (SELECT 1 FROM native_input_deliveries n WHERE n.input_id=mailbox_inputs.id AND n.state IN ('dispatching','accepted','uncertain'))
|
|
523
|
+
ORDER BY id ASC`)
|
|
524
|
+
.all(cutoff);
|
|
525
|
+
const result = this.stmtReplay.run(now, cutoff);
|
|
526
|
+
const dead = (row, attempts) => {
|
|
527
|
+
let preview = [];
|
|
528
|
+
try {
|
|
529
|
+
preview = JSON.parse(row.preview_json);
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
preview = [];
|
|
533
|
+
}
|
|
534
|
+
return { ...this.hydrate(row, preview), status: 'dead', attempts };
|
|
535
|
+
};
|
|
536
|
+
return {
|
|
537
|
+
replayed: result.changes,
|
|
538
|
+
newlyDead: [
|
|
539
|
+
...stalePending.map((row) => dead(row, row.attempts)),
|
|
540
|
+
...dying.map((row) => dead(row, row.attempts + 1)),
|
|
541
|
+
],
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
depth() {
|
|
545
|
+
const rows = this.stmtDepth.all();
|
|
546
|
+
const byStatus = new Map(rows.map((row) => [row.status, row.n]));
|
|
547
|
+
return {
|
|
548
|
+
pending: byStatus.get('pending') ?? 0,
|
|
549
|
+
claimed: byStatus.get('claimed') ?? 0,
|
|
550
|
+
dead: byStatus.get('dead') ?? 0,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Schedule a wake. The fourth job: when to come back, and for what channel.
|
|
555
|
+
*
|
|
556
|
+
* `mailbox_schedules` is this daemon's own alarm clock, not anyone's
|
|
557
|
+
* calendar — a consumer's calendar is source data and is read through the
|
|
558
|
+
* source actions (design §6.3).
|
|
559
|
+
*/
|
|
560
|
+
scheduleWake(channelKey, dueAt) {
|
|
561
|
+
const inserted = this.db
|
|
562
|
+
.prepare(`INSERT INTO mailbox_schedules (channel_key, due_at, created_at) VALUES (?, ?, ?)`)
|
|
563
|
+
.run(channelKey, dueAt, this.now());
|
|
564
|
+
return Number(inserted.lastInsertRowid);
|
|
565
|
+
}
|
|
566
|
+
/** Wakes that are due and have not fired. The caller marks them fired when it acts. */
|
|
567
|
+
dueWakes(now = this.now(), limit = 50) {
|
|
568
|
+
return this.db
|
|
569
|
+
.prepare(`SELECT id, channel_key, due_at FROM mailbox_schedules
|
|
570
|
+
WHERE fired_at IS NULL AND due_at <= ?
|
|
571
|
+
ORDER BY due_at ASC, id ASC LIMIT ?`)
|
|
572
|
+
.all(now, Math.min(Math.max(limit, 1), 500)).map((row) => ({ id: row.id, channelKey: row.channel_key, dueAt: row.due_at }));
|
|
573
|
+
}
|
|
574
|
+
/** Mark a wake fired. Idempotent: a second call changes nothing. */
|
|
575
|
+
markWakeFired(id) {
|
|
576
|
+
return (this.db
|
|
577
|
+
.prepare(`UPDATE mailbox_schedules SET fired_at = ? WHERE id = ? AND fired_at IS NULL`)
|
|
578
|
+
.run(this.now(), id).changes === 1);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
exports.Mailbox = Mailbox;
|
|
582
|
+
//# sourceMappingURL=mailbox.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DatabaseAdapter } from '../db-manager.js';
|
|
2
|
+
import type { BeginModelRunInput, ModelRunRecord, ModelRunStatus } from './model-run-types.js';
|
|
3
|
+
type ModelRunAdapter = Pick<DatabaseAdapter, 'prepare'>;
|
|
4
|
+
export declare function beginModelRun(adapter: ModelRunAdapter, input: BeginModelRunInput): ModelRunRecord;
|
|
5
|
+
export declare function commitModelRun(adapter: ModelRunAdapter, modelRunId: string, summary?: string, tokenCount?: number): ModelRunRecord;
|
|
6
|
+
export declare function failModelRun(adapter: ModelRunAdapter, modelRunId: string, errorSummary: string, tokenCount?: number): ModelRunRecord;
|
|
7
|
+
export declare function getModelRun(adapter: ModelRunAdapter, modelRunId: string): ModelRunRecord | null;
|
|
8
|
+
export interface ModelRunNativeInput {
|
|
9
|
+
inputId: number;
|
|
10
|
+
stimulusId: string;
|
|
11
|
+
kind: string;
|
|
12
|
+
principalId: string;
|
|
13
|
+
channelKey: string;
|
|
14
|
+
occurredAt: number;
|
|
15
|
+
status: string;
|
|
16
|
+
nativeState: string;
|
|
17
|
+
}
|
|
18
|
+
/** Read one run's native input cohort without copying payloads or inventing legacy principals. */
|
|
19
|
+
export declare function listModelRunNativeInputs(adapter: ModelRunAdapter, modelRunId: string, principalId: string, options?: {
|
|
20
|
+
afterId?: number;
|
|
21
|
+
limit?: number;
|
|
22
|
+
}): {
|
|
23
|
+
items: ModelRunNativeInput[];
|
|
24
|
+
nextCursor: number | null;
|
|
25
|
+
runStatus: ModelRunStatus;
|
|
26
|
+
};
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=model-run-store.d.ts.map
|