@jungjaehoon/mama-os 0.49.3 → 0.51.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.
Files changed (180) hide show
  1. package/dist/agent/agent-loop.d.ts +5 -1
  2. package/dist/agent/agent-loop.d.ts.map +1 -1
  3. package/dist/agent/agent-loop.js +22 -3
  4. package/dist/agent/agent-loop.js.map +1 -1
  5. package/dist/agent/code-act/host-bridge.d.ts.map +1 -1
  6. package/dist/agent/code-act/host-bridge.js +45 -10
  7. package/dist/agent/code-act/host-bridge.js.map +1 -1
  8. package/dist/agent/codex-app-server-process.d.ts.map +1 -1
  9. package/dist/agent/codex-app-server-process.js +37 -0
  10. package/dist/agent/codex-app-server-process.js.map +1 -1
  11. package/dist/agent/gateway-tool-executor.d.ts +19 -10
  12. package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
  13. package/dist/agent/gateway-tool-executor.js +685 -180
  14. package/dist/agent/gateway-tool-executor.js.map +1 -1
  15. package/dist/agent/gateway-tools.md +8 -7
  16. package/dist/agent/native-effect-observer.d.ts +19 -0
  17. package/dist/agent/native-effect-observer.d.ts.map +1 -0
  18. package/dist/agent/native-effect-observer.js +70 -0
  19. package/dist/agent/native-effect-observer.js.map +1 -0
  20. package/dist/agent/persistent-cli-process.d.ts.map +1 -1
  21. package/dist/agent/persistent-cli-process.js +6 -1
  22. package/dist/agent/persistent-cli-process.js.map +1 -1
  23. package/dist/agent/tool-registry.d.ts.map +1 -1
  24. package/dist/agent/tool-registry.js +17 -10
  25. package/dist/agent/tool-registry.js.map +1 -1
  26. package/dist/agent/types.d.ts +8 -2
  27. package/dist/agent/types.d.ts.map +1 -1
  28. package/dist/agent/types.js.map +1 -1
  29. package/dist/api/agent-raw-handler.d.ts +15 -1
  30. package/dist/api/agent-raw-handler.d.ts.map +1 -1
  31. package/dist/api/agent-raw-handler.js +14 -0
  32. package/dist/api/agent-raw-handler.js.map +1 -1
  33. package/dist/api/report-handler.d.ts +19 -3
  34. package/dist/api/report-handler.d.ts.map +1 -1
  35. package/dist/api/report-handler.js +123 -17
  36. package/dist/api/report-handler.js.map +1 -1
  37. package/dist/api/report-persistence.d.ts +3 -3
  38. package/dist/api/report-persistence.d.ts.map +1 -1
  39. package/dist/api/report-persistence.js +11 -21
  40. package/dist/api/report-persistence.js.map +1 -1
  41. package/dist/cli/commands/start.d.ts +4 -0
  42. package/dist/cli/commands/start.d.ts.map +1 -1
  43. package/dist/cli/commands/start.js +53 -162
  44. package/dist/cli/commands/start.js.map +1 -1
  45. package/dist/cli/runtime/agent-loop-init.d.ts.map +1 -1
  46. package/dist/cli/runtime/agent-loop-init.js +1 -0
  47. package/dist/cli/runtime/agent-loop-init.js.map +1 -1
  48. package/dist/cli/runtime/api-routes-init.d.ts +5 -19
  49. package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
  50. package/dist/cli/runtime/api-routes-init.js +7 -79
  51. package/dist/cli/runtime/api-routes-init.js.map +1 -1
  52. package/dist/concurrency/lane-manager.d.ts.map +1 -1
  53. package/dist/concurrency/lane-manager.js +3 -2
  54. package/dist/concurrency/lane-manager.js.map +1 -1
  55. package/dist/connectors/calendar/index.d.ts.map +1 -1
  56. package/dist/connectors/calendar/index.js +70 -37
  57. package/dist/connectors/calendar/index.js.map +1 -1
  58. package/dist/connectors/drive/index.d.ts.map +1 -1
  59. package/dist/connectors/drive/index.js +1 -0
  60. package/dist/connectors/drive/index.js.map +1 -1
  61. package/dist/connectors/framework/polling-scheduler.js +3 -3
  62. package/dist/connectors/framework/polling-scheduler.js.map +1 -1
  63. package/dist/connectors/framework/raw-store.d.ts +15 -1
  64. package/dist/connectors/framework/raw-store.d.ts.map +1 -1
  65. package/dist/connectors/framework/raw-store.js +128 -29
  66. package/dist/connectors/framework/raw-store.js.map +1 -1
  67. package/dist/connectors/framework/types.d.ts +9 -0
  68. package/dist/connectors/framework/types.d.ts.map +1 -1
  69. package/dist/connectors/trello/index.d.ts.map +1 -1
  70. package/dist/connectors/trello/index.js +1 -0
  71. package/dist/connectors/trello/index.js.map +1 -1
  72. package/dist/db/migrations/operator-owner-action-candidates.d.ts +19 -0
  73. package/dist/db/migrations/operator-owner-action-candidates.d.ts.map +1 -0
  74. package/dist/db/migrations/operator-owner-action-candidates.js +254 -0
  75. package/dist/db/migrations/operator-owner-action-candidates.js.map +1 -0
  76. package/dist/db/migrations/operator-task-external-lifecycle.d.ts +9 -0
  77. package/dist/db/migrations/operator-task-external-lifecycle.d.ts.map +1 -1
  78. package/dist/db/migrations/operator-task-external-lifecycle.js +52 -22
  79. package/dist/db/migrations/operator-task-external-lifecycle.js.map +1 -1
  80. package/dist/db/migrations/operator-task-review-lifecycle.d.ts.map +1 -1
  81. package/dist/db/migrations/operator-task-review-lifecycle.js +3 -0
  82. package/dist/db/migrations/operator-task-review-lifecycle.js.map +1 -1
  83. package/dist/db/migrations/owner-action-effects.d.ts +18 -0
  84. package/dist/db/migrations/owner-action-effects.d.ts.map +1 -0
  85. package/dist/db/migrations/owner-action-effects.js +111 -0
  86. package/dist/db/migrations/owner-action-effects.js.map +1 -0
  87. package/dist/db/migrations/raw-item-revisions.d.ts +4 -0
  88. package/dist/db/migrations/raw-item-revisions.d.ts.map +1 -0
  89. package/dist/db/migrations/raw-item-revisions.js +29 -0
  90. package/dist/db/migrations/raw-item-revisions.js.map +1 -0
  91. package/dist/gateways/message-router.d.ts.map +1 -1
  92. package/dist/gateways/message-router.js +14 -4
  93. package/dist/gateways/message-router.js.map +1 -1
  94. package/dist/gateways/telegram-response-presenter.d.ts +4 -0
  95. package/dist/gateways/telegram-response-presenter.d.ts.map +1 -1
  96. package/dist/gateways/telegram-response-presenter.js +46 -41
  97. package/dist/gateways/telegram-response-presenter.js.map +1 -1
  98. package/dist/gateways/telegram-text-entities.d.ts +99 -0
  99. package/dist/gateways/telegram-text-entities.d.ts.map +1 -0
  100. package/dist/gateways/telegram-text-entities.js +189 -0
  101. package/dist/gateways/telegram-text-entities.js.map +1 -0
  102. package/dist/gateways/telegram.d.ts +4 -0
  103. package/dist/gateways/telegram.d.ts.map +1 -1
  104. package/dist/gateways/telegram.js +52 -12
  105. package/dist/gateways/telegram.js.map +1 -1
  106. package/dist/gateways/types.d.ts +8 -0
  107. package/dist/gateways/types.d.ts.map +1 -1
  108. package/dist/operator/board-read-views.d.ts +9 -4
  109. package/dist/operator/board-read-views.d.ts.map +1 -1
  110. package/dist/operator/board-read-views.js +11 -0
  111. package/dist/operator/board-read-views.js.map +1 -1
  112. package/dist/operator/external-lifecycle-candidates.d.ts +15 -1
  113. package/dist/operator/external-lifecycle-candidates.d.ts.map +1 -1
  114. package/dist/operator/external-lifecycle-candidates.js +164 -0
  115. package/dist/operator/external-lifecycle-candidates.js.map +1 -1
  116. package/dist/operator/external-lifecycle-discovery.d.ts +61 -0
  117. package/dist/operator/external-lifecycle-discovery.d.ts.map +1 -0
  118. package/dist/operator/external-lifecycle-discovery.js +103 -0
  119. package/dist/operator/external-lifecycle-discovery.js.map +1 -0
  120. package/dist/operator/external-lifecycle.d.ts +23 -0
  121. package/dist/operator/external-lifecycle.d.ts.map +1 -1
  122. package/dist/operator/operator-trigger-loop.d.ts +4 -5
  123. package/dist/operator/operator-trigger-loop.d.ts.map +1 -1
  124. package/dist/operator/operator-trigger-loop.js +25 -44
  125. package/dist/operator/operator-trigger-loop.js.map +1 -1
  126. package/dist/operator/owner-action-effects.d.ts +101 -0
  127. package/dist/operator/owner-action-effects.d.ts.map +1 -0
  128. package/dist/operator/owner-action-effects.js +335 -0
  129. package/dist/operator/owner-action-effects.js.map +1 -0
  130. package/dist/operator/owner-event-inbox.d.ts +1 -0
  131. package/dist/operator/owner-event-inbox.d.ts.map +1 -1
  132. package/dist/operator/owner-event-inbox.js +6 -0
  133. package/dist/operator/owner-event-inbox.js.map +1 -1
  134. package/dist/operator/owner-event-loop.d.ts +3 -0
  135. package/dist/operator/owner-event-loop.d.ts.map +1 -1
  136. package/dist/operator/owner-event-loop.js +34 -0
  137. package/dist/operator/owner-event-loop.js.map +1 -1
  138. package/dist/operator/owner-event-outcome.d.ts.map +1 -1
  139. package/dist/operator/owner-event-outcome.js +6 -0
  140. package/dist/operator/owner-event-outcome.js.map +1 -1
  141. package/dist/operator/owner-event-policy.d.ts.map +1 -1
  142. package/dist/operator/owner-event-policy.js +2 -26
  143. package/dist/operator/owner-event-policy.js.map +1 -1
  144. package/dist/operator/owner-event-prompt.d.ts.map +1 -1
  145. package/dist/operator/owner-event-prompt.js +2 -5
  146. package/dist/operator/owner-event-prompt.js.map +1 -1
  147. package/dist/operator/pending-report-store.d.ts +2 -2
  148. package/dist/operator/pending-report-store.d.ts.map +1 -1
  149. package/dist/operator/pending-report-store.js +9 -4
  150. package/dist/operator/pending-report-store.js.map +1 -1
  151. package/dist/operator/report-run.d.ts +10 -11
  152. package/dist/operator/report-run.d.ts.map +1 -1
  153. package/dist/operator/report-run.js +8 -48
  154. package/dist/operator/report-run.js.map +1 -1
  155. package/dist/operator/task-ledger.d.ts +93 -14
  156. package/dist/operator/task-ledger.d.ts.map +1 -1
  157. package/dist/operator/task-ledger.js +430 -103
  158. package/dist/operator/task-ledger.js.map +1 -1
  159. package/dist/operator/temporal-reconcile.d.ts.map +1 -1
  160. package/dist/operator/temporal-reconcile.js +6 -2
  161. package/dist/operator/temporal-reconcile.js.map +1 -1
  162. package/dist/operator/workorder-consumer.d.ts +2 -0
  163. package/dist/operator/workorder-consumer.d.ts.map +1 -1
  164. package/dist/operator/workorder-consumer.js +20 -5
  165. package/dist/operator/workorder-consumer.js.map +1 -1
  166. package/dist/scheduler/heartbeat.d.ts.map +1 -1
  167. package/dist/scheduler/heartbeat.js +1 -0
  168. package/dist/scheduler/heartbeat.js.map +1 -1
  169. package/dist/sqlite.d.ts +1 -1
  170. package/dist/sqlite.d.ts.map +1 -1
  171. package/dist/sqlite.js +3 -2
  172. package/dist/sqlite.js.map +1 -1
  173. package/dist/wiki/wiki-read.d.ts +2 -1
  174. package/dist/wiki/wiki-read.d.ts.map +1 -1
  175. package/dist/wiki/wiki-read.js +13 -2
  176. package/dist/wiki/wiki-read.js.map +1 -1
  177. package/dist/wiki/wiki-turn-contract.js +1 -1
  178. package/dist/wiki/wiki-turn-contract.js.map +1 -1
  179. package/package.json +2 -2
  180. package/public/viewer/operator/operator.js +3 -1
