@jungjaehoon/mama-os 0.32.0 → 0.32.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +15 -13
- package/dist/agent/agent-loop.d.ts +10 -1
- package/dist/agent/agent-loop.d.ts.map +1 -1
- package/dist/agent/agent-loop.js +221 -103
- package/dist/agent/agent-loop.js.map +1 -1
- package/dist/agent/backend-model-policy.d.ts +9 -0
- package/dist/agent/backend-model-policy.d.ts.map +1 -0
- package/dist/agent/backend-model-policy.js +21 -0
- package/dist/agent/backend-model-policy.js.map +1 -0
- package/dist/agent/backend-model-runner-factory.d.ts +27 -0
- package/dist/agent/backend-model-runner-factory.d.ts.map +1 -0
- package/dist/agent/backend-model-runner-factory.js +85 -0
- package/dist/agent/backend-model-runner-factory.js.map +1 -0
- package/dist/agent/cline-cli-adapter.d.ts +124 -0
- package/dist/agent/cline-cli-adapter.d.ts.map +1 -0
- package/dist/agent/cline-cli-adapter.js +1040 -0
- package/dist/agent/cline-cli-adapter.js.map +1 -0
- package/dist/agent/cline-native-tool-policy.d.ts +3 -0
- package/dist/agent/cline-native-tool-policy.d.ts.map +1 -0
- package/dist/agent/cline-native-tool-policy.js +50 -0
- package/dist/agent/cline-native-tool-policy.js.map +1 -0
- package/dist/agent/code-act/constants.d.ts +1 -1
- package/dist/agent/code-act/constants.d.ts.map +1 -1
- package/dist/agent/code-act/constants.js +13 -1
- package/dist/agent/code-act/constants.js.map +1 -1
- package/dist/agent/code-act/host-bridge.d.ts.map +1 -1
- package/dist/agent/code-act/host-bridge.js +35 -74
- package/dist/agent/code-act/host-bridge.js.map +1 -1
- package/dist/agent/codex-app-server-process.d.ts +5 -0
- package/dist/agent/codex-app-server-process.d.ts.map +1 -1
- package/dist/agent/codex-app-server-process.js +30 -0
- package/dist/agent/codex-app-server-process.js.map +1 -1
- package/dist/agent/codex-auxiliary-tools.d.ts +17 -0
- package/dist/agent/codex-auxiliary-tools.d.ts.map +1 -0
- package/dist/agent/codex-auxiliary-tools.js +267 -0
- package/dist/agent/codex-auxiliary-tools.js.map +1 -0
- package/dist/agent/codex-home.d.ts.map +1 -1
- package/dist/agent/codex-home.js +1 -0
- package/dist/agent/codex-home.js.map +1 -1
- package/dist/agent/gateway-tool-catalog.d.ts +14 -0
- package/dist/agent/gateway-tool-catalog.d.ts.map +1 -0
- package/dist/agent/gateway-tool-catalog.js +41 -0
- package/dist/agent/gateway-tool-catalog.js.map +1 -0
- package/dist/agent/gateway-tool-executor.d.ts +12 -1
- package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
- package/dist/agent/gateway-tool-executor.js +149 -15
- package/dist/agent/gateway-tool-executor.js.map +1 -1
- package/dist/agent/gateway-tools.md +5 -7
- package/dist/agent/managed-agent-runtime-sync.d.ts.map +1 -1
- package/dist/agent/managed-agent-runtime-sync.js +17 -3
- package/dist/agent/managed-agent-runtime-sync.js.map +1 -1
- package/dist/agent/managed-agent-validation.js +1 -1
- package/dist/agent/managed-agent-validation.js.map +1 -1
- package/dist/agent/model-runner.d.ts +11 -1
- package/dist/agent/model-runner.d.ts.map +1 -1
- package/dist/agent/model-runner.js +12 -1
- package/dist/agent/model-runner.js.map +1 -1
- package/dist/agent/session-pool.d.ts +10 -9
- package/dist/agent/session-pool.d.ts.map +1 -1
- package/dist/agent/session-pool.js +12 -47
- package/dist/agent/session-pool.js.map +1 -1
- package/dist/agent/tool-registry.d.ts +6 -4
- package/dist/agent/tool-registry.d.ts.map +1 -1
- package/dist/agent/tool-registry.js +62 -30
- package/dist/agent/tool-registry.js.map +1 -1
- package/dist/agent/types.d.ts +36 -4
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/api/graph-api.d.ts.map +1 -1
- package/dist/api/graph-api.js +15 -5
- package/dist/api/graph-api.js.map +1 -1
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +7 -16
- package/dist/api/index.js.map +1 -1
- package/dist/cli/commands/connector.d.ts +6 -1
- package/dist/cli/commands/connector.d.ts.map +1 -1
- package/dist/cli/commands/connector.js +61 -45
- package/dist/cli/commands/connector.js.map +1 -1
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +43 -10
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +12 -0
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/setup.d.ts +22 -0
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +79 -28
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/start.d.ts +13 -9
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +190 -97
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +4 -0
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/config/config-manager.d.ts.map +1 -1
- package/dist/cli/config/config-manager.js +63 -14
- package/dist/cli/config/config-manager.js.map +1 -1
- package/dist/cli/config/types.d.ts +12 -5
- package/dist/cli/config/types.d.ts.map +1 -1
- package/dist/cli/config/types.js +3 -4
- package/dist/cli/config/types.js.map +1 -1
- package/dist/cli/index.js +6 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/runtime/agent-loop-init.d.ts +1 -1
- package/dist/cli/runtime/agent-loop-init.d.ts.map +1 -1
- package/dist/cli/runtime/agent-loop-init.js +8 -3
- package/dist/cli/runtime/agent-loop-init.js.map +1 -1
- package/dist/cli/runtime/api-routes-init.d.ts +38 -1
- package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
- package/dist/cli/runtime/api-routes-init.js +150 -30
- package/dist/cli/runtime/api-routes-init.js.map +1 -1
- package/dist/cli/runtime/api-server-init.d.ts +4 -0
- package/dist/cli/runtime/api-server-init.d.ts.map +1 -1
- package/dist/cli/runtime/api-server-init.js +3 -1
- package/dist/cli/runtime/api-server-init.js.map +1 -1
- package/dist/cli/runtime/connector-bootstrap.d.ts +8 -0
- package/dist/cli/runtime/connector-bootstrap.d.ts.map +1 -0
- package/dist/cli/runtime/connector-bootstrap.js +14 -0
- package/dist/cli/runtime/connector-bootstrap.js.map +1 -0
- package/dist/cli/runtime/connector-init.d.ts +4 -1
- package/dist/cli/runtime/connector-init.d.ts.map +1 -1
- package/dist/cli/runtime/connector-init.js +2 -8
- package/dist/cli/runtime/connector-init.js.map +1 -1
- package/dist/cli/runtime/envelope-bootstrap.d.ts +2 -1
- package/dist/cli/runtime/envelope-bootstrap.d.ts.map +1 -1
- package/dist/cli/runtime/envelope-bootstrap.js +7 -15
- package/dist/cli/runtime/envelope-bootstrap.js.map +1 -1
- package/dist/cli/runtime/gateway-init.d.ts +1 -1
- package/dist/cli/runtime/gateway-init.d.ts.map +1 -1
- package/dist/cli/runtime/gateway-init.js.map +1 -1
- package/dist/cli/runtime/mama-core-init.d.ts +10 -2
- package/dist/cli/runtime/mama-core-init.d.ts.map +1 -1
- package/dist/cli/runtime/mama-core-init.js +52 -68
- package/dist/cli/runtime/mama-core-init.js.map +1 -1
- package/dist/cli/runtime/memory-agent-init.d.ts.map +1 -1
- package/dist/cli/runtime/memory-agent-init.js +18 -5
- package/dist/cli/runtime/memory-agent-init.js.map +1 -1
- package/dist/cli/runtime/scheduler-init.d.ts +4 -0
- package/dist/cli/runtime/scheduler-init.d.ts.map +1 -1
- package/dist/cli/runtime/scheduler-init.js +24 -3
- package/dist/cli/runtime/scheduler-init.js.map +1 -1
- package/dist/cli/runtime/shutdown.d.ts +4 -0
- package/dist/cli/runtime/shutdown.d.ts.map +1 -1
- package/dist/cli/runtime/shutdown.js +30 -6
- package/dist/cli/runtime/shutdown.js.map +1 -1
- package/dist/cli/runtime/utilities.d.ts +2 -0
- package/dist/cli/runtime/utilities.d.ts.map +1 -1
- package/dist/cli/runtime/utilities.js +33 -0
- package/dist/cli/runtime/utilities.js.map +1 -1
- package/dist/connectors/index.d.ts +4 -1
- package/dist/connectors/index.d.ts.map +1 -1
- package/dist/connectors/index.js +3 -2
- package/dist/connectors/index.js.map +1 -1
- package/dist/connectors/private-connector-policy.d.ts +150 -0
- package/dist/connectors/private-connector-policy.d.ts.map +1 -0
- package/dist/connectors/private-connector-policy.js +289 -0
- package/dist/connectors/private-connector-policy.js.map +1 -0
- package/dist/connectors/private-prompt-overlay.d.ts +17 -0
- package/dist/connectors/private-prompt-overlay.d.ts.map +1 -0
- package/dist/connectors/private-prompt-overlay.js +156 -0
- package/dist/connectors/private-prompt-overlay.js.map +1 -0
- package/dist/db/migrations/operator-task-external-lifecycle.d.ts +8 -0
- package/dist/db/migrations/operator-task-external-lifecycle.d.ts.map +1 -0
- package/dist/db/migrations/operator-task-external-lifecycle.js +169 -0
- package/dist/db/migrations/operator-task-external-lifecycle.js.map +1 -0
- package/dist/envelope/enforcer.js +8 -7
- package/dist/envelope/enforcer.js.map +1 -1
- package/dist/envelope/reactive-config.d.ts.map +1 -1
- package/dist/envelope/reactive-config.js +2 -1
- package/dist/envelope/reactive-config.js.map +1 -1
- package/dist/envelope/tool-connector-scope.d.ts.map +1 -1
- package/dist/envelope/tool-connector-scope.js +4 -0
- package/dist/envelope/tool-connector-scope.js.map +1 -1
- package/dist/gateways/discord.d.ts.map +1 -1
- package/dist/gateways/discord.js +11 -6
- package/dist/gateways/discord.js.map +1 -1
- package/dist/gateways/image-analyzer.d.ts +5 -0
- package/dist/gateways/image-analyzer.d.ts.map +1 -1
- package/dist/gateways/image-analyzer.js +8 -0
- package/dist/gateways/image-analyzer.js.map +1 -1
- package/dist/gateways/message-router.d.ts +11 -1
- package/dist/gateways/message-router.d.ts.map +1 -1
- package/dist/gateways/message-router.js +146 -52
- package/dist/gateways/message-router.js.map +1 -1
- package/dist/gateways/slack.d.ts.map +1 -1
- package/dist/gateways/slack.js +6 -2
- package/dist/gateways/slack.js.map +1 -1
- package/dist/gateways/telegram-message-ledger.d.ts +8 -1
- package/dist/gateways/telegram-message-ledger.d.ts.map +1 -1
- package/dist/gateways/telegram-message-ledger.js +141 -24
- package/dist/gateways/telegram-message-ledger.js.map +1 -1
- package/dist/gateways/telegram.d.ts +2 -0
- package/dist/gateways/telegram.d.ts.map +1 -1
- package/dist/gateways/telegram.js +19 -12
- package/dist/gateways/telegram.js.map +1 -1
- package/dist/gateways/types.d.ts +1 -1
- package/dist/gateways/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/multi-agent/agent-process-manager.d.ts +14 -4
- package/dist/multi-agent/agent-process-manager.d.ts.map +1 -1
- package/dist/multi-agent/agent-process-manager.js +144 -47
- package/dist/multi-agent/agent-process-manager.js.map +1 -1
- package/dist/multi-agent/dashboard-agent-persona.d.ts +3 -3
- package/dist/multi-agent/dashboard-agent-persona.d.ts.map +1 -1
- package/dist/multi-agent/dashboard-agent-persona.js +10 -13
- package/dist/multi-agent/dashboard-agent-persona.js.map +1 -1
- package/dist/multi-agent/memory-agent-persona.d.ts +1 -1
- package/dist/multi-agent/memory-agent-persona.d.ts.map +1 -1
- package/dist/multi-agent/memory-agent-persona.js +1 -1
- package/dist/multi-agent/runtime-process.d.ts +3 -0
- package/dist/multi-agent/runtime-process.d.ts.map +1 -1
- package/dist/multi-agent/runtime-process.js +10 -2
- package/dist/multi-agent/runtime-process.js.map +1 -1
- package/dist/multi-agent/types.d.ts +12 -2
- package/dist/multi-agent/types.d.ts.map +1 -1
- package/dist/multi-agent/types.js.map +1 -1
- package/dist/multi-agent/wiki-agent-persona.d.ts +2 -2
- package/dist/multi-agent/wiki-agent-persona.js +2 -2
- package/dist/multi-agent/workflow-types.d.ts +1 -1
- package/dist/multi-agent/workflow-types.d.ts.map +1 -1
- package/dist/onboarding/complete-autonomous-prompt.d.ts +1 -1
- package/dist/onboarding/complete-autonomous-prompt.d.ts.map +1 -1
- package/dist/onboarding/complete-autonomous-prompt.js +9 -9
- package/dist/operator/board-slot-instructions.d.ts +1 -1
- package/dist/operator/board-slot-instructions.js +2 -2
- package/dist/operator/board-slot-instructions.js.map +1 -1
- package/dist/operator/briefs.d.ts +3 -0
- package/dist/operator/briefs.d.ts.map +1 -1
- package/dist/operator/briefs.js +64 -3
- package/dist/operator/briefs.js.map +1 -1
- package/dist/operator/console-brief.d.ts +6 -3
- package/dist/operator/console-brief.d.ts.map +1 -1
- package/dist/operator/console-brief.js +39 -9
- package/dist/operator/console-brief.js.map +1 -1
- package/dist/operator/external-lifecycle-candidates.d.ts +56 -0
- package/dist/operator/external-lifecycle-candidates.d.ts.map +1 -0
- package/dist/operator/external-lifecycle-candidates.js +415 -0
- package/dist/operator/external-lifecycle-candidates.js.map +1 -0
- package/dist/operator/external-lifecycle.d.ts +82 -0
- package/dist/operator/external-lifecycle.d.ts.map +1 -0
- package/dist/operator/external-lifecycle.js +27 -0
- package/dist/operator/external-lifecycle.js.map +1 -0
- package/dist/operator/file-coordination.d.ts +7 -0
- package/dist/operator/file-coordination.d.ts.map +1 -0
- package/dist/operator/file-coordination.js +84 -0
- package/dist/operator/file-coordination.js.map +1 -0
- package/dist/operator/operator-trigger-loop.d.ts +19 -4
- package/dist/operator/operator-trigger-loop.d.ts.map +1 -1
- package/dist/operator/operator-trigger-loop.js +151 -23
- package/dist/operator/operator-trigger-loop.js.map +1 -1
- package/dist/operator/pending-report-store.d.ts +62 -3
- package/dist/operator/pending-report-store.d.ts.map +1 -1
- package/dist/operator/pending-report-store.js +297 -40
- package/dist/operator/pending-report-store.js.map +1 -1
- package/dist/operator/report-carry-delivery.d.ts +35 -0
- package/dist/operator/report-carry-delivery.d.ts.map +1 -0
- package/dist/operator/report-carry-delivery.js +55 -0
- package/dist/operator/report-carry-delivery.js.map +1 -0
- package/dist/operator/report-carry.d.ts +50 -30
- package/dist/operator/report-carry.d.ts.map +1 -1
- package/dist/operator/report-carry.js +323 -71
- package/dist/operator/report-carry.js.map +1 -1
- package/dist/operator/situation-report.d.ts +30 -11
- package/dist/operator/situation-report.d.ts.map +1 -1
- package/dist/operator/situation-report.js +79 -35
- package/dist/operator/situation-report.js.map +1 -1
- package/dist/operator/task-ledger.d.ts +101 -0
- package/dist/operator/task-ledger.d.ts.map +1 -1
- package/dist/operator/task-ledger.js +599 -124
- package/dist/operator/task-ledger.js.map +1 -1
- package/dist/operator/temporal-runtime.d.ts.map +1 -1
- package/dist/operator/temporal-runtime.js +3 -1
- package/dist/operator/temporal-runtime.js.map +1 -1
- package/dist/operator/temporal-worker.js +1 -1
- package/dist/operator/trigger-author.d.ts +13 -1
- package/dist/operator/trigger-author.d.ts.map +1 -1
- package/dist/operator/trigger-author.js +35 -0
- package/dist/operator/trigger-author.js.map +1 -1
- package/dist/operator/worker-run.d.ts +1 -1
- package/dist/operator/worker-run.d.ts.map +1 -1
- package/dist/operator/worker-run.js +27 -9
- package/dist/operator/worker-run.js.map +1 -1
- package/dist/operator/workorder-consumer.d.ts +12 -1
- package/dist/operator/workorder-consumer.d.ts.map +1 -1
- package/dist/operator/workorder-consumer.js +125 -6
- package/dist/operator/workorder-consumer.js.map +1 -1
- package/dist/operator/workorder-hooks.d.ts +7 -2
- package/dist/operator/workorder-hooks.d.ts.map +1 -1
- package/dist/operator/workorder-hooks.js +22 -0
- package/dist/operator/workorder-hooks.js.map +1 -1
- package/dist/operator/workorder-publishers.d.ts +9 -2
- package/dist/operator/workorder-publishers.d.ts.map +1 -1
- package/dist/operator/workorder-publishers.js +167 -5
- package/dist/operator/workorder-publishers.js.map +1 -1
- package/dist/scheduler/cron-worker.d.ts +8 -4
- package/dist/scheduler/cron-worker.d.ts.map +1 -1
- package/dist/scheduler/cron-worker.js +56 -39
- package/dist/scheduler/cron-worker.js.map +1 -1
- package/dist/setup/setup-actions.d.ts +26 -0
- package/dist/setup/setup-actions.d.ts.map +1 -0
- package/dist/setup/setup-actions.js +371 -0
- package/dist/setup/setup-actions.js.map +1 -0
- package/dist/setup/setup-prompt.d.ts +1 -1
- package/dist/setup/setup-prompt.d.ts.map +1 -1
- package/dist/setup/setup-prompt.js +28 -33
- package/dist/setup/setup-prompt.js.map +1 -1
- package/dist/setup/setup-server.d.ts +6 -1
- package/dist/setup/setup-server.d.ts.map +1 -1
- package/dist/setup/setup-server.js +55 -9
- package/dist/setup/setup-server.js.map +1 -1
- package/dist/setup/setup-websocket.d.ts +14 -2
- package/dist/setup/setup-websocket.d.ts.map +1 -1
- package/dist/setup/setup-websocket.js +121 -37
- package/dist/setup/setup-websocket.js.map +1 -1
- package/package.json +2 -2
- package/public/setup.html +9 -6
- package/public/viewer/js/modules/agents.js +5 -4
- package/public/viewer/js/modules/settings.js +34 -10
- package/public/viewer/src/modules/agents.ts +5 -4
- package/public/viewer/src/modules/settings.ts +49 -11
- package/public/viewer/src/utils/api.ts +3 -2
- package/scripts/generate-gateway-tools.ts +13 -3
|
@@ -23,9 +23,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
23
23
|
exports.TaskLedger = exports.WORKORDER_CHANNEL_PREFIX = exports.TEMPORAL_WORKORDER_MAX_ATTEMPTS = exports.WORKORDER_KINDS = exports.TASK_KINDS = exports.TASK_PRIORITIES = exports.TASK_STATUSES = void 0;
|
|
24
24
|
const node_crypto_1 = require("node:crypto");
|
|
25
25
|
const operator_task_temporal_js_1 = require("../db/migrations/operator-task-temporal.js");
|
|
26
|
+
const operator_task_external_lifecycle_js_1 = require("../db/migrations/operator-task-external-lifecycle.js");
|
|
26
27
|
const effects_js_1 = require("../evidence/effects.js");
|
|
27
28
|
const temporal_effect_js_1 = require("./temporal-effect.js");
|
|
28
29
|
const task_temporal_js_1 = require("./task-temporal.js");
|
|
30
|
+
const external_lifecycle_candidates_js_1 = require("./external-lifecycle-candidates.js");
|
|
31
|
+
const workorder_publishers_js_1 = require("./workorder-publishers.js");
|
|
29
32
|
exports.TASK_STATUSES = [
|
|
30
33
|
'pending',
|
|
31
34
|
'in_progress',
|
|
@@ -258,6 +261,7 @@ class TaskLedger {
|
|
|
258
261
|
}
|
|
259
262
|
}
|
|
260
263
|
(0, operator_task_temporal_js_1.applyOperatorTaskTemporalMigration)(this.db);
|
|
264
|
+
(0, operator_task_external_lifecycle_js_1.applyOperatorTaskExternalLifecycleMigration)(this.db);
|
|
261
265
|
// Old-predicate unique index (no terminal exclusion) -> swap in place.
|
|
262
266
|
const idxRow = this.db
|
|
263
267
|
.prepare(`SELECT sql FROM sqlite_master WHERE type='index' AND name='idx_operator_tasks_source'`)
|
|
@@ -419,6 +423,432 @@ class TaskLedger {
|
|
|
419
423
|
const row = this.db.prepare(`SELECT * FROM operator_tasks WHERE id = ?`).get(id);
|
|
420
424
|
return row ? this.toRecord(row) : null;
|
|
421
425
|
}
|
|
426
|
+
/** Returns one active, explicitly receipted external identity for an owner task. */
|
|
427
|
+
getExternalBinding(taskId) {
|
|
428
|
+
const row = this.db
|
|
429
|
+
.prepare(`SELECT id, revision, task_id, connector, source_type, external_source_id,
|
|
430
|
+
last_observation_seq, created_by_attempt_id
|
|
431
|
+
FROM operator_external_task_bindings
|
|
432
|
+
WHERE task_id = ? AND active = 1`)
|
|
433
|
+
.get(taskId);
|
|
434
|
+
return row ? this.externalBindingFromRow(row) : null;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Host-only lookup material for immutable external-lifecycle candidates.
|
|
438
|
+
* The caller supplies exact event/source ids obtained from the connector index;
|
|
439
|
+
* no connector prose is read through this ledger surface.
|
|
440
|
+
*/
|
|
441
|
+
getExternalLifecycleCandidateSupport(eventIds, externalSourceIds) {
|
|
442
|
+
const uniqueEventIds = [...new Set(eventIds)];
|
|
443
|
+
const uniqueSourceIds = [...new Set(externalSourceIds)];
|
|
444
|
+
const directTaskIdsByEventId = new Map();
|
|
445
|
+
const effectTaskIdsByEventId = new Map();
|
|
446
|
+
const bindings = [];
|
|
447
|
+
if (uniqueEventIds.length > 0) {
|
|
448
|
+
const placeholders = uniqueEventIds.map(() => '?').join(',');
|
|
449
|
+
const directRows = this.db
|
|
450
|
+
.prepare(`SELECT source_event_id, id FROM operator_tasks
|
|
451
|
+
WHERE kind = 'owner' AND source_event_id IN (${placeholders})`)
|
|
452
|
+
.all(...uniqueEventIds);
|
|
453
|
+
for (const row of directRows) {
|
|
454
|
+
const ids = directTaskIdsByEventId.get(row.source_event_id) ?? [];
|
|
455
|
+
directTaskIdsByEventId.set(row.source_event_id, [...ids, row.id]);
|
|
456
|
+
}
|
|
457
|
+
const effectRows = this.db
|
|
458
|
+
.prepare(`SELECT DISTINCT json_each.value AS event_id, evidence_effects.target_id AS task_id
|
|
459
|
+
FROM evidence_effects, json_each(evidence_effects.source_event_ids_json)
|
|
460
|
+
WHERE evidence_effects.target_type = 'task'
|
|
461
|
+
AND json_each.value IN (${placeholders})`)
|
|
462
|
+
.all(...uniqueEventIds);
|
|
463
|
+
for (const row of effectRows) {
|
|
464
|
+
const taskId = Number(row.task_id);
|
|
465
|
+
if (!Number.isSafeInteger(taskId) || taskId < 1)
|
|
466
|
+
continue;
|
|
467
|
+
const ids = effectTaskIdsByEventId.get(row.event_id) ?? [];
|
|
468
|
+
effectTaskIdsByEventId.set(row.event_id, [...ids, taskId]);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (uniqueSourceIds.length > 0) {
|
|
472
|
+
const placeholders = uniqueSourceIds.map(() => '?').join(',');
|
|
473
|
+
const rows = this.db
|
|
474
|
+
.prepare(`SELECT id, revision, task_id, connector, source_type, external_source_id,
|
|
475
|
+
last_observation_seq
|
|
476
|
+
FROM operator_external_task_bindings
|
|
477
|
+
WHERE active = 1 AND connector = 'kagemusha' AND source_type = 'kanban_card'
|
|
478
|
+
AND external_source_id IN (${placeholders})`)
|
|
479
|
+
.all(...uniqueSourceIds);
|
|
480
|
+
for (const row of rows) {
|
|
481
|
+
bindings.push({
|
|
482
|
+
bindingId: row.id,
|
|
483
|
+
bindingRevision: row.revision,
|
|
484
|
+
taskId: row.task_id,
|
|
485
|
+
connector: row.connector,
|
|
486
|
+
sourceType: row.source_type,
|
|
487
|
+
externalSourceId: row.external_source_id,
|
|
488
|
+
lastObservationSeq: row.last_observation_seq,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const taskIds = new Set();
|
|
493
|
+
for (const ids of directTaskIdsByEventId.values())
|
|
494
|
+
ids.forEach((id) => taskIds.add(id));
|
|
495
|
+
for (const ids of effectTaskIdsByEventId.values())
|
|
496
|
+
ids.forEach((id) => taskIds.add(id));
|
|
497
|
+
for (const binding of bindings)
|
|
498
|
+
taskIds.add(binding.taskId);
|
|
499
|
+
const tasksById = new Map();
|
|
500
|
+
if (taskIds.size > 0) {
|
|
501
|
+
const ids = [...taskIds];
|
|
502
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
503
|
+
const rows = this.db
|
|
504
|
+
.prepare(`SELECT id, revision FROM operator_tasks WHERE kind = 'owner' AND id IN (${placeholders})`)
|
|
505
|
+
.all(...ids);
|
|
506
|
+
for (const row of rows)
|
|
507
|
+
tasksById.set(row.id, { taskId: row.id, revision: row.revision });
|
|
508
|
+
}
|
|
509
|
+
return { taskHints: { directTaskIdsByEventId, effectTaskIdsByEventId }, tasksById, bindings };
|
|
510
|
+
}
|
|
511
|
+
getReceiptedExternalCandidateIds(candidateIds) {
|
|
512
|
+
const ids = [...new Set(candidateIds)];
|
|
513
|
+
if (ids.length === 0)
|
|
514
|
+
return new Set();
|
|
515
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
516
|
+
const rows = this.db
|
|
517
|
+
.prepare(`SELECT candidate_id FROM operator_external_binding_receipts WHERE candidate_id IN (${placeholders})
|
|
518
|
+
UNION
|
|
519
|
+
SELECT candidate_id FROM operator_external_lifecycle_receipts WHERE candidate_id IN (${placeholders})`)
|
|
520
|
+
.all(...ids, ...ids);
|
|
521
|
+
return new Set(rows.map((row) => row.candidate_id));
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Recover a candidate only from the payload of the exact currently claimed
|
|
525
|
+
* board attempt. Revalidate the serialized payload: database bytes are not
|
|
526
|
+
* a capability merely because they originated in a previous host process.
|
|
527
|
+
*/
|
|
528
|
+
loadBoardCandidate(attemptId, candidateId, kind) {
|
|
529
|
+
const attempt = this.getWorkOrderById(attemptId);
|
|
530
|
+
if (!attempt || attempt.workKind !== 'board' || attempt.status !== 'in_progress') {
|
|
531
|
+
throw new Error(`external lifecycle candidate requires claimed board attempt ${attemptId}`);
|
|
532
|
+
}
|
|
533
|
+
const { attempts: _attempts, ...payload } = attempt.payload;
|
|
534
|
+
(0, workorder_publishers_js_1.validateWorkOrderPayload)('board', payload);
|
|
535
|
+
if (payload.mode !== 'reconcile' || !payload.candidates) {
|
|
536
|
+
throw new Error(`external lifecycle candidate ${candidateId} is absent from attempt ${attemptId}`);
|
|
537
|
+
}
|
|
538
|
+
const candidatePayload = payload.candidates;
|
|
539
|
+
const candidates = kind === 'binding'
|
|
540
|
+
? candidatePayload.bindingCandidates
|
|
541
|
+
: candidatePayload.lifecycleCandidates;
|
|
542
|
+
const candidate = candidates.find((value) => value.candidateId === candidateId);
|
|
543
|
+
if (!candidate) {
|
|
544
|
+
throw new Error(`external lifecycle ${kind} candidate ${candidateId} is absent from attempt ${attemptId}`);
|
|
545
|
+
}
|
|
546
|
+
return candidate;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Read the complete, immutable candidate set for a board attempt against
|
|
550
|
+
* its durable decision receipts. This deliberately works for stale claims
|
|
551
|
+
* as well as live in-progress attempts: recovery must never infer effects
|
|
552
|
+
* from a runner result when the database can answer exactly.
|
|
553
|
+
*/
|
|
554
|
+
inspectBoardCandidateAttempt(attemptId) {
|
|
555
|
+
const attempt = this.getWorkOrderById(attemptId);
|
|
556
|
+
if (!attempt || attempt.workKind !== 'board') {
|
|
557
|
+
throw new Error(`board candidate inspection requires board attempt ${attemptId}`);
|
|
558
|
+
}
|
|
559
|
+
const { attempts: _attempts, ...payload } = attempt.payload;
|
|
560
|
+
(0, workorder_publishers_js_1.validateWorkOrderPayload)('board', payload);
|
|
561
|
+
if (payload.mode !== 'reconcile' || !payload.candidates) {
|
|
562
|
+
return { disposition: 'none' };
|
|
563
|
+
}
|
|
564
|
+
const candidates = payload.candidates;
|
|
565
|
+
const all = [...candidates.bindingCandidates, ...candidates.lifecycleCandidates];
|
|
566
|
+
if (all.length === 0) {
|
|
567
|
+
return { disposition: 'none' };
|
|
568
|
+
}
|
|
569
|
+
const outcomes = [];
|
|
570
|
+
const missingCandidateIds = [];
|
|
571
|
+
for (const candidate of all) {
|
|
572
|
+
const receipt = this.getExternalCandidateReceipt(candidate.candidateId);
|
|
573
|
+
if (!receipt) {
|
|
574
|
+
missingCandidateIds.push(candidate.candidateId);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (receipt.kind !== candidate.kind) {
|
|
578
|
+
throw new Error(`external lifecycle candidate ${candidate.candidateId} has a receipt of the wrong kind`);
|
|
579
|
+
}
|
|
580
|
+
outcomes.push(receipt.outcome);
|
|
581
|
+
}
|
|
582
|
+
if (missingCandidateIds.length === 0) {
|
|
583
|
+
return { disposition: 'complete', outcomes };
|
|
584
|
+
}
|
|
585
|
+
if (missingCandidateIds.length === all.length) {
|
|
586
|
+
return { disposition: 'zero' };
|
|
587
|
+
}
|
|
588
|
+
return { disposition: 'partial', missingCandidateIds };
|
|
589
|
+
}
|
|
590
|
+
applyExternalBindingDecision(attemptId, input, origin) {
|
|
591
|
+
(0, external_lifecycle_candidates_js_1.validateExternalLifecycleDecision)('binding', input);
|
|
592
|
+
if (origin.workOrderAttemptId !== attemptId) {
|
|
593
|
+
throw new Error(`external binding decision requires trusted attempt origin ${attemptId}`);
|
|
594
|
+
}
|
|
595
|
+
let receipt = null;
|
|
596
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
597
|
+
try {
|
|
598
|
+
const candidate = this.loadBoardCandidate(attemptId, input.candidate_id, 'binding');
|
|
599
|
+
if (candidate.kind !== 'binding') {
|
|
600
|
+
throw new Error(`external lifecycle candidate ${input.candidate_id} has the wrong kind`);
|
|
601
|
+
}
|
|
602
|
+
if (input.expected_revision !== candidate.taskRevision) {
|
|
603
|
+
throw new Error(`external binding candidate ${candidate.candidateId} revision does not match its host snapshot`);
|
|
604
|
+
}
|
|
605
|
+
if (origin.causeEventIds?.length !== 1 || origin.causeEventIds[0] !== candidate.eventId) {
|
|
606
|
+
throw new Error(`external binding candidate ${candidate.candidateId} requires its exact host origin event`);
|
|
607
|
+
}
|
|
608
|
+
const existing = this.getExternalCandidateReceipt(candidate.candidateId);
|
|
609
|
+
if (existing) {
|
|
610
|
+
if (existing.kind !== 'binding') {
|
|
611
|
+
throw new Error(`external lifecycle candidate ${candidate.candidateId} already has another receipt kind`);
|
|
612
|
+
}
|
|
613
|
+
this.assertBindingReplayMatches(existing, candidate, input);
|
|
614
|
+
receipt = existing;
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
const task = this.getRowById(candidate.taskId);
|
|
618
|
+
if (!task || task.kind !== 'owner') {
|
|
619
|
+
throw new Error(`external binding candidate ${candidate.candidateId} references no owner task`);
|
|
620
|
+
}
|
|
621
|
+
const outcome = task.revision === candidate.taskRevision
|
|
622
|
+
? input.decision === 'bind'
|
|
623
|
+
? 'bound'
|
|
624
|
+
: 'declined'
|
|
625
|
+
: 'superseded';
|
|
626
|
+
let bindingId;
|
|
627
|
+
if (outcome === 'bound') {
|
|
628
|
+
const collision = this.db
|
|
629
|
+
.prepare(`SELECT id, task_id, external_source_id FROM operator_external_task_bindings
|
|
630
|
+
WHERE active = 1 AND (task_id = ? OR (connector = ? AND source_type = ? AND external_source_id = ?))`)
|
|
631
|
+
.get(candidate.taskId, candidate.connector, candidate.sourceType, candidate.externalSourceId);
|
|
632
|
+
if (collision) {
|
|
633
|
+
throw new Error(`external binding conflict for task ${candidate.taskId} or ${candidate.externalSourceId}`);
|
|
634
|
+
}
|
|
635
|
+
const now = this.now();
|
|
636
|
+
const inserted = this.db
|
|
637
|
+
.prepare(`INSERT INTO operator_external_task_bindings
|
|
638
|
+
(task_id, connector, source_type, external_source_id, last_observation_seq,
|
|
639
|
+
created_by_attempt_id, active, created_at, updated_at)
|
|
640
|
+
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`)
|
|
641
|
+
.run(candidate.taskId, candidate.connector, candidate.sourceType, candidate.externalSourceId, candidate.operatorObservationSeq, attemptId, now, now);
|
|
642
|
+
bindingId = Number(inserted.lastInsertRowid);
|
|
643
|
+
}
|
|
644
|
+
this.insertBindingReceipt(candidate, attemptId, input, outcome, bindingId, origin);
|
|
645
|
+
receipt = {
|
|
646
|
+
kind: 'binding',
|
|
647
|
+
candidateId: candidate.candidateId,
|
|
648
|
+
workOrderAttemptId: attemptId,
|
|
649
|
+
taskId: candidate.taskId,
|
|
650
|
+
outcome,
|
|
651
|
+
reason: input.reason,
|
|
652
|
+
...(bindingId === undefined ? {} : { bindingId }),
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
this.db.exec('COMMIT');
|
|
656
|
+
}
|
|
657
|
+
catch (error) {
|
|
658
|
+
this.db.exec('ROLLBACK');
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
return receipt;
|
|
662
|
+
}
|
|
663
|
+
applyExternalLifecycleDecision(attemptId, input, origin) {
|
|
664
|
+
(0, external_lifecycle_candidates_js_1.validateExternalLifecycleDecision)('lifecycle', input);
|
|
665
|
+
if (origin.workOrderAttemptId !== attemptId) {
|
|
666
|
+
throw new Error(`external lifecycle decision requires trusted attempt origin ${attemptId}`);
|
|
667
|
+
}
|
|
668
|
+
let receipt = null;
|
|
669
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
670
|
+
try {
|
|
671
|
+
const candidate = this.loadBoardCandidate(attemptId, input.candidate_id, 'lifecycle');
|
|
672
|
+
if (candidate.kind !== 'lifecycle') {
|
|
673
|
+
throw new Error(`external lifecycle candidate ${input.candidate_id} has the wrong kind`);
|
|
674
|
+
}
|
|
675
|
+
if (input.expected_revision !== candidate.taskRevision) {
|
|
676
|
+
throw new Error(`external lifecycle candidate ${candidate.candidateId} revision does not match its host snapshot`);
|
|
677
|
+
}
|
|
678
|
+
if (origin.causeEventIds?.length !== 1 || origin.causeEventIds[0] !== candidate.eventId) {
|
|
679
|
+
throw new Error(`external lifecycle candidate ${candidate.candidateId} requires its exact host origin event`);
|
|
680
|
+
}
|
|
681
|
+
const existing = this.getExternalCandidateReceipt(candidate.candidateId);
|
|
682
|
+
if (existing) {
|
|
683
|
+
if (existing.kind !== 'lifecycle') {
|
|
684
|
+
throw new Error(`external lifecycle candidate ${candidate.candidateId} already has another receipt kind`);
|
|
685
|
+
}
|
|
686
|
+
this.assertLifecycleReplayMatches(existing, candidate, input);
|
|
687
|
+
receipt = existing;
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
const task = this.getRowById(candidate.taskId);
|
|
691
|
+
const binding = this.getExternalBinding(candidate.taskId);
|
|
692
|
+
if (!task || task.kind !== 'owner' || !binding) {
|
|
693
|
+
throw new Error(`external lifecycle candidate ${candidate.candidateId} has no active binding`);
|
|
694
|
+
}
|
|
695
|
+
const superseded = binding.id !== candidate.bindingId ||
|
|
696
|
+
binding.revision !== candidate.bindingRevision ||
|
|
697
|
+
binding.connector !== candidate.connector ||
|
|
698
|
+
binding.sourceType !== candidate.sourceType ||
|
|
699
|
+
binding.externalSourceId !== candidate.externalSourceId ||
|
|
700
|
+
binding.createdByAttemptId >= attemptId ||
|
|
701
|
+
task.revision !== candidate.taskRevision ||
|
|
702
|
+
candidate.operatorObservationSeq <= binding.lastObservationSeq;
|
|
703
|
+
const taskRevisionBefore = task.revision;
|
|
704
|
+
let taskRevisionAfter = task.revision;
|
|
705
|
+
let outcome;
|
|
706
|
+
if (superseded) {
|
|
707
|
+
outcome = 'superseded';
|
|
708
|
+
}
|
|
709
|
+
else if (input.decision === 'apply') {
|
|
710
|
+
const transitioned = this.transitionTaskInTransaction(candidate.taskId, { status: candidate.proposedStatus, latest_event: candidate.evidenceSummary }, origin, true);
|
|
711
|
+
taskRevisionAfter = transitioned.revision;
|
|
712
|
+
outcome = 'applied';
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
outcome = 'retained';
|
|
716
|
+
}
|
|
717
|
+
if (outcome !== 'superseded') {
|
|
718
|
+
const updated = this.db
|
|
719
|
+
.prepare(`UPDATE operator_external_task_bindings
|
|
720
|
+
SET last_observation_seq = ?, revision = revision + 1, updated_at = ?
|
|
721
|
+
WHERE id = ? AND revision = ? AND last_observation_seq < ?`)
|
|
722
|
+
.run(candidate.operatorObservationSeq, this.now(), candidate.bindingId, candidate.bindingRevision, candidate.operatorObservationSeq);
|
|
723
|
+
if (updated.changes !== 1) {
|
|
724
|
+
throw new Error(`external lifecycle candidate ${candidate.candidateId} binding watermark changed`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
this.insertLifecycleReceipt(candidate, attemptId, input, outcome, taskRevisionBefore, taskRevisionAfter, origin);
|
|
728
|
+
receipt = {
|
|
729
|
+
kind: 'lifecycle',
|
|
730
|
+
candidateId: candidate.candidateId,
|
|
731
|
+
workOrderAttemptId: attemptId,
|
|
732
|
+
taskId: candidate.taskId,
|
|
733
|
+
outcome,
|
|
734
|
+
reason: input.reason,
|
|
735
|
+
taskRevisionBefore,
|
|
736
|
+
taskRevisionAfter,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
this.db.exec('COMMIT');
|
|
740
|
+
}
|
|
741
|
+
catch (error) {
|
|
742
|
+
this.db.exec('ROLLBACK');
|
|
743
|
+
throw error;
|
|
744
|
+
}
|
|
745
|
+
return receipt;
|
|
746
|
+
}
|
|
747
|
+
getExternalCandidateReceipt(candidateId) {
|
|
748
|
+
const binding = this.db
|
|
749
|
+
.prepare(`SELECT candidate_id, workorder_attempt_id, task_id, outcome, reason, binding_id
|
|
750
|
+
FROM operator_external_binding_receipts WHERE candidate_id = ?`)
|
|
751
|
+
.get(candidateId);
|
|
752
|
+
const lifecycle = this.db
|
|
753
|
+
.prepare(`SELECT candidate_id, workorder_attempt_id, task_id, outcome, reason,
|
|
754
|
+
task_revision_before, task_revision_after
|
|
755
|
+
FROM operator_external_lifecycle_receipts WHERE candidate_id = ?`)
|
|
756
|
+
.get(candidateId);
|
|
757
|
+
if (binding && lifecycle) {
|
|
758
|
+
throw new Error(`external lifecycle candidate ${candidateId} has duplicate global receipts`);
|
|
759
|
+
}
|
|
760
|
+
const receiptKind = binding ? 'binding' : lifecycle ? 'lifecycle' : null;
|
|
761
|
+
const identity = this.db
|
|
762
|
+
.prepare(`SELECT receipt_kind FROM operator_external_receipt_identities WHERE candidate_id = ?`)
|
|
763
|
+
.get(candidateId);
|
|
764
|
+
if (receiptKind === null) {
|
|
765
|
+
if (identity) {
|
|
766
|
+
throw new Error(`external lifecycle candidate ${candidateId} has an orphan receipt identity`);
|
|
767
|
+
}
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
if (!identity || identity.receipt_kind !== receiptKind) {
|
|
771
|
+
throw new Error(`external lifecycle candidate ${candidateId} has inconsistent global receipt identity`);
|
|
772
|
+
}
|
|
773
|
+
if (binding) {
|
|
774
|
+
return {
|
|
775
|
+
kind: 'binding',
|
|
776
|
+
candidateId: binding.candidate_id,
|
|
777
|
+
workOrderAttemptId: binding.workorder_attempt_id,
|
|
778
|
+
taskId: binding.task_id,
|
|
779
|
+
outcome: binding.outcome,
|
|
780
|
+
reason: binding.reason,
|
|
781
|
+
...(binding.binding_id === null ? {} : { bindingId: binding.binding_id }),
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
return lifecycle
|
|
785
|
+
? {
|
|
786
|
+
kind: 'lifecycle',
|
|
787
|
+
candidateId: lifecycle.candidate_id,
|
|
788
|
+
workOrderAttemptId: lifecycle.workorder_attempt_id,
|
|
789
|
+
taskId: lifecycle.task_id,
|
|
790
|
+
outcome: lifecycle.outcome,
|
|
791
|
+
reason: lifecycle.reason,
|
|
792
|
+
taskRevisionBefore: lifecycle.task_revision_before,
|
|
793
|
+
taskRevisionAfter: lifecycle.task_revision_after,
|
|
794
|
+
}
|
|
795
|
+
: null;
|
|
796
|
+
}
|
|
797
|
+
externalBindingFromRow(row) {
|
|
798
|
+
return {
|
|
799
|
+
id: row.id,
|
|
800
|
+
revision: row.revision,
|
|
801
|
+
taskId: row.task_id,
|
|
802
|
+
connector: row.connector,
|
|
803
|
+
sourceType: row.source_type,
|
|
804
|
+
externalSourceId: row.external_source_id,
|
|
805
|
+
lastObservationSeq: row.last_observation_seq,
|
|
806
|
+
createdByAttemptId: row.created_by_attempt_id,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
assertBindingReplayMatches(receipt, candidate, input) {
|
|
810
|
+
const stored = this.db
|
|
811
|
+
.prepare(`SELECT decision FROM operator_external_binding_receipts WHERE candidate_id = ?`)
|
|
812
|
+
.get(candidate.candidateId);
|
|
813
|
+
if (!stored ||
|
|
814
|
+
stored.decision !== input.decision ||
|
|
815
|
+
receipt.taskId !== candidate.taskId ||
|
|
816
|
+
receipt.reason !== input.reason) {
|
|
817
|
+
throw new Error(`external binding receipt ${candidate.candidateId} conflicts with replay decision`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
insertBindingReceipt(candidate, attemptId, input, outcome, bindingId, origin) {
|
|
821
|
+
this.db
|
|
822
|
+
.prepare(`INSERT INTO operator_external_binding_receipts
|
|
823
|
+
(candidate_id, decision, workorder_attempt_id, task_id, event_id, connector, source_type,
|
|
824
|
+
external_source_id, channel_partition, content_sha256, source_timestamp_ms,
|
|
825
|
+
operator_ingest_seq, operator_observation_seq, task_revision, outcome, reason, binding_id,
|
|
826
|
+
origin_run_id, origin_cause_event_ids, created_at)
|
|
827
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
828
|
+
.run(candidate.candidateId, input.decision, attemptId, candidate.taskId, candidate.eventId, candidate.connector, candidate.sourceType, candidate.externalSourceId, candidate.channelPartition, candidate.contentSha256, candidate.sourceTimestampMs, candidate.operatorIngestSeq, candidate.operatorObservationSeq, candidate.taskRevision, outcome, input.reason, bindingId ?? null, origin.runId ?? null, JSON.stringify(origin.causeEventIds ?? []), this.now());
|
|
829
|
+
}
|
|
830
|
+
assertLifecycleReplayMatches(receipt, candidate, input) {
|
|
831
|
+
const stored = this.db
|
|
832
|
+
.prepare(`SELECT decision FROM operator_external_lifecycle_receipts WHERE candidate_id = ?`)
|
|
833
|
+
.get(candidate.candidateId);
|
|
834
|
+
if (!stored ||
|
|
835
|
+
stored.decision !== input.decision ||
|
|
836
|
+
receipt.taskId !== candidate.taskId ||
|
|
837
|
+
receipt.reason !== input.reason) {
|
|
838
|
+
throw new Error(`external lifecycle receipt ${candidate.candidateId} conflicts with replay decision`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
insertLifecycleReceipt(candidate, attemptId, input, outcome, taskRevisionBefore, taskRevisionAfter, origin) {
|
|
842
|
+
this.db
|
|
843
|
+
.prepare(`INSERT INTO operator_external_lifecycle_receipts
|
|
844
|
+
(candidate_id, decision, workorder_attempt_id, task_id, event_id, connector, source_type,
|
|
845
|
+
external_source_id, channel_partition, content_sha256, source_timestamp_ms,
|
|
846
|
+
operator_ingest_seq, operator_observation_seq, binding_id, binding_revision,
|
|
847
|
+
task_revision_before, task_revision_after, outcome, reason, origin_run_id,
|
|
848
|
+
origin_cause_event_ids, created_at)
|
|
849
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
850
|
+
.run(candidate.candidateId, input.decision, attemptId, candidate.taskId, candidate.eventId, candidate.connector, candidate.sourceType, candidate.externalSourceId, candidate.channelPartition, candidate.contentSha256, candidate.sourceTimestampMs, candidate.operatorIngestSeq, candidate.operatorObservationSeq, candidate.bindingId, candidate.bindingRevision, taskRevisionBefore, taskRevisionAfter, outcome, input.reason, origin.runId ?? null, JSON.stringify(origin.causeEventIds ?? []), this.now());
|
|
851
|
+
}
|
|
422
852
|
/**
|
|
423
853
|
* Create a task. Idempotent under at-least-once delivery: a duplicate
|
|
424
854
|
* (source_channel, source_event_id) UPSERTS - the existing row gets the new
|
|
@@ -585,6 +1015,37 @@ class TaskLedger {
|
|
|
585
1015
|
(0, effects_js_1.recordUnattributedChange)(this.db, common, input.origin.causeKind ?? 'owner_message');
|
|
586
1016
|
}
|
|
587
1017
|
update(id, patch, origin = {}) {
|
|
1018
|
+
this.validateTaskUpdatePatch(patch);
|
|
1019
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
1020
|
+
try {
|
|
1021
|
+
const task = this.transitionTaskInTransaction(id, patch, origin);
|
|
1022
|
+
this.db.exec('COMMIT');
|
|
1023
|
+
return task;
|
|
1024
|
+
}
|
|
1025
|
+
catch (error) {
|
|
1026
|
+
this.db.exec('ROLLBACK');
|
|
1027
|
+
throw error;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* The one mutation primitive for owner-task state. Callers own the outer
|
|
1032
|
+
* transaction so lifecycle receipts, effects, generation ownership, and the
|
|
1033
|
+
* row revision can commit or roll back as one unit.
|
|
1034
|
+
*/
|
|
1035
|
+
transitionTaskInTransaction(id, patch, origin, skipCandidateGuard = false) {
|
|
1036
|
+
this.validateTaskUpdatePatch(patch);
|
|
1037
|
+
if (!skipCandidateGuard) {
|
|
1038
|
+
this.assertCandidateTaskMutationAllowed(id, patch, origin);
|
|
1039
|
+
}
|
|
1040
|
+
const existing = this.db.prepare('SELECT * FROM operator_tasks WHERE id = ?').get(id);
|
|
1041
|
+
if (!existing)
|
|
1042
|
+
throw new Error(`task_update: no task with id ${id}`);
|
|
1043
|
+
if (existing.kind === 'system') {
|
|
1044
|
+
throw new Error(`task_update: task ${id} is a system workorder row (host-managed)`);
|
|
1045
|
+
}
|
|
1046
|
+
return this.transitionOwnerTaskRowInTransaction(id, existing, patch, origin);
|
|
1047
|
+
}
|
|
1048
|
+
validateTaskUpdatePatch(patch) {
|
|
588
1049
|
if (patch.status === 'failed') {
|
|
589
1050
|
throw new Error(`task_update: 'failed' is a system-only status`);
|
|
590
1051
|
}
|
|
@@ -598,138 +1059,152 @@ class TaskLedger {
|
|
|
598
1059
|
if (patch.title !== undefined && patch.title.trim() === '') {
|
|
599
1060
|
throw new Error('task title must be a non-empty string');
|
|
600
1061
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
1062
|
+
}
|
|
1063
|
+
transitionOwnerTaskRowInTransaction(id, existing, patch, origin) {
|
|
1064
|
+
const next = {
|
|
1065
|
+
title: existing.title,
|
|
1066
|
+
status: existing.status,
|
|
1067
|
+
priority: existing.priority,
|
|
1068
|
+
assignee: existing.assignee,
|
|
1069
|
+
deadline: existing.deadline,
|
|
1070
|
+
due_at: existing.due_at,
|
|
1071
|
+
deadline_offset_minutes: existing.deadline_offset_minutes,
|
|
1072
|
+
latest_event: existing.latest_event,
|
|
1073
|
+
confirmed: existing.confirmed,
|
|
1074
|
+
temporal_epoch: existing.temporal_epoch,
|
|
1075
|
+
temporal_reconciled_occurrence_key: existing.temporal_reconciled_occurrence_key,
|
|
1076
|
+
last_temporal_checked_at: existing.last_temporal_checked_at,
|
|
1077
|
+
next_temporal_check_at: existing.next_temporal_check_at,
|
|
1078
|
+
last_temporal_attempt_id: existing.last_temporal_attempt_id,
|
|
1079
|
+
};
|
|
1080
|
+
if (patch.title !== undefined)
|
|
1081
|
+
next.title = patch.title.trim();
|
|
1082
|
+
if (patch.status !== undefined)
|
|
1083
|
+
next.status = patch.status;
|
|
1084
|
+
if (patch.priority !== undefined)
|
|
1085
|
+
next.priority = patch.priority;
|
|
1086
|
+
if (patch.assignee !== undefined)
|
|
1087
|
+
next.assignee = patch.assignee;
|
|
1088
|
+
if (patch.latest_event !== undefined)
|
|
1089
|
+
next.latest_event = patch.latest_event;
|
|
1090
|
+
if (patch.confirmed !== undefined)
|
|
1091
|
+
next.confirmed = patch.confirmed ? 1 : 0;
|
|
1092
|
+
const hasDueAt = Object.prototype.hasOwnProperty.call(patch, 'due_at');
|
|
1093
|
+
const hasDeadline = Object.prototype.hasOwnProperty.call(patch, 'deadline');
|
|
1094
|
+
if (hasDueAt && patch.due_at !== null && patch.due_at !== undefined) {
|
|
1095
|
+
const exactDue = (0, task_temporal_js_1.parseExactDueAt)(patch.due_at);
|
|
1096
|
+
if (hasDeadline && patch.deadline !== exactDue.deadline) {
|
|
1097
|
+
throw new Error('task_update: due_at and deadline conflict');
|
|
608
1098
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
confirmed: existing.confirmed,
|
|
619
|
-
temporal_epoch: existing.temporal_epoch,
|
|
620
|
-
temporal_reconciled_occurrence_key: existing.temporal_reconciled_occurrence_key,
|
|
621
|
-
last_temporal_checked_at: existing.last_temporal_checked_at,
|
|
622
|
-
next_temporal_check_at: existing.next_temporal_check_at,
|
|
623
|
-
last_temporal_attempt_id: existing.last_temporal_attempt_id,
|
|
624
|
-
};
|
|
625
|
-
if (patch.title !== undefined)
|
|
626
|
-
next.title = patch.title.trim();
|
|
627
|
-
if (patch.status !== undefined)
|
|
628
|
-
next.status = patch.status;
|
|
629
|
-
if (patch.priority !== undefined)
|
|
630
|
-
next.priority = patch.priority;
|
|
631
|
-
if (patch.assignee !== undefined)
|
|
632
|
-
next.assignee = patch.assignee;
|
|
633
|
-
if (patch.latest_event !== undefined)
|
|
634
|
-
next.latest_event = patch.latest_event;
|
|
635
|
-
if (patch.confirmed !== undefined)
|
|
636
|
-
next.confirmed = patch.confirmed ? 1 : 0;
|
|
637
|
-
const hasDueAt = Object.prototype.hasOwnProperty.call(patch, 'due_at');
|
|
638
|
-
const hasDeadline = Object.prototype.hasOwnProperty.call(patch, 'deadline');
|
|
639
|
-
if (hasDueAt && patch.due_at !== null && patch.due_at !== undefined) {
|
|
640
|
-
const exactDue = (0, task_temporal_js_1.parseExactDueAt)(patch.due_at);
|
|
641
|
-
if (hasDeadline && patch.deadline !== exactDue.deadline) {
|
|
642
|
-
throw new Error('task_update: due_at and deadline conflict');
|
|
643
|
-
}
|
|
644
|
-
next.due_at = exactDue.dueAt;
|
|
645
|
-
next.deadline = exactDue.deadline;
|
|
646
|
-
next.deadline_offset_minutes = exactDue.offsetMinutes;
|
|
647
|
-
}
|
|
648
|
-
else if (hasDeadline) {
|
|
649
|
-
next.deadline = patch.deadline ?? null;
|
|
650
|
-
next.due_at = null;
|
|
651
|
-
if (patch.deadline === null) {
|
|
652
|
-
next.deadline_offset_minutes = null;
|
|
653
|
-
}
|
|
1099
|
+
next.due_at = exactDue.dueAt;
|
|
1100
|
+
next.deadline = exactDue.deadline;
|
|
1101
|
+
next.deadline_offset_minutes = exactDue.offsetMinutes;
|
|
1102
|
+
}
|
|
1103
|
+
else if (hasDeadline) {
|
|
1104
|
+
next.deadline = patch.deadline ?? null;
|
|
1105
|
+
next.due_at = null;
|
|
1106
|
+
if (patch.deadline === null) {
|
|
1107
|
+
next.deadline_offset_minutes = null;
|
|
654
1108
|
}
|
|
655
|
-
|
|
656
|
-
|
|
1109
|
+
}
|
|
1110
|
+
else if (hasDueAt) {
|
|
1111
|
+
next.due_at = null;
|
|
1112
|
+
}
|
|
1113
|
+
const temporalChanged = next.due_at !== existing.due_at || next.deadline !== existing.deadline;
|
|
1114
|
+
const terminalToOpen = (existing.status === 'done' || existing.status === 'cancelled') &&
|
|
1115
|
+
next.status !== 'done' &&
|
|
1116
|
+
next.status !== 'cancelled';
|
|
1117
|
+
const openToTerminal = existing.status !== 'done' &&
|
|
1118
|
+
existing.status !== 'cancelled' &&
|
|
1119
|
+
(next.status === 'done' || next.status === 'cancelled');
|
|
1120
|
+
if (temporalChanged || terminalToOpen) {
|
|
1121
|
+
next.temporal_epoch = existing.temporal_epoch + 1;
|
|
1122
|
+
next.temporal_reconciled_occurrence_key = null;
|
|
1123
|
+
next.last_temporal_checked_at = null;
|
|
1124
|
+
next.next_temporal_check_at = null;
|
|
1125
|
+
next.last_temporal_attempt_id = null;
|
|
1126
|
+
}
|
|
1127
|
+
const persistedColumns = [
|
|
1128
|
+
'title',
|
|
1129
|
+
'status',
|
|
1130
|
+
'priority',
|
|
1131
|
+
'assignee',
|
|
1132
|
+
'deadline',
|
|
1133
|
+
'due_at',
|
|
1134
|
+
'deadline_offset_minutes',
|
|
1135
|
+
'latest_event',
|
|
1136
|
+
'confirmed',
|
|
1137
|
+
'temporal_epoch',
|
|
1138
|
+
'temporal_reconciled_occurrence_key',
|
|
1139
|
+
'last_temporal_checked_at',
|
|
1140
|
+
'next_temporal_check_at',
|
|
1141
|
+
'last_temporal_attempt_id',
|
|
1142
|
+
];
|
|
1143
|
+
const changedColumns = persistedColumns.filter((column) => next[column] !== existing[column]);
|
|
1144
|
+
if (changedColumns.length === 0) {
|
|
1145
|
+
return this.toRecord(existing);
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
const nextRevision = existing.revision + 1;
|
|
1149
|
+
const sets = changedColumns.map((column) => `${column} = ?`);
|
|
1150
|
+
const values = changedColumns.map((column) => next[column]);
|
|
1151
|
+
const updatedAt = this.now();
|
|
1152
|
+
sets.push('revision = ?', 'updated_at = ?');
|
|
1153
|
+
values.push(nextRevision, updatedAt);
|
|
1154
|
+
const updated = this.db
|
|
1155
|
+
.prepare(`UPDATE operator_tasks SET ${sets.join(', ')} WHERE id = ? AND revision = ?`)
|
|
1156
|
+
.run(...values, id, existing.revision);
|
|
1157
|
+
if (updated.changes !== 1) {
|
|
1158
|
+
throw new Error(`task_update: task ${id} revision changed during transition`);
|
|
657
1159
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
1160
|
+
this.recordTaskChange({
|
|
1161
|
+
kind: 'task_update',
|
|
1162
|
+
taskId: id,
|
|
1163
|
+
origin,
|
|
1164
|
+
channel: existing.source_channel,
|
|
1165
|
+
payload: {
|
|
1166
|
+
revision: nextRevision,
|
|
1167
|
+
changed: Object.fromEntries(changedColumns.map((column) => [column, next[column]])),
|
|
1168
|
+
},
|
|
1169
|
+
atMs: updatedAt,
|
|
1170
|
+
});
|
|
665
1171
|
if (temporalChanged || terminalToOpen) {
|
|
666
|
-
|
|
667
|
-
next.temporal_reconciled_occurrence_key = null;
|
|
668
|
-
next.last_temporal_checked_at = null;
|
|
669
|
-
next.next_temporal_check_at = null;
|
|
670
|
-
next.last_temporal_attempt_id = null;
|
|
1172
|
+
this.supersedeTemporalGenerationsInTransaction(id, Number(next.temporal_epoch));
|
|
671
1173
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
'status',
|
|
675
|
-
'priority',
|
|
676
|
-
'assignee',
|
|
677
|
-
'deadline',
|
|
678
|
-
'due_at',
|
|
679
|
-
'deadline_offset_minutes',
|
|
680
|
-
'latest_event',
|
|
681
|
-
'confirmed',
|
|
682
|
-
'temporal_epoch',
|
|
683
|
-
'temporal_reconciled_occurrence_key',
|
|
684
|
-
'last_temporal_checked_at',
|
|
685
|
-
'next_temporal_check_at',
|
|
686
|
-
'last_temporal_attempt_id',
|
|
687
|
-
];
|
|
688
|
-
const changedColumns = persistedColumns.filter((column) => next[column] !== existing[column]);
|
|
689
|
-
if (changedColumns.length === 0) {
|
|
690
|
-
// Nothing moved, so nothing is recorded. This is the whole point of the ledger:
|
|
691
|
-
// a call that ran is not a change, and the two must not be countable as one.
|
|
692
|
-
//
|
|
693
|
-
// The read-back happens after the catch, not here: a throw between COMMIT and
|
|
694
|
-
// return would send this into the catch and issue ROLLBACK with no open
|
|
695
|
-
// transaction, replacing the real error with a confusing one.
|
|
696
|
-
this.db.exec('COMMIT');
|
|
697
|
-
}
|
|
698
|
-
else {
|
|
699
|
-
const nextRevision = existing.revision + 1;
|
|
700
|
-
const sets = changedColumns.map((column) => `${column} = ?`);
|
|
701
|
-
const values = changedColumns.map((column) => next[column]);
|
|
702
|
-
const updatedAt = this.now();
|
|
703
|
-
sets.push('revision = ?', 'updated_at = ?');
|
|
704
|
-
values.push(nextRevision, updatedAt);
|
|
705
|
-
this.db
|
|
706
|
-
.prepare(`UPDATE operator_tasks SET ${sets.join(', ')} WHERE id = ?`)
|
|
707
|
-
.run(...values, id);
|
|
708
|
-
this.recordTaskChange({
|
|
709
|
-
kind: 'task_update',
|
|
710
|
-
taskId: id,
|
|
711
|
-
origin,
|
|
712
|
-
channel: existing.source_channel,
|
|
713
|
-
payload: {
|
|
714
|
-
revision: nextRevision,
|
|
715
|
-
changed: Object.fromEntries(changedColumns.map((column) => [column, next[column]])),
|
|
716
|
-
},
|
|
717
|
-
atMs: updatedAt,
|
|
718
|
-
});
|
|
719
|
-
if (temporalChanged || terminalToOpen) {
|
|
720
|
-
this.supersedeTemporalGenerationsInTransaction(id, Number(next.temporal_epoch));
|
|
721
|
-
}
|
|
722
|
-
else if (openToTerminal) {
|
|
723
|
-
this.supersedeAllActiveTemporalGenerationsInTransaction(id);
|
|
724
|
-
}
|
|
725
|
-
this.db.exec('COMMIT');
|
|
1174
|
+
else if (openToTerminal) {
|
|
1175
|
+
this.supersedeAllActiveTemporalGenerationsInTransaction(id);
|
|
726
1176
|
}
|
|
1177
|
+
const transitioned = this.getRowById(id);
|
|
1178
|
+
if (!transitioned)
|
|
1179
|
+
throw new Error(`task_update: task ${id} disappeared during transition`);
|
|
1180
|
+
return transitioned;
|
|
727
1181
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1182
|
+
}
|
|
1183
|
+
assertCandidateTaskMutationAllowed(taskId, patch, origin) {
|
|
1184
|
+
if (origin.workOrderAttemptId === undefined ||
|
|
1185
|
+
(!Object.prototype.hasOwnProperty.call(patch, 'status') &&
|
|
1186
|
+
!Object.prototype.hasOwnProperty.call(patch, 'latest_event'))) {
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
const attempt = this.getWorkOrderById(origin.workOrderAttemptId);
|
|
1190
|
+
// Unlike a lifecycle decision, this is a standing mutation guard. A late
|
|
1191
|
+
// direct or nested tool call can retain the host-issued attempt context
|
|
1192
|
+
// after its board workorder has reached a terminal status, and its durable
|
|
1193
|
+
// candidate payload must remain authoritative in that case. Decision
|
|
1194
|
+
// application still calls loadBoardCandidate(), which requires a claimed
|
|
1195
|
+
// in-progress attempt.
|
|
1196
|
+
if (!attempt || attempt.workKind !== 'board') {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const { attempts: _attempts, ...payload } = attempt.payload;
|
|
1200
|
+
(0, workorder_publishers_js_1.validateWorkOrderPayload)('board', payload);
|
|
1201
|
+
if (payload.mode !== 'reconcile' || !payload.candidates) {
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const candidates = payload.candidates;
|
|
1205
|
+
if ([...candidates.bindingCandidates, ...candidates.lifecycleCandidates].some((candidate) => candidate.taskId === taskId)) {
|
|
1206
|
+
throw new Error(`task_update: candidate-bound lifecycle task ${taskId} may change status/latest_event only through its receipted decision`);
|
|
731
1207
|
}
|
|
732
|
-
return this.getById(id);
|
|
733
1208
|
}
|
|
734
1209
|
supersedeTemporalGenerationsInTransaction(taskId, currentEpoch, excludeGenerationKey, updatedAt = this.now()) {
|
|
735
1210
|
const reason = 'task temporal occurrence superseded';
|