@@ -86,7 +86,10 @@ const obsidian_cli_args_js_1 = require("./obsidian-cli-args.js");
86
86
  const wiki_read_js_1 = require("../wiki/wiki-read.js");
87
87
  const changes_projection_js_1 = require("../operator/changes-projection.js");
88
88
  const task_list_views_js_1 = require("../operator/task-list-views.js");
89
+ const temporal_reconcile_js_1 = require("../operator/temporal-reconcile.js");
89
90
  const board_read_views_js_1 = require("../operator/board-read-views.js");
91
+ const owner_action_effects_js_1 = require("../operator/owner-action-effects.js");
92
+ const external_lifecycle_discovery_js_1 = require("../operator/external-lifecycle-discovery.js");
90
93
  const read_js_1 = require("../evidence/read.js");
91
94
  const private_connector_policy_js_1 = require("../connectors/private-connector-policy.js");
92
95
  const member_candidate_store_js_1 = require("../gateways/member-candidate-store.js");
@@ -113,7 +116,7 @@ function bindTemporalContextPacketTask(context, task) {
113
116
  function temporalIdentifierRef(value) {
114
117
  return `sha256:${(0, crypto_1.createHash)('sha256').update(value).digest('hex')}`;
115
118
  }
116
- function temporalPacketRawSourcesWithinBoundSource(context, sourceRefs, reviewWindow) {
119
+ function temporalPacketRawSourcesWithinBoundSource(context, sourceRefs, reviewWindow, additionalVisible = () => false) {
117
120
  const rawRefs = sourceRefs.filter((value) => typeof value === 'object' &&
118
121
  value !== null &&
119
122
  !Array.isArray(value) &&
@@ -121,29 +124,23 @@ function temporalPacketRawSourcesWithinBoundSource(context, sourceRefs, reviewWi
121
124
  if (!context.sourceChannel) {
122
125
  return rawRefs.length === 0;
123
126
  }
124
- const anchorMatches = (ref) => Boolean(context.sourceEventId &&
125
- [ref.raw_id, ref.source_id].some((value) => typeof value === 'string' && temporalIdentifierRef(value) === context.sourceEventId));
126
- const allWithinBoundChannel = rawRefs.every((ref) => {
127
- const eventMatches = reviewWindow !== undefined || !context.sourceEventId || anchorMatches(ref);
128
- const channelMatches = !context.sourceChannel ||
129
- (typeof ref.connector === 'string' &&
130
- typeof ref.channel_id === 'string' &&
131
- temporalIdentifierRef(`${ref.connector}:${ref.channel_id}`) === context.sourceChannel);
132
- return eventMatches && channelMatches;
133
- });
134
- return (allWithinBoundChannel &&
135
- (reviewWindow === undefined || rawRefs.some((ref) => anchorMatches(ref))));
127
+ const channelMatches = (ref) => typeof ref.connector === 'string' &&
128
+ typeof ref.channel_id === 'string' &&
129
+ temporalIdentifierRef(`${ref.connector}:${ref.channel_id}`) === context.sourceChannel;
130
+ const anchorMatches = (ref) => channelMatches(ref) &&
131
+ Boolean(context.sourceEventId &&
132
+ [ref.raw_id, ref.source_id].some((value) => typeof value === 'string' && temporalIdentifierRef(value) === context.sourceEventId));
133
+ return (rawRefs.every((ref) => (channelMatches(ref) &&
134
+ (reviewWindow !== undefined || !context.sourceEventId || anchorMatches(ref))) ||
135
+ additionalVisible(ref)) &&
136
+ ((rawRefs.length === 0 && reviewWindow === undefined) ||
137
+ !context.sourceEventId ||
138
+ rawRefs.some(anchorMatches)));
136
139
  }
137
- function temporalPacketReferencesBoundSource(context, sourceRefs, reviewWindow) {
138
- const hasRawReference = sourceRefs.some((value) => typeof value === 'object' &&
139
- value !== null &&
140
- !Array.isArray(value) &&
141
- value.kind === 'raw');
142
- if (context.sourceEventId || context.sourceChannel) {
143
- return (hasRawReference &&
144
- temporalPacketRawSourcesWithinBoundSource(context, sourceRefs, reviewWindow));
145
- }
146
- return temporalPacketRawSourcesWithinBoundSource(context, sourceRefs, reviewWindow);
140
+ function temporalPacketReferencesBoundSource(context, sourceRefs, reviewWindow, additionalVisible) {
141
+ const hasRaw = sourceRefs.some((ref) => typeof ref === 'object' && ref !== null && ref.kind === 'raw');
142
+ return ((!(context.sourceEventId || context.sourceChannel) || hasRaw) &&
143
+ temporalPacketRawSourcesWithinBoundSource(context, sourceRefs, reviewWindow, additionalVisible));
147
144
  }
148
145
  function temporalPacketHasExactReviewWindow(packetJson, reviewWindow) {
149
146
  try {
@@ -232,6 +229,7 @@ const TASK_UPDATE_PUBLIC_FIELD_SET = new Set(TASK_UPDATE_PUBLIC_FIELDS);
232
229
  */
233
230
  const TASK_CREATE_PUBLIC_FIELDS = [
234
231
  'title',
232
+ 'creation_key',
235
233
  'completion_criteria',
236
234
  'status',
237
235
  'priority',
@@ -501,6 +499,7 @@ class GatewayToolExecutor {
501
499
  telegramGateway = null;
502
500
  roleManager;
503
501
  privateConnectorPolicy;
502
+ connectorEventAdapter;
504
503
  executionContextStorage = new async_hooks_1.AsyncLocalStorage();
505
504
  envelopeEnforcer = new index_js_1.EnvelopeEnforcer();
506
505
  channelGrantProvider;
@@ -539,6 +538,95 @@ class GatewayToolExecutor {
539
538
  }
540
539
  taskLedger = null;
541
540
  ownerEventEffectLedger = null;
541
+ ownerActionEffectLedger = null;
542
+ createNativeEffectObserver(state) {
543
+ const ledger = this.ownerActionEffectLedger;
544
+ if (!ledger || !state?.envelope || !state.modelRunId || state.memberScopeRequired) {
545
+ return undefined;
546
+ }
547
+ const attempt = state.workorderAttemptId === undefined
548
+ ? null
549
+ : this.taskLedger?.getWorkOrderById(state.workorderAttemptId);
550
+ const occurrenceKey = attempt ? `workorder:${attempt.idempotencyKey}` : state.sourceMessageRef;
551
+ if (!occurrenceKey) {
552
+ throw new Error('Native effect observation needs an owner occurrence');
553
+ }
554
+ const context = {
555
+ ownerScope: state.agentContext?.principalId ?? 'owner:runtime',
556
+ occurrenceKey,
557
+ modelRunId: state.modelRunId,
558
+ envelopeHash: state.envelope.envelope_hash,
559
+ ...(attempt ? { workOrderAttemptId: attempt.id } : {}),
560
+ };
561
+ if (ledger.hasUnsafeReplayEffects(occurrenceKey)) {
562
+ throw new types_js_1.AgentError('Owner occurrence needs effect reconciliation before replay', 'CODE_ACT_MUTATION_OUTCOME_UNKNOWN', undefined, false);
563
+ }
564
+ const admissionKey = `native-run:${(0, crypto_1.createHash)('sha256').update(context.modelRunId).digest('hex')}`;
565
+ ledger.begin(context, admissionKey, 'native_run', { admitted: true });
566
+ let sequence = 0;
567
+ const pending = new Map();
568
+ return {
569
+ started: (name, input) => {
570
+ const id = typeof input.nativeToolUseId === 'string'
571
+ ? input.nativeToolUseId
572
+ : `observed:${++sequence}`;
573
+ const key = `native:${(0, crypto_1.createHash)('sha256').update(`${context.modelRunId}:${id}`).digest('hex')}`;
574
+ const reservation = ledger.begin(context, key, 'native_tool', { toolName: name });
575
+ if (reservation.state !== 'execute') {
576
+ throw new types_js_1.AgentError('Native effect already observed', 'CODE_ACT_MUTATION_OUTCOME_UNKNOWN', undefined, false);
577
+ }
578
+ pending.set(id, { key, name });
579
+ },
580
+ settled: (name, id, isError) => {
581
+ const match = pending.get(id) ?? [...pending.values()].find((entry) => entry.name === name);
582
+ if (!match) {
583
+ // Completion may be the first provider observation after reconnect.
584
+ const key = `native:${(0, crypto_1.createHash)('sha256').update(`${context.modelRunId}:${id}`).digest('hex')}`;
585
+ ledger.begin(context, key, 'native_tool', { toolName: name });
586
+ ledger.markUnknown(context, key, 'native_tool', 'completion observed without matching start');
587
+ return;
588
+ }
589
+ if (isError) {
590
+ ledger.markUnknown(context, match.key, 'native_tool', 'native tool reported failure');
591
+ }
592
+ else {
593
+ ledger.confirm(context, match.key, 'native_tool', { success: true });
594
+ }
595
+ for (const [pendingId, entry] of pending) {
596
+ if (entry === match) {
597
+ pending.delete(pendingId);
598
+ }
599
+ }
600
+ },
601
+ finished: () => {
602
+ if (pending.size > 0) {
603
+ throw new types_js_1.AgentError('Native effects did not settle before final response', 'CODE_ACT_MUTATION_OUTCOME_UNKNOWN', undefined, false);
604
+ }
605
+ ledger.confirm(context, admissionKey, 'native_run', { completed: true });
606
+ },
607
+ interrupted: () => {
608
+ ledger.markUnknown(context, admissionKey, 'native_run', 'native turn did not finish cleanly');
609
+ for (const { key } of pending.values()) {
610
+ ledger.markUnknown(context, key, 'native_tool', 'native run interrupted');
611
+ }
612
+ },
613
+ };
614
+ }
615
+ setOwnerActionEffectLedger(ledger) {
616
+ this.ownerActionEffectLedger = ledger;
617
+ }
618
+ ownerEffectPort(context) {
619
+ const ledger = this.ownerActionEffectLedger;
620
+ if (!ledger) {
621
+ throw new Error('Owner action effect ledger is not configured');
622
+ }
623
+ return {
624
+ begin: (_batch, key, kind, intent) => ledger.begin(context, key, kind, intent),
625
+ inspect: (_batch, key, kind) => ledger.inspect(context, key, kind),
626
+ confirm: (_batch, key, kind, result) => ledger.confirm(context, key, kind, result),
627
+ markUnknown: (_batch, key, kind, error) => ledger.markUnknown(context, key, kind, error),
628
+ };
629
+ }
542
630
  setTaskLedger(ledger) {
543
631
  this.taskLedger = ledger;
544
632
  }
@@ -765,7 +853,10 @@ class GatewayToolExecutor {
765
853
  constructor(options = {}) {
766
854
  this.channelGrantProvider = options.channelGrantProvider ?? read_js_1.liveBoundaryChannels;
767
855
  const privateWorkspaceRoot = (0, path_1.resolve)(process.env.MAMA_WORKSPACE || (0, path_1.join)((0, os_1.homedir)(), '.mama', 'workspace'));
768
- this.driveTools = new drive_tools_js_1.DriveToolService({ workspaceRoot: privateWorkspaceRoot });
856
+ this.driveTools = new drive_tools_js_1.DriveToolService({
857
+ workspaceRoot: privateWorkspaceRoot,
858
+ runGws: options.driveGwsRunner,
859
+ });
769
860
  this.imageTranslationTools = new image_translation_tools_js_1.ImageTranslationToolService({
770
861
  workspaceRoot: privateWorkspaceRoot,
771
862
  });
@@ -794,6 +885,7 @@ class GatewayToolExecutor {
794
885
  this.roleManager = (0, role_manager_js_1.getRoleManager)(options.rolesConfig ? { rolesConfig: options.rolesConfig } : undefined);
795
886
  this.privateConnectorPolicy =
796
887
  options.privateConnectorPolicy ?? DEFAULT_PRIVATE_CONNECTOR_POLICY;
888
+ this.connectorEventAdapter = options.connectorEventAdapter;
797
889
  if (options.mamaApi) {
798
890
  this.mamaApi = options.mamaApi;
799
891
  }
@@ -1336,6 +1428,75 @@ class GatewayToolExecutor {
1336
1428
  }
1337
1429
  return modelRunId;
1338
1430
  }
1431
+ async requireOwnerActionContext() {
1432
+ const state = this.getExecutionState();
1433
+ const envelope = state.envelope;
1434
+ if (!envelope || !state.modelRunId || state.memberScopeRequired) {
1435
+ throw new types_js_1.AgentError('Owner action requires the current authenticated owner envelope and model run', 'WORKORDER_SUPERSEDED', undefined, false);
1436
+ }
1437
+ const api = await this.initializeMAMAApi();
1438
+ const run = await api.getModelRun?.(state.modelRunId);
1439
+ if (!run || run.status !== 'running' || run.envelope_hash !== envelope.envelope_hash) {
1440
+ throw new Error('Owner action model run is no longer current for this envelope');
1441
+ }
1442
+ const attempt = state.workorderAttemptId === undefined
1443
+ ? null
1444
+ : this.taskLedger?.getWorkOrderById(state.workorderAttemptId);
1445
+ if (state.workorderAttemptId !== undefined &&
1446
+ (!attempt ||
1447
+ attempt.status !== 'in_progress' ||
1448
+ run.input_refs?.workorderAttemptId !== attempt.id)) {
1449
+ throw new Error('Owner action workorder attempt is no longer active');
1450
+ }
1451
+ const occurrenceKey = attempt ? `workorder:${attempt.idempotencyKey}` : state.sourceMessageRef;
1452
+ if (!occurrenceKey) {
1453
+ throw new Error('Owner action is missing its host-issued occurrence identity');
1454
+ }
1455
+ return {
1456
+ ownerScope: state.agentContext?.principalId ?? 'owner:runtime',
1457
+ occurrenceKey,
1458
+ modelRunId: state.modelRunId,
1459
+ envelopeHash: envelope.envelope_hash,
1460
+ ...(attempt ? { workOrderAttemptId: attempt.id } : {}),
1461
+ };
1462
+ }
1463
+ temporalCorroborationVisible = (ref) => {
1464
+ if (typeof ref.connector !== 'string' || typeof ref.channel_id !== 'string') {
1465
+ return false;
1466
+ }
1467
+ return this.currentOwnerPartitionVisibility()({
1468
+ connector: ref.connector,
1469
+ channel: ref.channel_id,
1470
+ });
1471
+ };
1472
+ currentOwnerPartitionVisibility() {
1473
+ const state = this.getExecutionState();
1474
+ if (!state.envelope || state.memberScopeRequired) {
1475
+ throw new Error('Owner source visibility requires an owner envelope');
1476
+ }
1477
+ const channels = (0, read_js_1.narrowGrantToEnvelope)(snapshotChannelGrant(this.channelGrantProvider), {
1478
+ connectors: state.envelope.scope.raw_connectors,
1479
+ scopes: state.envelope.scope.memory_scopes,
1480
+ });
1481
+ return ({ connector, channel }) => channels[connector]?.includes(channel) === true;
1482
+ }
1483
+ async externalCandidateSource(candidateId) {
1484
+ const attemptId = this.getExecutionState().workorderAttemptId;
1485
+ if (attemptId !== undefined &&
1486
+ this.taskLedger?.getWorkOrderById(attemptId)?.workKind === 'board') {
1487
+ await this.requireExternalLifecycleModelRun(attemptId);
1488
+ if (this.getExecutionState().envelope) {
1489
+ const context = await this.requireOwnerActionContext();
1490
+ if (this.taskLedger
1491
+ .listOwnerActionCandidates(context)
1492
+ .some((candidate) => candidate.candidateId === candidateId)) {
1493
+ return { kind: 'owner_run', context };
1494
+ }
1495
+ }
1496
+ return { kind: 'board', attemptId };
1497
+ }
1498
+ return { kind: 'owner_run', context: await this.requireOwnerActionContext() };
1499
+ }
1339
1500
  async beginTraceIfNeeded(ctx, gatewayCallId) {
1340
1501
  if (ctx.modelRunId) {
1341
1502
  const api = await this.initializeMAMAApi();
@@ -1878,9 +2039,9 @@ class GatewayToolExecutor {
1878
2039
  case 'Bash':
1879
2040
  return await this.executeBash(input);
1880
2041
  case 'discord_send':
1881
- return await this.executeDiscordSend(input);
2042
+ return await this.executeOwnerWorkspaceEffect('discord_send', input, () => this.executeDiscordSend(input));
1882
2043
  case 'slack_send':
1883
- return await this.executeSlackSend(input);
2044
+ return await this.executeOwnerWorkspaceEffect('slack_send', input, () => this.executeSlackSend(input));
1884
2045
  case 'telegram_send':
1885
2046
  return await this.executeTelegramSend(input);
1886
2047
  case 'ocr_image': {
@@ -2106,16 +2267,35 @@ class GatewayToolExecutor {
2106
2267
  const uploadInput = input;
2107
2268
  const state = this.getExecutionState();
2108
2269
  const sourceMessageRef = state.sourceMessageRef;
2109
- if (!sourceMessageRef?.startsWith('owner-event:')) {
2270
+ const legacyOwnerEvent = sourceMessageRef?.startsWith('owner-event:') === true;
2271
+ if (!legacyOwnerEvent && !this.ownerActionEffectLedger) {
2110
2272
  return {
2111
2273
  success: true,
2112
2274
  result: (0, drive_tools_js_1.asUntrustedDriveEvidence)(await this.driveTools.upload(uploadInput)),
2113
2275
  };
2114
2276
  }
2115
- const effect = this.requireOwnerEventEffect(uploadInput.effect_key, 'drive_upload');
2116
- const ledger = this.requireOwnerEventEffectLedger();
2117
- const occurrence = `${sourceMessageRef}:drive:${effect.actionKey}`;
2277
+ const requestSha256 = (0, crypto_1.createHash)('sha256')
2278
+ .update(JSON.stringify([
2279
+ uploadInput.localPath,
2280
+ uploadInput.folderId,
2281
+ uploadInput.fileName ?? null,
2282
+ uploadInput.destinationCapability ?? null,
2283
+ ]))
2284
+ .digest('hex');
2285
+ const context = legacyOwnerEvent ? null : await this.requireOwnerActionContext();
2286
+ const effect = legacyOwnerEvent
2287
+ ? this.requireOwnerEventEffect(uploadInput.effect_key, 'drive_upload')
2288
+ : { batchId: 0, actionKey: `drive:${uploadInput.effect_key ?? requestSha256}` };
2289
+ const ledger = context
2290
+ ? this.ownerEffectPort(context)
2291
+ : this.requireOwnerEventEffectLedger();
2292
+ const occurrence = context
2293
+ ? `${context.ownerScope}:${context.occurrenceKey}:drive:${effect.actionKey}`
2294
+ : `${sourceMessageRef}:drive:${effect.actionKey}`;
2118
2295
  let reservation = ledger.inspect(effect.batchId, effect.actionKey, 'drive_upload');
2296
+ if (context && reservation && reservation.intent.requestSha256 !== requestSha256) {
2297
+ throw new Error('Drive action key was already reserved for a different upload request');
2298
+ }
2119
2299
  if (reservation?.state === 'confirmed') {
2120
2300
  if (!reservation.result) {
2121
2301
  throw new Error('Confirmed Drive upload is missing its durable result');
@@ -2129,6 +2309,7 @@ class GatewayToolExecutor {
2129
2309
  reservation = ledger.begin(effect.batchId, effect.actionKey, 'drive_upload', {
2130
2310
  folderId: preparation.prepared.folderId,
2131
2311
  fileName: preparation.prepared.fileName,
2312
+ ...(context ? { requestSha256 } : {}),
2132
2313
  });
2133
2314
  if (reservation.state === 'confirmed') {
2134
2315
  if (!reservation.result) {
@@ -2173,8 +2354,7 @@ class GatewayToolExecutor {
2173
2354
  case 'os_get_config':
2174
2355
  return await this.executeGetConfig(input);
2175
2356
  case 'webchat_send':
2176
- return await this.executeWebchatSend(input // session_id omitted: all files use shared outbound dir
2177
- );
2357
+ return await this.executeOwnerWorkspaceEffect('webchat_send', input, () => this.executeWebchatSend(input));
2178
2358
  // Code-Act sandbox execution
2179
2359
  case 'code_act':
2180
2360
  return await this.executeCodeAct(input);
@@ -2296,16 +2476,20 @@ class GatewayToolExecutor {
2296
2476
  if (!slotsInput || typeof slotsInput !== 'object') {
2297
2477
  throw new types_js_1.AgentError('report_publish requires slots object', 'TOOL_ERROR', undefined, false);
2298
2478
  }
2299
- if (this.getExecutionState().temporalWorkContext) {
2300
- const slotNames = Object.keys(slotsInput);
2301
- if (slotNames.length !== 1 ||
2302
- slotNames[0] !== 'pipeline' ||
2303
- typeof slotsInput.pipeline !== 'string') {
2304
- throw new types_js_1.AgentError('Temporal report_publish accepts exactly the host-derived pipeline slot', 'TOOL_ERROR', undefined, false);
2305
- }
2479
+ const basisRevision = input.basis_revision;
2480
+ if (basisRevision !== undefined &&
2481
+ basisRevision !== null &&
2482
+ (typeof basisRevision !== 'string' ||
2483
+ !basisRevision.trim() ||
2484
+ basisRevision !== basisRevision.trim())) {
2485
+ throw new types_js_1.AgentError('report_publish basis_revision must be a canonical task basis from board_read', 'TOOL_ERROR', undefined, false);
2306
2486
  }
2307
2487
  if (this.reportPublisher) {
2308
- const publication = this.reportPublisher(slotsInput);
2488
+ const publication = basisRevision === undefined
2489
+ ? this.reportPublisher(slotsInput)
2490
+ : this.reportPublisher(slotsInput, {
2491
+ basisRevision: basisRevision,
2492
+ });
2309
2493
  // Backward compatibility: older injected publishers return void
2310
2494
  // or the exact changed slot array. Production distinguishes slots
2311
2495
  // accepted as present from slots whose HTML actually changed.
@@ -2632,13 +2816,23 @@ class GatewayToolExecutor {
2632
2816
  }
2633
2817
  case 'wiki_read': {
2634
2818
  const wikiAuthority = this.getExecutionState().wikiTaskRange;
2635
- if (!wikiAuthority || wikiAuthority.ownerDate === null) {
2636
- throw new types_js_1.AgentError('wiki_read requires a host-issued wiki workorder ownerDate', 'WORKORDER_SUPERSEDED', undefined, false);
2637
- }
2638
2819
  if (!this.obsidianVaultPath) {
2639
2820
  throw new types_js_1.AgentError('Wiki vault path not configured', 'TOOL_ERROR', undefined, false);
2640
2821
  }
2641
2822
  try {
2823
+ if (!wikiAuthority) {
2824
+ const readInput = input;
2825
+ return {
2826
+ success: true,
2827
+ ...(0, wiki_read_js_1.readWikiPages)({
2828
+ root: this.obsidianVaultPath,
2829
+ paths: readInput.paths,
2830
+ contentOffset: readInput.content_offset,
2831
+ contentLimit: readInput.content_limit,
2832
+ contentVersions: readInput.content_versions,
2833
+ }),
2834
+ };
2835
+ }
2642
2836
  const attemptId = this.getExecutionState().workorderAttemptId;
2643
2837
  if (!Number.isSafeInteger(attemptId) || attemptId < 1) {
2644
2838
  throw new Error('wiki_read requires a host-issued workorder attempt');
@@ -2650,7 +2844,7 @@ class GatewayToolExecutor {
2650
2844
  }
2651
2845
  const paths = Array.isArray(readInput.paths) ? readInput.paths : [];
2652
2846
  for (const rawPath of paths) {
2653
- const path = (0, wiki_read_js_1.assertAllowedWikiWorkorderPath)(rawPath, wikiAuthority.ownerDate);
2847
+ const path = (0, wiki_read_js_1.normalizeWikiRelativePath)(rawPath);
2654
2848
  const key = this.wikiReadCoverageKey(attemptId, path);
2655
2849
  const prior = this.wikiReadCoverage.get(key);
2656
2850
  if (requestedOffset === 0) {
@@ -2662,10 +2856,17 @@ class GatewayToolExecutor {
2662
2856
  }
2663
2857
  const result = (0, wiki_read_js_1.readWikiPages)({
2664
2858
  root: this.obsidianVaultPath,
2665
- ownerDate: wikiAuthority.ownerDate,
2666
2859
  paths: readInput.paths,
2667
2860
  contentOffset: readInput.content_offset,
2668
2861
  contentLimit: readInput.content_limit,
2862
+ contentVersions: Object.fromEntries(paths.map((rawPath) => {
2863
+ const path = (0, wiki_read_js_1.normalizeWikiRelativePath)(rawPath);
2864
+ return [
2865
+ path,
2866
+ this.wikiReadCoverage.get(this.wikiReadCoverageKey(attemptId, path))
2867
+ ?.contentVersion ?? null,
2868
+ ];
2869
+ })),
2669
2870
  });
2670
2871
  for (const page of result.pages) {
2671
2872
  const key = this.wikiReadCoverageKey(attemptId, page.path);
@@ -2941,32 +3142,62 @@ class GatewayToolExecutor {
2941
3142
  };
2942
3143
  }
2943
3144
  }
3145
+ case 'task_external_candidates': {
3146
+ if (!this.taskLedger) {
3147
+ throw new Error('Task ledger not configured');
3148
+ }
3149
+ const raw = input;
3150
+ if (!Array.isArray(raw.event_ids) ||
3151
+ raw.event_ids.length < 1 ||
3152
+ raw.event_ids.length > 20 ||
3153
+ raw.event_ids.some((id) => typeof id !== 'string' || !id.trim() || id !== id.trim())) {
3154
+ throw new Error('task_external_candidates requires 1-20 observed event_ids');
3155
+ }
3156
+ const context = await this.requireOwnerActionContext();
3157
+ const adapter = this.connectorEventAdapter ?? (await getContextPacketLookupAdapter());
3158
+ const result = (0, external_lifecycle_discovery_js_1.attestOwnerExternalLifecycleCandidates)({
3159
+ eventIds: raw.event_ids,
3160
+ context,
3161
+ ledger: this.taskLedger,
3162
+ getAdapter: () => adapter,
3163
+ privateConnectorPolicy: this.privateConnectorPolicy,
3164
+ rawConnectorScope: this.getExecutionState().envelope.scope.raw_connectors,
3165
+ isPartitionVisible: this.currentOwnerPartitionVisibility(),
3166
+ });
3167
+ return { success: true, ...result };
3168
+ }
2944
3169
  case 'task_external_bind': {
2945
3170
  if (!this.taskLedger) {
2946
3171
  return { success: false, error: 'Task ledger not configured' };
2947
3172
  }
2948
- const state = this.getExecutionState();
2949
- const attemptId = state.workorderAttemptId;
2950
- if (attemptId === undefined || !Number.isSafeInteger(attemptId) || attemptId <= 0) {
2951
- throw new types_js_1.AgentError('task_external_bind requires a trusted claimed board attempt', 'WORKORDER_SUPERSEDED', undefined, false);
2952
- }
2953
3173
  try {
2954
3174
  (0, external_lifecycle_candidates_js_1.validateExternalLifecycleDecision)('binding', input);
2955
3175
  }
2956
3176
  catch (error) {
2957
3177
  throw new types_js_1.AgentError(error instanceof Error ? error.message : 'task_external_bind input is invalid', 'TOOL_ERROR', error instanceof Error ? error : undefined, false);
2958
3178
  }
2959
- const modelRunId = await this.requireExternalLifecycleModelRun(attemptId);
3179
+ const source = await this.externalCandidateSource(input.candidate_id);
3180
+ const attemptId = source.kind === 'board' ? source.attemptId : source.context.workOrderAttemptId;
3181
+ const modelRunId = source.kind === 'owner_run'
3182
+ ? source.context.modelRunId
3183
+ : this.getExecutionState().modelRunId;
2960
3184
  let candidate;
2961
3185
  try {
2962
- candidate = this.taskLedger.loadBoardCandidate(attemptId, input.candidate_id, 'binding');
3186
+ candidate = this.taskLedger.loadExternalCandidate(source, input.candidate_id, 'binding');
2963
3187
  }
2964
3188
  catch (error) {
2965
3189
  throw new types_js_1.AgentError(error instanceof Error
2966
3190
  ? error.message
2967
3191
  : 'task_external_bind candidate is unavailable', 'WORKORDER_SUPERSEDED', error instanceof Error ? error : undefined, false);
2968
3192
  }
2969
- const receipt = this.taskLedger.applyExternalBindingDecision(attemptId, input, {
3193
+ if (this.getExecutionState().envelope &&
3194
+ !this.currentOwnerPartitionVisibility()({
3195
+ connector: candidate.connector,
3196
+ channel: candidate.channelPartition,
3197
+ })) {
3198
+ throw new Error('External candidate source is no longer visible under the current owner grant');
3199
+ }
3200
+ const receipt = this.taskLedger.applyExternalBindingDecision(source, input, {
2970
3201
  runId: modelRunId,
2971
3202
  workOrderAttemptId: attemptId,
2972
3203
  causeEventIds: [candidate.eventId],
@@ -2984,28 +3215,34 @@ class GatewayToolExecutor {
2984
3215
  if (!this.taskLedger) {
2985
3216
  return { success: false, error: 'Task ledger not configured' };
2986
3217
  }
2987
- const state = this.getExecutionState();
2988
- const attemptId = state.workorderAttemptId;
2989
- if (attemptId === undefined || !Number.isSafeInteger(attemptId) || attemptId <= 0) {
2990
- throw new types_js_1.AgentError('task_lifecycle_reconcile requires a trusted claimed board attempt', 'WORKORDER_SUPERSEDED', undefined, false);
2991
- }
2992
3218
  try {
2993
3219
  (0, external_lifecycle_candidates_js_1.validateExternalLifecycleDecision)('lifecycle', input);
2994
3220
  }
2995
3221
  catch (error) {
2996
3222
  throw new types_js_1.AgentError(error instanceof Error ? error.message : 'task_lifecycle_reconcile input is invalid', 'TOOL_ERROR', error instanceof Error ? error : undefined, false);
2997
3223
  }
2998
- const modelRunId = await this.requireExternalLifecycleModelRun(attemptId);
3224
+ const source = await this.externalCandidateSource(input.candidate_id);
3225
+ const attemptId = source.kind === 'board' ? source.attemptId : source.context.workOrderAttemptId;
3226
+ const modelRunId = source.kind === 'owner_run'
3227
+ ? source.context.modelRunId
3228
+ : this.getExecutionState().modelRunId;
2999
3229
  let candidate;
3000
3230
  try {
3001
- candidate = this.taskLedger.loadBoardCandidate(attemptId, input.candidate_id, 'lifecycle');
3231
+ candidate = this.taskLedger.loadExternalCandidate(source, input.candidate_id, 'lifecycle');
3002
3232
  }
3003
3233
  catch (error) {
3004
3234
  throw new types_js_1.AgentError(error instanceof Error
3005
3235
  ? error.message
3006
3236
  : 'task_lifecycle_reconcile candidate is unavailable', 'WORKORDER_SUPERSEDED', error instanceof Error ? error : undefined, false);
3007
3237
  }
3008
- const receipt = this.taskLedger.applyExternalLifecycleDecision(attemptId, input, {
3238
+ if (this.getExecutionState().envelope &&
3239
+ !this.currentOwnerPartitionVisibility()({
3240
+ connector: candidate.connector,
3241
+ channel: candidate.channelPartition,
3242
+ })) {
3243
+ throw new Error('External candidate source is no longer visible under the current owner grant');
3244
+ }
3245
+ const receipt = this.taskLedger.applyExternalLifecycleDecision(source, input, {
3009
3246
  runId: modelRunId,
3010
3247
  workOrderAttemptId: attemptId,
3011
3248
  causeEventIds: [candidate.eventId],
@@ -3041,20 +3278,29 @@ class GatewayToolExecutor {
3041
3278
  if (completionCriteria.trim().length > task_ledger_js_1.TASK_COMPLETION_CRITERIA_MAX_LENGTH) {
3042
3279
  throw new types_js_1.AgentError(`task_create completion_criteria must be at most ${task_ledger_js_1.TASK_COMPLETION_CRITERIA_MAX_LENGTH} characters`, 'TOOL_ERROR', undefined, false);
3043
3280
  }
3044
- return {
3045
- success: true,
3046
- task: (0, task_list_views_js_1.serializeTaskToolRecord)(
3047
- // The run id comes from trusted execution state, never from the tool call:
3048
- // an agent that can name its own run in the effect ledger can sign someone
3049
- // else's work with it.
3050
- this.taskLedger.create(input, {
3051
- runId: this.getExecutionState().modelRunId ?? null,
3052
- workOrderAttemptId: this.getExecutionState().workorderAttemptId,
3053
- requiresExpectedRevision: this.getExecutionState().workorderAttemptId !== undefined,
3054
- causeEventIds: this.getExecutionState().causeEventIds,
3055
- causeKind: this.getExecutionState().source === 'operator' ? 'clock' : 'owner_message',
3056
- })),
3057
- };
3281
+ const { creation_key: creationKey, ...taskInput } = rawTaskCreate;
3282
+ const create = () => this.taskLedger.create(taskInput, {
3283
+ runId: this.getExecutionState().modelRunId ?? null,
3284
+ workOrderAttemptId: this.getExecutionState().workorderAttemptId,
3285
+ requiresExpectedRevision: this.getExecutionState().workorderAttemptId !== undefined,
3286
+ causeEventIds: this.getExecutionState().causeEventIds,
3287
+ causeKind: this.getExecutionState().source === 'operator' ? 'clock' : 'owner_message',
3288
+ });
3289
+ if (this.ownerActionEffectLedger) {
3290
+ if (typeof creationKey !== 'string' ||
3291
+ !creationKey.trim() ||
3292
+ creationKey.length > 200) {
3293
+ throw new types_js_1.AgentError('task_create requires creation_key: a stable name for this logical task within the occurrence; reuse it on retry.', 'TOOL_ERROR', undefined, false);
3294
+ }
3295
+ const context = await this.requireOwnerActionContext();
3296
+ const receipt = this.ownerActionEffectLedger.atomic(context, `task:${creationKey}`, 'task_create', taskInput, () => ({ taskId: create().id }));
3297
+ const task = this.taskLedger.getById(Number(receipt.taskId));
3298
+ if (!task) {
3299
+ throw new Error('Created task receipt references a missing task');
3300
+ }
3301
+ return { success: true, task: (0, task_list_views_js_1.serializeTaskToolRecord)(task) };
3302
+ }
3303
+ return { success: true, task: (0, task_list_views_js_1.serializeTaskToolRecord)(create()) };
3058
3304
  }
3059
3305
  case 'task_reclassify': {
3060
3306
  if (!this.taskLedger) {
@@ -3117,9 +3363,6 @@ class GatewayToolExecutor {
3117
3363
  'Use latest_event for input; responses return latestEvent.', 'TOOL_ERROR', undefined, false);
3118
3364
  }
3119
3365
  if (Object.prototype.hasOwnProperty.call(rawTaskUpdate, 'completion_criteria')) {
3120
- if (this.getExecutionState().source === 'owner-event') {
3121
- throw new types_js_1.AgentError('task_update completion_criteria is unavailable on owner-event turns; full Board maintenance or the owner conversation owns qualification', 'TOOL_ERROR', undefined, false);
3122
- }
3123
3366
  const criteria = rawTaskUpdate.completion_criteria;
3124
3367
  if (typeof criteria !== 'string' ||
3125
3368
  criteria.trim().length === 0 ||
@@ -3155,23 +3398,47 @@ class GatewayToolExecutor {
3155
3398
  }
3156
3399
  const parsed = JSON.parse(packet.packet_json);
3157
3400
  const selectedRaw = parsed.selected_evidence?.find((item) => item.ref?.kind === 'raw' && item.ref.raw_id === reviewAnchorRef)?.ref;
3401
+ if (!selectedRaw) {
3402
+ return {
3403
+ success: false,
3404
+ code: 'review_anchor_not_selected',
3405
+ error: 'The review anchor is not raw evidence selected in this context packet.',
3406
+ next_action: 'Read relevant raw evidence progressively and use a context packet that selects its actual raw_id. Do not guess an event ID or retry the same packet.',
3407
+ };
3408
+ }
3158
3409
  const task = this.taskLedger.getById(id);
3159
3410
  const connector = selectedRaw?.connector;
3160
3411
  const channelId = selectedRaw?.channel_id;
3161
3412
  const sourceChannel = typeof connector === 'string' && typeof channelId === 'string'
3162
3413
  ? `${connector}:${channelId}`
3163
3414
  : null;
3164
- if (!task || !sourceChannel || task.sourceChannel !== sourceChannel) {
3165
- throw new types_js_1.AgentError('task_update review anchor is outside the task source boundary', 'TOOL_ERROR', undefined, false);
3415
+ if (!task || !sourceChannel) {
3416
+ throw new types_js_1.AgentError('task_update review anchor has no verified source identity', 'TOOL_ERROR', undefined, false);
3417
+ }
3418
+ const visible = this.currentOwnerPartitionVisibility();
3419
+ const taskSourceSeparator = task.sourceChannel?.indexOf(':') ?? -1;
3420
+ if (!visible({ connector: connector, channel: channelId }) ||
3421
+ (task.sourceChannel &&
3422
+ (taskSourceSeparator < 1 ||
3423
+ !visible({
3424
+ connector: task.sourceChannel.slice(0, taskSourceSeparator),
3425
+ channel: task.sourceChannel.slice(taskSourceSeparator + 1),
3426
+ })))) {
3427
+ return {
3428
+ success: false,
3429
+ code: 'review_source_not_authorized',
3430
+ error: 'Current owner grants do not authorize the task source and review evidence source.',
3431
+ };
3166
3432
  }
3167
- const adapter = (await getContextPacketLookupAdapter());
3433
+ const adapter = (this.connectorEventAdapter ??
3434
+ (await getContextPacketLookupAdapter()));
3168
3435
  const anchor = adapter
3169
3436
  .prepare(`SELECT source_connector, channel,
3170
3437
  COALESCE(event_datetime, source_timestamp_ms) AS source_timestamp_ms
3171
3438
  FROM connector_event_index
3172
3439
  WHERE event_index_id = ?
3173
3440
  LIMIT 1`)
3174
- .get(reviewAnchorRef);
3441
+ .all(reviewAnchorRef)[0];
3175
3442
  if (!anchor ||
3176
3443
  anchor.source_connector !== connector ||
3177
3444
  anchor.channel !== channelId ||
@@ -3264,8 +3531,8 @@ class GatewayToolExecutor {
3264
3531
  (effectInput.outcome === 'deferred'
3265
3532
  ? Array.isArray(packet.source_refs) &&
3266
3533
  packet.source_refs.length > 0 &&
3267
- !temporalPacketReferencesBoundSource(context, packet.source_refs, reviewWindow)
3268
- : !temporalPacketReferencesBoundSource(context, packet.source_refs, reviewWindow))) {
3534
+ !temporalPacketReferencesBoundSource(context, packet.source_refs, reviewWindow, this.temporalCorroborationVisible)
3535
+ : !temporalPacketReferencesBoundSource(context, packet.source_refs, reviewWindow, this.temporalCorroborationVisible))) {
3269
3536
  throw new types_js_1.AgentError('task_temporal_reconcile context packet is not bound to the active task source', 'TOOL_ERROR', undefined, false);
3270
3537
  }
3271
3538
  const evidence = {
@@ -3393,6 +3660,61 @@ class GatewayToolExecutor {
3393
3660
  return { success: false, error: 'Failed to read file: attachment extraction failed' };
3394
3661
  }
3395
3662
  }
3663
+ async executeOwnerWorkspaceEffect(kind, intent, execute) {
3664
+ const ledger = this.ownerActionEffectLedger;
3665
+ if (!ledger) {
3666
+ try {
3667
+ const { effectStarted: _notStarted, ...result } = await execute();
3668
+ return result;
3669
+ }
3670
+ catch (error) {
3671
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
3672
+ }
3673
+ }
3674
+ const context = await this.requireOwnerActionContext();
3675
+ const digest = (0, crypto_1.createHash)('sha256').update((0, owner_action_effects_js_1.canonicalOwnerActionJson)(intent)).digest('hex');
3676
+ const actionKey = `${kind}:${digest}`;
3677
+ const reserved = ledger.begin(context, actionKey, kind, { inputSha256: digest });
3678
+ if (reserved.state === 'confirmed') {
3679
+ return {
3680
+ success: true,
3681
+ output: 'This exact action already completed in this occurrence; it was not executed again.',
3682
+ };
3683
+ }
3684
+ if (reserved.state !== 'execute') {
3685
+ throw new types_js_1.AgentError('Prior workspace effect needs reconciliation before replay', 'CODE_ACT_MUTATION_OUTCOME_UNKNOWN', undefined, false);
3686
+ }
3687
+ try {
3688
+ const result = await execute();
3689
+ if (!result.success && result.effectStarted === false) {
3690
+ ledger.releaseUnstarted(context, actionKey, kind);
3691
+ const { effectStarted: _notStarted, ...failure } = result;
3692
+ return failure;
3693
+ }
3694
+ if (!result.success) {
3695
+ throw new Error('Workspace effect returned failure; partial effects may exist');
3696
+ }
3697
+ ledger.confirm(context, actionKey, kind, { success: true });
3698
+ return result;
3699
+ }
3700
+ catch (error) {
3701
+ // A spawn failure proves the child never ran. A nonzero exit does NOT:
3702
+ // a command may write data before exiting with an error.
3703
+ const spawnError = error;
3704
+ if ((kind === 'Bash' || kind === 'obsidian') &&
3705
+ spawnError.code === 'ENOENT' &&
3706
+ typeof spawnError.syscall === 'string' &&
3707
+ spawnError.syscall.startsWith('spawn')) {
3708
+ ledger.releaseUnstarted(context, actionKey, kind);
3709
+ return {
3710
+ success: false,
3711
+ error: 'Executable or working directory is unavailable; no process started.',
3712
+ };
3713
+ }
3714
+ ledger.markUnknown(context, actionKey, kind, 'workspace effect did not settle successfully');
3715
+ throw new types_js_1.AgentError('Workspace effect outcome requires reconciliation', 'CODE_ACT_MUTATION_OUTCOME_UNKNOWN', error instanceof Error ? error : undefined, false);
3716
+ }
3717
+ }
3396
3718
  /**
3397
3719
  * Execute Write tool - Write content to a file
3398
3720
  * Checks path permissions based on current AgentContext
@@ -3422,15 +3744,12 @@ class GatewayToolExecutor {
3422
3744
  return { success: false, error: `Access denied: Can only write files to ${mamaDir}` };
3423
3745
  }
3424
3746
  }
3425
- try {
3747
+ return this.executeOwnerWorkspaceEffect('Write', { path: (0, path_1.resolve)(expandedPath), content }, async () => {
3426
3748
  const dir = (0, path_1.dirname)(expandedPath);
3427
3749
  (0, fs_1.mkdirSync)(dir, { recursive: true });
3428
3750
  (0, fs_1.writeFileSync)(expandedPath, content, 'utf-8');
3429
3751
  return { success: true };
3430
- }
3431
- catch (err) {
3432
- return { success: false, error: `Failed to write file: ${err}` };
3433
- }
3752
+ });
3434
3753
  }
3435
3754
  /**
3436
3755
  * Execute Bash tool - Execute bash command
@@ -3545,36 +3864,25 @@ class GatewayToolExecutor {
3545
3864
  };
3546
3865
  }
3547
3866
  }
3548
- // Handle restart: deferred restart (agent survives to respond, service restarts after 3s)
3549
- const restartPattern = /systemctl\s+--user\s+restart\s+mama-os/i;
3550
- if (restartPattern.test(command)) {
3551
- const child = (0, child_process_1.spawn)('bash', ['-c', 'sleep 3 && systemctl --user restart mama-os'], {
3552
- detached: true,
3553
- stdio: 'ignore',
3554
- });
3555
- child.unref();
3556
- return {
3557
- success: true,
3558
- output: 'mama-os restart will execute in 3 seconds. Current session will be terminated.',
3559
- };
3560
- }
3561
- try {
3867
+ return this.executeOwnerWorkspaceEffect('Bash', { command, cwd: (0, path_1.resolve)(cwd) }, async () => {
3868
+ // A deferred restart is still a non-replayable shell effect.
3869
+ const restartPattern = /systemctl\s+--user\s+restart\s+mama-os/i;
3870
+ if (restartPattern.test(command)) {
3871
+ const child = (0, child_process_1.spawn)('bash', ['-c', 'sleep 3 && systemctl --user restart mama-os'], {
3872
+ detached: true,
3873
+ stdio: 'ignore',
3874
+ });
3875
+ child.unref();
3876
+ return { success: true, output: 'mama-os restart will execute in 3 seconds.' };
3877
+ }
3562
3878
  const output = (0, child_process_1.execSync)(command, {
3563
- cwd: workdir || process.env.MAMA_WORKSPACE || (0, path_1.join)((0, os_1.homedir)(), '.mama', 'workspace'),
3879
+ cwd,
3564
3880
  encoding: 'utf-8',
3565
3881
  maxBuffer: 10 * 1024 * 1024,
3566
3882
  timeout: 60000,
3567
3883
  });
3568
3884
  return { success: true, output };
3569
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3570
- }
3571
- catch (err) {
3572
- return {
3573
- success: false,
3574
- error: `Command failed: ${err.message}`,
3575
- output: err.stdout || err.stderr,
3576
- };
3577
- }
3885
+ });
3578
3886
  }
3579
3887
  /**
3580
3888
  * Execute discord_send tool - Send message/file to Discord channel
@@ -3583,10 +3891,10 @@ class GatewayToolExecutor {
3583
3891
  async executeDiscordSend(input) {
3584
3892
  const { channel_id, message, image_path, file_path } = input;
3585
3893
  if (!channel_id) {
3586
- return { success: false, error: 'channel_id is required' };
3894
+ return { success: false, effectStarted: false, error: 'channel_id is required' };
3587
3895
  }
3588
3896
  if (!this.discordGateway) {
3589
- return { success: false, error: 'Discord gateway not configured' };
3897
+ return { success: false, effectStarted: false, error: 'Discord gateway not configured' };
3590
3898
  }
3591
3899
  try {
3592
3900
  // file_path takes precedence, fallback to image_path for backwards compatibility
@@ -3598,7 +3906,11 @@ class GatewayToolExecutor {
3598
3906
  await this.discordGateway.sendMessage(channel_id, message);
3599
3907
  }
3600
3908
  else {
3601
- return { success: false, error: 'Either message, file_path, or image_path is required' };
3909
+ return {
3910
+ success: false,
3911
+ effectStarted: false,
3912
+ error: 'Either message, file_path, or image_path is required',
3913
+ };
3602
3914
  }
3603
3915
  return { success: true };
3604
3916
  }
@@ -3612,10 +3924,10 @@ class GatewayToolExecutor {
3612
3924
  async executeSlackSend(input) {
3613
3925
  const { channel_id, message, file_path } = input;
3614
3926
  if (!channel_id) {
3615
- return { success: false, error: 'channel_id is required' };
3927
+ return { success: false, effectStarted: false, error: 'channel_id is required' };
3616
3928
  }
3617
3929
  if (!this.slackGateway) {
3618
- return { success: false, error: 'Slack gateway not configured' };
3930
+ return { success: false, effectStarted: false, error: 'Slack gateway not configured' };
3619
3931
  }
3620
3932
  try {
3621
3933
  if (file_path) {
@@ -3625,7 +3937,11 @@ class GatewayToolExecutor {
3625
3937
  await this.slackGateway.sendMessage(channel_id, message);
3626
3938
  }
3627
3939
  else {
3628
- return { success: false, error: 'Either message or file_path is required' };
3940
+ return {
3941
+ success: false,
3942
+ effectStarted: false,
3943
+ error: 'Either message or file_path is required',
3944
+ };
3629
3945
  }
3630
3946
  return { success: true };
3631
3947
  }
@@ -3645,7 +3961,8 @@ class GatewayToolExecutor {
3645
3961
  return { success: false, error: 'Telegram gateway not configured' };
3646
3962
  }
3647
3963
  const sourceMessageRef = this.getExecutionState().sourceMessageRef;
3648
- const ownerEvent = sourceMessageRef?.startsWith('owner-event:') === true;
3964
+ const legacyOwnerEvent = sourceMessageRef?.startsWith('owner-event:') === true;
3965
+ const ownerEvent = legacyOwnerEvent || this.ownerActionEffectLedger !== null;
3649
3966
  let ownerEffect = null;
3650
3967
  let ownerLedger = null;
3651
3968
  let ownerIntent = null;
@@ -3655,9 +3972,27 @@ class GatewayToolExecutor {
3655
3972
  // `-> error/missing receipt -> unknown -> reconcile-only
3656
3973
  try {
3657
3974
  if (ownerEvent) {
3658
- ownerEffect = this.requireOwnerEventEffect(delivery_key, 'telegram_send');
3659
- ownerLedger = this.requireOwnerEventEffectLedger();
3660
- idempotencyKey = `${sourceMessageRef}:telegram:${delivery_key}`;
3975
+ if (legacyOwnerEvent) {
3976
+ ownerEffect = this.requireOwnerEventEffect(delivery_key, 'telegram_send');
3977
+ ownerLedger = this.requireOwnerEventEffectLedger();
3978
+ idempotencyKey = `${sourceMessageRef}:telegram:${delivery_key}`;
3979
+ }
3980
+ else {
3981
+ const context = await this.requireOwnerActionContext();
3982
+ const identity = [
3983
+ sourceMessageRef ?? context.occurrenceKey,
3984
+ chat_id,
3985
+ message ?? null,
3986
+ file_path ?? null,
3987
+ sticker_emotion ?? null,
3988
+ ];
3989
+ if (delivery_key !== undefined) {
3990
+ identity.push(delivery_key);
3991
+ }
3992
+ idempotencyKey = (0, crypto_1.createHash)('sha256').update(JSON.stringify(identity)).digest('hex');
3993
+ ownerEffect = { batchId: 0, actionKey: `telegram:${delivery_key ?? idempotencyKey}` };
3994
+ ownerLedger = this.ownerEffectPort(context);
3995
+ }
3661
3996
  const existing = ownerLedger.inspect(ownerEffect.batchId, ownerEffect.actionKey, 'telegram_send');
3662
3997
  if (existing && !(0, owner_event_effects_js_1.isOwnerEventTelegramIntentV1)(existing.intent)) {
3663
3998
  return existing.state === 'confirmed'
@@ -3679,9 +4014,20 @@ class GatewayToolExecutor {
3679
4014
  if (reservation.state === 'confirmed')
3680
4015
  return { success: true };
3681
4016
  if (reservation.state === 'reconcile') {
4017
+ const variants = ownerIntent.variant === 'image' ? ['image', 'file'] : [ownerIntent.variant];
4018
+ for (const variant of variants) {
4019
+ const receipt = this.telegramGateway.readOutboundDeliveryReceipt?.(idempotencyKey, variant);
4020
+ if (isExactTelegramDeliveryReceipt(receipt, idempotencyKey, variant)) {
4021
+ ownerLedger.confirm(ownerEffect.batchId, ownerEffect.actionKey, 'telegram_send', {
4022
+ version: 1,
4023
+ ...receipt,
4024
+ });
4025
+ return { success: true };
4026
+ }
4027
+ }
3682
4028
  return {
3683
4029
  success: false,
3684
- error: 'owner-event Telegram effect is reconcile-only; automatic replay is disabled',
4030
+ error: 'Telegram delivery outcome is unconfirmed; reconcile the existing delivery before sending again.',
3685
4031
  };
3686
4032
  }
3687
4033
  }
@@ -3895,11 +4241,14 @@ class GatewayToolExecutor {
3895
4241
  async executeWebchatSend(input) {
3896
4242
  const { message, file_path } = input;
3897
4243
  if (!message && !file_path) {
3898
- return { success: false, error: 'Either message or file_path is required' };
4244
+ return {
4245
+ success: false,
4246
+ effectStarted: false,
4247
+ error: 'Either message or file_path is required',
4248
+ };
3899
4249
  }
3900
4250
  try {
3901
4251
  const outboundDir = (0, path_1.join)((0, os_1.homedir)(), '.mama', 'workspace', 'media', 'outbound');
3902
- (0, fs_1.mkdirSync)(outboundDir, { recursive: true });
3903
4252
  if (file_path) {
3904
4253
  // Expand ~ to home directory
3905
4254
  const homeDir = (0, os_1.homedir)();
@@ -3909,7 +4258,7 @@ class GatewayToolExecutor {
3909
4258
  // Check path permission based on role
3910
4259
  const pathPermission = this.checkPathPermission(expandedPath);
3911
4260
  if (!pathPermission.allowed) {
3912
- return { success: false, error: pathPermission.error };
4261
+ return { success: false, effectStarted: false, error: pathPermission.error };
3913
4262
  }
3914
4263
  // Fallback security for contexts without path restrictions:
3915
4264
  // Only allow reading from ~/.mama/ directory
@@ -3922,17 +4271,19 @@ class GatewayToolExecutor {
3922
4271
  if (rel.startsWith('..') || (0, path_1.isAbsolute)(rel)) {
3923
4272
  return {
3924
4273
  success: false,
4274
+ effectStarted: false,
3925
4275
  error: `Access denied: Can only copy files from ${mamaDir}`,
3926
4276
  };
3927
4277
  }
3928
4278
  }
3929
4279
  if (!(0, fs_1.existsSync)(expandedPath)) {
3930
- return { success: false, error: `File not found: ${expandedPath}` };
4280
+ return { success: false, effectStarted: false, error: `File not found: ${expandedPath}` };
3931
4281
  }
3932
4282
  // Copy file to outbound directory with timestamp prefix
3933
4283
  const baseName = (0, path_1.basename)(expandedPath) || 'file';
3934
4284
  const outName = `${Date.now()}_${baseName}`;
3935
4285
  const outPath = (0, path_1.join)(outboundDir, outName);
4286
+ (0, fs_1.mkdirSync)(outboundDir, { recursive: true });
3936
4287
  (0, fs_1.copyFileSync)(expandedPath, outPath);
3937
4288
  const viewerPath = `~/.mama/workspace/media/outbound/${outName}`;
3938
4289
  return {
@@ -3956,10 +4307,8 @@ class GatewayToolExecutor {
3956
4307
  }
3957
4308
  /**
3958
4309
  * schedule_upcoming (M8 P4): read the calendar connector's raw store and
3959
- * return events in [now, now+days] plus a compact text digest. Lazy readonly
3960
- * open (operator-handler pattern); prefers metadata JSON start when present.
3961
- * v1 limits (documented in the tool description): no recurrence expansion,
3962
- * no cancellation tracking.
4310
+ * return intervals overlapping [now, now+days] plus a compact text digest.
4311
+ * Reads a bounded page from the local snapshot and never calls the upstream API.
3963
4312
  */
3964
4313
  scheduleDb = null;
3965
4314
  executeScheduleUpcoming(input) {
@@ -3978,17 +4327,178 @@ class GatewayToolExecutor {
3978
4327
  this.scheduleDb = new sqlite_js_1.default(dbPath);
3979
4328
  this.scheduleDb.prepare('PRAGMA busy_timeout = 5000').get();
3980
4329
  }
3981
- const now = Date.now();
4330
+ let offset = 0;
4331
+ let now = Date.now();
4332
+ let cursorReadVersion;
4333
+ if (input.cursor) {
4334
+ try {
4335
+ const decoded = JSON.parse(Buffer.from(input.cursor, 'base64url').toString('utf8'));
4336
+ if (!Number.isInteger(decoded.offset) ||
4337
+ typeof decoded.offset !== 'number' ||
4338
+ decoded.offset < 1 ||
4339
+ decoded.days !== days ||
4340
+ typeof decoded.now !== 'number' ||
4341
+ typeof decoded.readVersion !== 'string') {
4342
+ throw new Error('invalid fields');
4343
+ }
4344
+ offset = decoded.offset;
4345
+ now = decoded.now;
4346
+ cursorReadVersion = decoded.readVersion;
4347
+ }
4348
+ catch {
4349
+ return { success: false, error: 'Invalid schedule cursor; restart from page one' };
4350
+ }
4351
+ }
3982
4352
  const until = now + days * 86_400_000;
3983
- let rows;
3984
4353
  try {
3985
- rows = this.scheduleDb
3986
- .prepare(`SELECT source_id, channel, content, timestamp, metadata
3987
- FROM raw_items
3988
- WHERE timestamp >= ? AND timestamp <= ?
3989
- ORDER BY timestamp ASC
3990
- LIMIT 50`)
3991
- .all(now, until);
4354
+ const readSnapshot = this.scheduleDb.transaction(() => {
4355
+ const latestRows = this.scheduleDb.prepare(`WITH ranked AS (
4356
+ SELECT id, source_id, channel, content, timestamp, metadata, source_cursor, created_at,
4357
+ ROW_NUMBER() OVER (
4358
+ PARTITION BY COALESCE(json_extract(metadata, '$.eventId'), source_id)
4359
+ ORDER BY CASE
4360
+ WHEN json_extract(metadata, '$.eventId') IS NOT NULL THEN 1
4361
+ ELSE 0
4362
+ END DESC,
4363
+ COALESCE((julianday(source_cursor) - 2440587.5) * 86400000,
4364
+ (julianday(json_extract(metadata, '$.observedAt')) - 2440587.5) * 86400000,
4365
+ created_at) DESC,
4366
+ id DESC
4367
+ ) AS observation_rank
4368
+ FROM raw_items
4369
+ WHERE source = 'calendar'
4370
+ )
4371
+ SELECT id, source_id, channel, content, timestamp, metadata, source_cursor, created_at
4372
+ FROM ranked
4373
+ WHERE observation_rank = 1`).all();
4374
+ const selected = latestRows.flatMap((row) => {
4375
+ let metadata = {};
4376
+ try {
4377
+ metadata = row.metadata ? JSON.parse(row.metadata) : {};
4378
+ }
4379
+ catch {
4380
+ metadata = {};
4381
+ }
4382
+ const metaStart = metadata.start ?? metadata.startTime ?? metadata.start_time;
4383
+ const metaEnd = metadata.end ?? metadata.endTime ?? metadata.end_time;
4384
+ const allDay = metadata.allDay === true;
4385
+ const timeZone = typeof metadata.timeZone === 'string' ? metadata.timeZone : 'UTC';
4386
+ // Contain a bad all-day date to this row: startOfTaskDate throws on an unresolvable value,
4387
+ // and inside flatMap that would abort the whole schedule_upcoming read and clear scheduleDb.
4388
+ // NaN lets the finite-number guard below skip only this row.
4389
+ const dayStart = (value) => {
4390
+ try {
4391
+ return (0, temporal_reconcile_js_1.startOfTaskDate)(value, null, timeZone);
4392
+ }
4393
+ catch {
4394
+ return Number.NaN;
4395
+ }
4396
+ };
4397
+ const startMs = allDay && typeof metaStart === 'string'
4398
+ ? dayStart(metaStart)
4399
+ : typeof metaStart === 'string' || typeof metaStart === 'number'
4400
+ ? new Date(metaStart).getTime()
4401
+ : row.timestamp;
4402
+ const endMs = allDay && typeof metaEnd === 'string'
4403
+ ? dayStart(metaEnd)
4404
+ : typeof metaEnd === 'string' || typeof metaEnd === 'number'
4405
+ ? new Date(metaEnd).getTime()
4406
+ : startMs;
4407
+ if (metadata.status === 'cancelled' ||
4408
+ !Number.isFinite(startMs) ||
4409
+ !Number.isFinite(endMs) ||
4410
+ startMs > until ||
4411
+ endMs <= now) {
4412
+ return [];
4413
+ }
4414
+ const eventId = typeof metadata.eventId === 'string' ? metadata.eventId : row.source_id;
4415
+ const observedAt = typeof row.source_cursor === 'string' && Number.isFinite(Date.parse(row.source_cursor))
4416
+ ? row.source_cursor
4417
+ : typeof metadata.observedAt === 'string' &&
4418
+ Number.isFinite(Date.parse(metadata.observedAt))
4419
+ ? metadata.observedAt
4420
+ : null;
4421
+ const event = {
4422
+ sourceId: eventId,
4423
+ title: typeof metadata.summary === 'string'
4424
+ ? metadata.summary.slice(0, 120)
4425
+ : ((row.content ?? '').split('\n')[0]?.slice(0, 120) ?? ''),
4426
+ start: new Date(Number.isFinite(startMs) ? startMs : row.timestamp).toISOString(),
4427
+ end: typeof metaEnd === 'string' ? metaEnd : null,
4428
+ status: typeof metadata.status === 'string' ? metadata.status : 'unknown',
4429
+ channel: row.channel,
4430
+ ...(allDay && typeof metaStart === 'string' && typeof metaEnd === 'string'
4431
+ ? {
4432
+ allDay: true,
4433
+ startDate: metaStart,
4434
+ endDateExclusive: metaEnd,
4435
+ timeZone,
4436
+ }
4437
+ : {}),
4438
+ };
4439
+ return [{ row, event, observedAt, startMs }];
4440
+ });
4441
+ selected.sort((left, right) => left.startMs - right.startMs || left.event.sourceId.localeCompare(right.event.sourceId));
4442
+ const readVersion = (0, crypto_1.createHash)('sha256')
4443
+ .update(JSON.stringify(selected.map(({ row, event }) => ({
4444
+ id: row.id,
4445
+ sourceId: row.source_id,
4446
+ content: row.content,
4447
+ metadata: row.metadata,
4448
+ sourceCursor: row.source_cursor,
4449
+ timestamp: row.timestamp,
4450
+ event,
4451
+ }))))
4452
+ .digest('hex');
4453
+ const observedTimes = selected
4454
+ .map(({ observedAt }) => (observedAt === null ? Number.NaN : Date.parse(observedAt)))
4455
+ .filter(Number.isFinite);
4456
+ const insertedTimes = selected.map(({ row }) => row.created_at).filter(Number.isFinite);
4457
+ return {
4458
+ total: selected.length,
4459
+ rows: selected.slice(offset, offset + 50),
4460
+ readVersion,
4461
+ observedAt: observedTimes.length > 0 ? Math.max(...observedTimes) : null,
4462
+ insertedAt: insertedTimes.length > 0 ? Math.max(...insertedTimes) : null,
4463
+ };
4464
+ })();
4465
+ if (cursorReadVersion && cursorReadVersion !== readSnapshot.readVersion) {
4466
+ return { success: false, error: 'Schedule snapshot changed; restart from page one' };
4467
+ }
4468
+ const events = readSnapshot.rows.map(({ event }) => event);
4469
+ const nextOffset = offset + events.length;
4470
+ const nextCursor = nextOffset < readSnapshot.total
4471
+ ? Buffer.from(JSON.stringify({
4472
+ offset: nextOffset,
4473
+ days,
4474
+ now,
4475
+ readVersion: readSnapshot.readVersion,
4476
+ }), 'utf8').toString('base64url')
4477
+ : null;
4478
+ const text = events.length === 0
4479
+ ? `(no calendar events in the next ${days} days)`
4480
+ : events
4481
+ .map((event) => `- ${event.start.slice(0, 16)} ${event.title} (${event.channel})`)
4482
+ .join('\n');
4483
+ return {
4484
+ success: true,
4485
+ events,
4486
+ text,
4487
+ total: readSnapshot.total,
4488
+ returned: events.length,
4489
+ nextCursor,
4490
+ observedAt: readSnapshot.observedAt === null ? null : new Date(readSnapshot.observedAt).toISOString(),
4491
+ insertedAt: readSnapshot.insertedAt === null ? null : new Date(readSnapshot.insertedAt).toISOString(),
4492
+ cacheAgeMs: readSnapshot.observedAt === null
4493
+ ? null
4494
+ : Math.max(0, Date.now() - readSnapshot.observedAt),
4495
+ readVersion: readSnapshot.readVersion,
4496
+ coverage: {
4497
+ complete: nextCursor === null,
4498
+ source: 'local_snapshot',
4499
+ upstreamComplete: 'unknown',
4500
+ },
4501
+ };
3992
4502
  }
3993
4503
  catch (err) {
3994
4504
  // Corrupt/locked store or missing table: fail closed with a readable
@@ -3999,30 +4509,6 @@ class GatewayToolExecutor {
3999
4509
  error: `Calendar raw store unreadable: ${err instanceof Error ? err.message : String(err)}`,
4000
4510
  };
4001
4511
  }
4002
- const events = rows.map((row) => {
4003
- let start = row.timestamp;
4004
- try {
4005
- const meta = row.metadata ? JSON.parse(row.metadata) : null;
4006
- const metaStart = meta?.start ?? meta?.startTime ?? meta?.start_time;
4007
- if (typeof metaStart === 'string' || typeof metaStart === 'number') {
4008
- const parsed = new Date(metaStart).getTime();
4009
- if (Number.isFinite(parsed))
4010
- start = parsed;
4011
- }
4012
- }
4013
- catch {
4014
- /* metadata is best-effort */
4015
- }
4016
- return {
4017
- title: (row.content ?? '').split('\n')[0]?.slice(0, 120) ?? '',
4018
- start: new Date(start).toISOString(),
4019
- channel: row.channel,
4020
- };
4021
- });
4022
- const text = events.length === 0
4023
- ? `(no calendar events in the next ${days} days)`
4024
- : events.map((e) => `- ${e.start.slice(0, 16)} ${e.title} (${e.channel})`).join('\n');
4025
- return { success: true, events, text };
4026
4512
  }
4027
4513
  /**
4028
4514
  * Execute Obsidian CLI command on the wiki vault.
@@ -4063,28 +4549,45 @@ class GatewayToolExecutor {
4063
4549
  catch (error) {
4064
4550
  throw new types_js_1.AgentError(error instanceof Error ? error.message : 'Invalid Obsidian CLI arguments', 'TOOL_ERROR', undefined, false);
4065
4551
  }
4066
- try {
4552
+ const invoke = async () => {
4067
4553
  const { stdout } = await execFileAsync('obsidian', cliArgs, {
4068
4554
  timeout: 15000,
4069
4555
  encoding: 'utf-8',
4070
4556
  maxBuffer: 1024 * 1024,
4071
4557
  });
4072
- return {
4073
- success: true,
4074
- data: { output: stdout.trim() },
4075
- };
4558
+ return { success: true, output: stdout.trim() };
4559
+ };
4560
+ const readCommands = new Set([
4561
+ 'read',
4562
+ 'daily:read',
4563
+ 'search',
4564
+ 'search:context',
4565
+ 'files',
4566
+ 'folders',
4567
+ 'vault',
4568
+ 'vaults',
4569
+ 'links',
4570
+ 'backlinks',
4571
+ 'properties',
4572
+ 'property:read',
4573
+ 'aliases',
4574
+ 'tags',
4575
+ ]);
4576
+ try {
4577
+ const result = readCommands.has(command)
4578
+ ? await invoke()
4579
+ : await this.executeOwnerWorkspaceEffect('obsidian', { command, args: args ?? {}, vault: this.obsidianVaultPath }, invoke);
4580
+ return result.success
4581
+ ? { success: true, data: { output: result.output } }
4582
+ : result;
4076
4583
  }
4077
- catch (err) {
4078
- const msg = err instanceof Error ? err.message : String(err);
4079
- if (msg.includes('not enabled') || msg.includes('ENOENT') || msg.includes('not running')) {
4080
- return {
4081
- success: false,
4082
- error: 'Obsidian CLI unavailable (app not running). Use wiki_publish fallback.',
4083
- };
4584
+ catch (error) {
4585
+ if (error instanceof types_js_1.AgentError) {
4586
+ throw error;
4084
4587
  }
4085
4588
  return {
4086
4589
  success: false,
4087
- error: `Obsidian CLI error: ${msg.substring(0, 500)}`,
4590
+ error: 'Obsidian CLI unavailable or command failed; inspect the configured vault before retrying.',
4088
4591
  };
4089
4592
  }
4090
4593
  }
@@ -4461,7 +4964,9 @@ class GatewayToolExecutor {
4461
4964
  // prefix and causeEventIds: the agent never restates what the host knows.
4462
4965
  if (temporalContext && this.taskLedger) {
4463
4966
  const boundTask = this.taskLedger.getById(temporalContext.taskId);
4464
- const rawChannel = boundTask?.sourceChannel ?? null;
4967
+ const rawChannel = (boundTask?.status === 'review'
4968
+ ? (boundTask.reviewAnchorSourceChannel ?? boundTask.sourceChannel)
4969
+ : boundTask?.sourceChannel) ?? null;
4465
4970
  // Trimmed: a whitespace-only event id is truthy but fails ref
4466
4971
  // normalization downstream - which would fail the WHOLE compile,
4467
4972
  // exactly what "strictly additive" forbids.
@@ -4528,7 +5033,7 @@ class GatewayToolExecutor {
4528
5033
  : undefined,
4529
5034
  });
4530
5035
  if (temporalContext &&
4531
- !temporalPacketRawSourcesWithinBoundSource(temporalContext, result.packet.source_refs, reviewWindow)) {
5036
+ !temporalPacketRawSourcesWithinBoundSource(temporalContext, result.packet.source_refs, reviewWindow, this.temporalCorroborationVisible)) {
4532
5037
  throw new Error('context_compile packet exceeds the active temporal task source');
4533
5038
  }
4534
5039
  if (wikiAuthority) {