@jungjaehoon/mama-os 0.49.3 → 0.50.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 (135) 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 +39 -7
  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 +17 -6
  12. package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
  13. package/dist/agent/gateway-tool-executor.js +512 -143
  14. package/dist/agent/gateway-tool-executor.js.map +1 -1
  15. package/dist/agent/gateway-tools.md +7 -6
  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 +15 -8
  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/report-handler.d.ts +19 -3
  30. package/dist/api/report-handler.d.ts.map +1 -1
  31. package/dist/api/report-handler.js +123 -17
  32. package/dist/api/report-handler.js.map +1 -1
  33. package/dist/api/report-persistence.d.ts +3 -3
  34. package/dist/api/report-persistence.d.ts.map +1 -1
  35. package/dist/api/report-persistence.js +11 -21
  36. package/dist/api/report-persistence.js.map +1 -1
  37. package/dist/cli/commands/start.d.ts +4 -0
  38. package/dist/cli/commands/start.d.ts.map +1 -1
  39. package/dist/cli/commands/start.js +49 -157
  40. package/dist/cli/commands/start.js.map +1 -1
  41. package/dist/cli/runtime/agent-loop-init.d.ts.map +1 -1
  42. package/dist/cli/runtime/agent-loop-init.js +1 -0
  43. package/dist/cli/runtime/agent-loop-init.js.map +1 -1
  44. package/dist/cli/runtime/api-routes-init.d.ts +5 -19
  45. package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
  46. package/dist/cli/runtime/api-routes-init.js +7 -79
  47. package/dist/cli/runtime/api-routes-init.js.map +1 -1
  48. package/dist/db/migrations/operator-owner-action-candidates.d.ts +19 -0
  49. package/dist/db/migrations/operator-owner-action-candidates.d.ts.map +1 -0
  50. package/dist/db/migrations/operator-owner-action-candidates.js +254 -0
  51. package/dist/db/migrations/operator-owner-action-candidates.js.map +1 -0
  52. package/dist/db/migrations/operator-task-external-lifecycle.d.ts +9 -0
  53. package/dist/db/migrations/operator-task-external-lifecycle.d.ts.map +1 -1
  54. package/dist/db/migrations/operator-task-external-lifecycle.js +52 -22
  55. package/dist/db/migrations/operator-task-external-lifecycle.js.map +1 -1
  56. package/dist/db/migrations/operator-task-review-lifecycle.d.ts.map +1 -1
  57. package/dist/db/migrations/operator-task-review-lifecycle.js +3 -0
  58. package/dist/db/migrations/operator-task-review-lifecycle.js.map +1 -1
  59. package/dist/db/migrations/owner-action-effects.d.ts +18 -0
  60. package/dist/db/migrations/owner-action-effects.d.ts.map +1 -0
  61. package/dist/db/migrations/owner-action-effects.js +111 -0
  62. package/dist/db/migrations/owner-action-effects.js.map +1 -0
  63. package/dist/gateways/message-router.d.ts.map +1 -1
  64. package/dist/gateways/message-router.js +14 -4
  65. package/dist/gateways/message-router.js.map +1 -1
  66. package/dist/gateways/telegram-text-entities.d.ts +99 -0
  67. package/dist/gateways/telegram-text-entities.d.ts.map +1 -0
  68. package/dist/gateways/telegram-text-entities.js +189 -0
  69. package/dist/gateways/telegram-text-entities.js.map +1 -0
  70. package/dist/gateways/telegram.d.ts.map +1 -1
  71. package/dist/gateways/telegram.js +13 -2
  72. package/dist/gateways/telegram.js.map +1 -1
  73. package/dist/gateways/types.d.ts +8 -0
  74. package/dist/gateways/types.d.ts.map +1 -1
  75. package/dist/operator/board-read-views.d.ts +9 -4
  76. package/dist/operator/board-read-views.d.ts.map +1 -1
  77. package/dist/operator/board-read-views.js +11 -0
  78. package/dist/operator/board-read-views.js.map +1 -1
  79. package/dist/operator/external-lifecycle-candidates.d.ts +15 -1
  80. package/dist/operator/external-lifecycle-candidates.d.ts.map +1 -1
  81. package/dist/operator/external-lifecycle-candidates.js +164 -0
  82. package/dist/operator/external-lifecycle-candidates.js.map +1 -1
  83. package/dist/operator/external-lifecycle-discovery.d.ts +61 -0
  84. package/dist/operator/external-lifecycle-discovery.d.ts.map +1 -0
  85. package/dist/operator/external-lifecycle-discovery.js +103 -0
  86. package/dist/operator/external-lifecycle-discovery.js.map +1 -0
  87. package/dist/operator/external-lifecycle.d.ts +23 -0
  88. package/dist/operator/external-lifecycle.d.ts.map +1 -1
  89. package/dist/operator/owner-action-effects.d.ts +101 -0
  90. package/dist/operator/owner-action-effects.d.ts.map +1 -0
  91. package/dist/operator/owner-action-effects.js +335 -0
  92. package/dist/operator/owner-action-effects.js.map +1 -0
  93. package/dist/operator/owner-event-inbox.d.ts +1 -0
  94. package/dist/operator/owner-event-inbox.d.ts.map +1 -1
  95. package/dist/operator/owner-event-inbox.js +6 -0
  96. package/dist/operator/owner-event-inbox.js.map +1 -1
  97. package/dist/operator/owner-event-loop.d.ts +3 -0
  98. package/dist/operator/owner-event-loop.d.ts.map +1 -1
  99. package/dist/operator/owner-event-loop.js +34 -0
  100. package/dist/operator/owner-event-loop.js.map +1 -1
  101. package/dist/operator/owner-event-outcome.d.ts.map +1 -1
  102. package/dist/operator/owner-event-outcome.js +6 -0
  103. package/dist/operator/owner-event-outcome.js.map +1 -1
  104. package/dist/operator/owner-event-policy.d.ts.map +1 -1
  105. package/dist/operator/owner-event-policy.js +2 -26
  106. package/dist/operator/owner-event-policy.js.map +1 -1
  107. package/dist/operator/owner-event-prompt.d.ts.map +1 -1
  108. package/dist/operator/owner-event-prompt.js +2 -5
  109. package/dist/operator/owner-event-prompt.js.map +1 -1
  110. package/dist/operator/task-ledger.d.ts +93 -14
  111. package/dist/operator/task-ledger.d.ts.map +1 -1
  112. package/dist/operator/task-ledger.js +430 -103
  113. package/dist/operator/task-ledger.js.map +1 -1
  114. package/dist/operator/temporal-reconcile.d.ts.map +1 -1
  115. package/dist/operator/temporal-reconcile.js +6 -2
  116. package/dist/operator/temporal-reconcile.js.map +1 -1
  117. package/dist/operator/workorder-consumer.d.ts +2 -0
  118. package/dist/operator/workorder-consumer.d.ts.map +1 -1
  119. package/dist/operator/workorder-consumer.js +20 -5
  120. package/dist/operator/workorder-consumer.js.map +1 -1
  121. package/dist/scheduler/heartbeat.d.ts.map +1 -1
  122. package/dist/scheduler/heartbeat.js +1 -0
  123. package/dist/scheduler/heartbeat.js.map +1 -1
  124. package/dist/sqlite.d.ts +1 -1
  125. package/dist/sqlite.d.ts.map +1 -1
  126. package/dist/sqlite.js +3 -2
  127. package/dist/sqlite.js.map +1 -1
  128. package/dist/wiki/wiki-read.d.ts +2 -1
  129. package/dist/wiki/wiki-read.d.ts.map +1 -1
  130. package/dist/wiki/wiki-read.js +13 -2
  131. package/dist/wiki/wiki-read.js.map +1 -1
  132. package/dist/wiki/wiki-turn-contract.js +1 -1
  133. package/dist/wiki/wiki-turn-contract.js.map +1 -1
  134. package/package.json +1 -1
  135. package/public/viewer/operator/operator.js +3 -1
@@ -87,6 +87,8 @@ 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
89
  const board_read_views_js_1 = require("../operator/board-read-views.js");
90
+ const owner_action_effects_js_1 = require("../operator/owner-action-effects.js");
91
+ const external_lifecycle_discovery_js_1 = require("../operator/external-lifecycle-discovery.js");
90
92
  const read_js_1 = require("../evidence/read.js");
91
93
  const private_connector_policy_js_1 = require("../connectors/private-connector-policy.js");
92
94
  const member_candidate_store_js_1 = require("../gateways/member-candidate-store.js");
@@ -113,7 +115,7 @@ function bindTemporalContextPacketTask(context, task) {
113
115
  function temporalIdentifierRef(value) {
114
116
  return `sha256:${(0, crypto_1.createHash)('sha256').update(value).digest('hex')}`;
115
117
  }
116
- function temporalPacketRawSourcesWithinBoundSource(context, sourceRefs, reviewWindow) {
118
+ function temporalPacketRawSourcesWithinBoundSource(context, sourceRefs, reviewWindow, additionalVisible = () => false) {
117
119
  const rawRefs = sourceRefs.filter((value) => typeof value === 'object' &&
118
120
  value !== null &&
119
121
  !Array.isArray(value) &&
@@ -121,29 +123,23 @@ function temporalPacketRawSourcesWithinBoundSource(context, sourceRefs, reviewWi
121
123
  if (!context.sourceChannel) {
122
124
  return rawRefs.length === 0;
123
125
  }
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))));
126
+ const channelMatches = (ref) => typeof ref.connector === 'string' &&
127
+ typeof ref.channel_id === 'string' &&
128
+ temporalIdentifierRef(`${ref.connector}:${ref.channel_id}`) === context.sourceChannel;
129
+ const anchorMatches = (ref) => channelMatches(ref) &&
130
+ Boolean(context.sourceEventId &&
131
+ [ref.raw_id, ref.source_id].some((value) => typeof value === 'string' && temporalIdentifierRef(value) === context.sourceEventId));
132
+ return (rawRefs.every((ref) => (channelMatches(ref) &&
133
+ (reviewWindow !== undefined || !context.sourceEventId || anchorMatches(ref))) ||
134
+ additionalVisible(ref)) &&
135
+ ((rawRefs.length === 0 && reviewWindow === undefined) ||
136
+ !context.sourceEventId ||
137
+ rawRefs.some(anchorMatches)));
136
138
  }
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);
139
+ function temporalPacketReferencesBoundSource(context, sourceRefs, reviewWindow, additionalVisible) {
140
+ const hasRaw = sourceRefs.some((ref) => typeof ref === 'object' && ref !== null && ref.kind === 'raw');
141
+ return ((!(context.sourceEventId || context.sourceChannel) || hasRaw) &&
142
+ temporalPacketRawSourcesWithinBoundSource(context, sourceRefs, reviewWindow, additionalVisible));
147
143
  }
148
144
  function temporalPacketHasExactReviewWindow(packetJson, reviewWindow) {
149
145
  try {
@@ -232,6 +228,7 @@ const TASK_UPDATE_PUBLIC_FIELD_SET = new Set(TASK_UPDATE_PUBLIC_FIELDS);
232
228
  */
233
229
  const TASK_CREATE_PUBLIC_FIELDS = [
234
230
  'title',
231
+ 'creation_key',
235
232
  'completion_criteria',
236
233
  'status',
237
234
  'priority',
@@ -501,6 +498,7 @@ class GatewayToolExecutor {
501
498
  telegramGateway = null;
502
499
  roleManager;
503
500
  privateConnectorPolicy;
501
+ connectorEventAdapter;
504
502
  executionContextStorage = new async_hooks_1.AsyncLocalStorage();
505
503
  envelopeEnforcer = new index_js_1.EnvelopeEnforcer();
506
504
  channelGrantProvider;
@@ -539,6 +537,95 @@ class GatewayToolExecutor {
539
537
  }
540
538
  taskLedger = null;
541
539
  ownerEventEffectLedger = null;
540
+ ownerActionEffectLedger = null;
541
+ createNativeEffectObserver(state) {
542
+ const ledger = this.ownerActionEffectLedger;
543
+ if (!ledger || !state?.envelope || !state.modelRunId || state.memberScopeRequired) {
544
+ return undefined;
545
+ }
546
+ const attempt = state.workorderAttemptId === undefined
547
+ ? null
548
+ : this.taskLedger?.getWorkOrderById(state.workorderAttemptId);
549
+ const occurrenceKey = attempt ? `workorder:${attempt.idempotencyKey}` : state.sourceMessageRef;
550
+ if (!occurrenceKey) {
551
+ throw new Error('Native effect observation needs an owner occurrence');
552
+ }
553
+ const context = {
554
+ ownerScope: state.agentContext?.principalId ?? 'owner:runtime',
555
+ occurrenceKey,
556
+ modelRunId: state.modelRunId,
557
+ envelopeHash: state.envelope.envelope_hash,
558
+ ...(attempt ? { workOrderAttemptId: attempt.id } : {}),
559
+ };
560
+ if (ledger.hasUnsafeReplayEffects(occurrenceKey)) {
561
+ throw new types_js_1.AgentError('Owner occurrence needs effect reconciliation before replay', 'CODE_ACT_MUTATION_OUTCOME_UNKNOWN', undefined, false);
562
+ }
563
+ const admissionKey = `native-run:${(0, crypto_1.createHash)('sha256').update(context.modelRunId).digest('hex')}`;
564
+ ledger.begin(context, admissionKey, 'native_run', { admitted: true });
565
+ let sequence = 0;
566
+ const pending = new Map();
567
+ return {
568
+ started: (name, input) => {
569
+ const id = typeof input.nativeToolUseId === 'string'
570
+ ? input.nativeToolUseId
571
+ : `observed:${++sequence}`;
572
+ const key = `native:${(0, crypto_1.createHash)('sha256').update(`${context.modelRunId}:${id}`).digest('hex')}`;
573
+ const reservation = ledger.begin(context, key, 'native_tool', { toolName: name });
574
+ if (reservation.state !== 'execute') {
575
+ throw new types_js_1.AgentError('Native effect already observed', 'CODE_ACT_MUTATION_OUTCOME_UNKNOWN', undefined, false);
576
+ }
577
+ pending.set(id, { key, name });
578
+ },
579
+ settled: (name, id, isError) => {
580
+ const match = pending.get(id) ?? [...pending.values()].find((entry) => entry.name === name);
581
+ if (!match) {
582
+ // Completion may be the first provider observation after reconnect.
583
+ const key = `native:${(0, crypto_1.createHash)('sha256').update(`${context.modelRunId}:${id}`).digest('hex')}`;
584
+ ledger.begin(context, key, 'native_tool', { toolName: name });
585
+ ledger.markUnknown(context, key, 'native_tool', 'completion observed without matching start');
586
+ return;
587
+ }
588
+ if (isError) {
589
+ ledger.markUnknown(context, match.key, 'native_tool', 'native tool reported failure');
590
+ }
591
+ else {
592
+ ledger.confirm(context, match.key, 'native_tool', { success: true });
593
+ }
594
+ for (const [pendingId, entry] of pending) {
595
+ if (entry === match) {
596
+ pending.delete(pendingId);
597
+ }
598
+ }
599
+ },
600
+ finished: () => {
601
+ if (pending.size > 0) {
602
+ throw new types_js_1.AgentError('Native effects did not settle before final response', 'CODE_ACT_MUTATION_OUTCOME_UNKNOWN', undefined, false);
603
+ }
604
+ ledger.confirm(context, admissionKey, 'native_run', { completed: true });
605
+ },
606
+ interrupted: () => {
607
+ ledger.markUnknown(context, admissionKey, 'native_run', 'native turn did not finish cleanly');
608
+ for (const { key } of pending.values()) {
609
+ ledger.markUnknown(context, key, 'native_tool', 'native run interrupted');
610
+ }
611
+ },
612
+ };
613
+ }
614
+ setOwnerActionEffectLedger(ledger) {
615
+ this.ownerActionEffectLedger = ledger;
616
+ }
617
+ ownerEffectPort(context) {
618
+ const ledger = this.ownerActionEffectLedger;
619
+ if (!ledger) {
620
+ throw new Error('Owner action effect ledger is not configured');
621
+ }
622
+ return {
623
+ begin: (_batch, key, kind, intent) => ledger.begin(context, key, kind, intent),
624
+ inspect: (_batch, key, kind) => ledger.inspect(context, key, kind),
625
+ confirm: (_batch, key, kind, result) => ledger.confirm(context, key, kind, result),
626
+ markUnknown: (_batch, key, kind, error) => ledger.markUnknown(context, key, kind, error),
627
+ };
628
+ }
542
629
  setTaskLedger(ledger) {
543
630
  this.taskLedger = ledger;
544
631
  }
@@ -765,7 +852,10 @@ class GatewayToolExecutor {
765
852
  constructor(options = {}) {
766
853
  this.channelGrantProvider = options.channelGrantProvider ?? read_js_1.liveBoundaryChannels;
767
854
  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 });
855
+ this.driveTools = new drive_tools_js_1.DriveToolService({
856
+ workspaceRoot: privateWorkspaceRoot,
857
+ runGws: options.driveGwsRunner,
858
+ });
769
859
  this.imageTranslationTools = new image_translation_tools_js_1.ImageTranslationToolService({
770
860
  workspaceRoot: privateWorkspaceRoot,
771
861
  });
@@ -794,6 +884,7 @@ class GatewayToolExecutor {
794
884
  this.roleManager = (0, role_manager_js_1.getRoleManager)(options.rolesConfig ? { rolesConfig: options.rolesConfig } : undefined);
795
885
  this.privateConnectorPolicy =
796
886
  options.privateConnectorPolicy ?? DEFAULT_PRIVATE_CONNECTOR_POLICY;
887
+ this.connectorEventAdapter = options.connectorEventAdapter;
797
888
  if (options.mamaApi) {
798
889
  this.mamaApi = options.mamaApi;
799
890
  }
@@ -1336,6 +1427,75 @@ class GatewayToolExecutor {
1336
1427
  }
1337
1428
  return modelRunId;
1338
1429
  }
1430
+ async requireOwnerActionContext() {
1431
+ const state = this.getExecutionState();
1432
+ const envelope = state.envelope;
1433
+ if (!envelope || !state.modelRunId || state.memberScopeRequired) {
1434
+ throw new types_js_1.AgentError('Owner action requires the current authenticated owner envelope and model run', 'WORKORDER_SUPERSEDED', undefined, false);
1435
+ }
1436
+ const api = await this.initializeMAMAApi();
1437
+ const run = await api.getModelRun?.(state.modelRunId);
1438
+ if (!run || run.status !== 'running' || run.envelope_hash !== envelope.envelope_hash) {
1439
+ throw new Error('Owner action model run is no longer current for this envelope');
1440
+ }
1441
+ const attempt = state.workorderAttemptId === undefined
1442
+ ? null
1443
+ : this.taskLedger?.getWorkOrderById(state.workorderAttemptId);
1444
+ if (state.workorderAttemptId !== undefined &&
1445
+ (!attempt ||
1446
+ attempt.status !== 'in_progress' ||
1447
+ run.input_refs?.workorderAttemptId !== attempt.id)) {
1448
+ throw new Error('Owner action workorder attempt is no longer active');
1449
+ }
1450
+ const occurrenceKey = attempt ? `workorder:${attempt.idempotencyKey}` : state.sourceMessageRef;
1451
+ if (!occurrenceKey) {
1452
+ throw new Error('Owner action is missing its host-issued occurrence identity');
1453
+ }
1454
+ return {
1455
+ ownerScope: state.agentContext?.principalId ?? 'owner:runtime',
1456
+ occurrenceKey,
1457
+ modelRunId: state.modelRunId,
1458
+ envelopeHash: envelope.envelope_hash,
1459
+ ...(attempt ? { workOrderAttemptId: attempt.id } : {}),
1460
+ };
1461
+ }
1462
+ temporalCorroborationVisible = (ref) => {
1463
+ if (typeof ref.connector !== 'string' || typeof ref.channel_id !== 'string') {
1464
+ return false;
1465
+ }
1466
+ return this.currentOwnerPartitionVisibility()({
1467
+ connector: ref.connector,
1468
+ channel: ref.channel_id,
1469
+ });
1470
+ };
1471
+ currentOwnerPartitionVisibility() {
1472
+ const state = this.getExecutionState();
1473
+ if (!state.envelope || state.memberScopeRequired) {
1474
+ throw new Error('Owner source visibility requires an owner envelope');
1475
+ }
1476
+ const channels = (0, read_js_1.narrowGrantToEnvelope)(snapshotChannelGrant(this.channelGrantProvider), {
1477
+ connectors: state.envelope.scope.raw_connectors,
1478
+ scopes: state.envelope.scope.memory_scopes,
1479
+ });
1480
+ return ({ connector, channel }) => channels[connector]?.includes(channel) === true;
1481
+ }
1482
+ async externalCandidateSource(candidateId) {
1483
+ const attemptId = this.getExecutionState().workorderAttemptId;
1484
+ if (attemptId !== undefined &&
1485
+ this.taskLedger?.getWorkOrderById(attemptId)?.workKind === 'board') {
1486
+ await this.requireExternalLifecycleModelRun(attemptId);
1487
+ if (this.getExecutionState().envelope) {
1488
+ const context = await this.requireOwnerActionContext();
1489
+ if (this.taskLedger
1490
+ .listOwnerActionCandidates(context)
1491
+ .some((candidate) => candidate.candidateId === candidateId)) {
1492
+ return { kind: 'owner_run', context };
1493
+ }
1494
+ }
1495
+ return { kind: 'board', attemptId };
1496
+ }
1497
+ return { kind: 'owner_run', context: await this.requireOwnerActionContext() };
1498
+ }
1339
1499
  async beginTraceIfNeeded(ctx, gatewayCallId) {
1340
1500
  if (ctx.modelRunId) {
1341
1501
  const api = await this.initializeMAMAApi();
@@ -1878,9 +2038,9 @@ class GatewayToolExecutor {
1878
2038
  case 'Bash':
1879
2039
  return await this.executeBash(input);
1880
2040
  case 'discord_send':
1881
- return await this.executeDiscordSend(input);
2041
+ return await this.executeOwnerWorkspaceEffect('discord_send', input, () => this.executeDiscordSend(input));
1882
2042
  case 'slack_send':
1883
- return await this.executeSlackSend(input);
2043
+ return await this.executeOwnerWorkspaceEffect('slack_send', input, () => this.executeSlackSend(input));
1884
2044
  case 'telegram_send':
1885
2045
  return await this.executeTelegramSend(input);
1886
2046
  case 'ocr_image': {
@@ -2106,16 +2266,35 @@ class GatewayToolExecutor {
2106
2266
  const uploadInput = input;
2107
2267
  const state = this.getExecutionState();
2108
2268
  const sourceMessageRef = state.sourceMessageRef;
2109
- if (!sourceMessageRef?.startsWith('owner-event:')) {
2269
+ const legacyOwnerEvent = sourceMessageRef?.startsWith('owner-event:') === true;
2270
+ if (!legacyOwnerEvent && !this.ownerActionEffectLedger) {
2110
2271
  return {
2111
2272
  success: true,
2112
2273
  result: (0, drive_tools_js_1.asUntrustedDriveEvidence)(await this.driveTools.upload(uploadInput)),
2113
2274
  };
2114
2275
  }
2115
- const effect = this.requireOwnerEventEffect(uploadInput.effect_key, 'drive_upload');
2116
- const ledger = this.requireOwnerEventEffectLedger();
2117
- const occurrence = `${sourceMessageRef}:drive:${effect.actionKey}`;
2276
+ const requestSha256 = (0, crypto_1.createHash)('sha256')
2277
+ .update(JSON.stringify([
2278
+ uploadInput.localPath,
2279
+ uploadInput.folderId,
2280
+ uploadInput.fileName ?? null,
2281
+ uploadInput.destinationCapability ?? null,
2282
+ ]))
2283
+ .digest('hex');
2284
+ const context = legacyOwnerEvent ? null : await this.requireOwnerActionContext();
2285
+ const effect = legacyOwnerEvent
2286
+ ? this.requireOwnerEventEffect(uploadInput.effect_key, 'drive_upload')
2287
+ : { batchId: 0, actionKey: `drive:${uploadInput.effect_key ?? requestSha256}` };
2288
+ const ledger = context
2289
+ ? this.ownerEffectPort(context)
2290
+ : this.requireOwnerEventEffectLedger();
2291
+ const occurrence = context
2292
+ ? `${context.ownerScope}:${context.occurrenceKey}:drive:${effect.actionKey}`
2293
+ : `${sourceMessageRef}:drive:${effect.actionKey}`;
2118
2294
  let reservation = ledger.inspect(effect.batchId, effect.actionKey, 'drive_upload');
2295
+ if (context && reservation && reservation.intent.requestSha256 !== requestSha256) {
2296
+ throw new Error('Drive action key was already reserved for a different upload request');
2297
+ }
2119
2298
  if (reservation?.state === 'confirmed') {
2120
2299
  if (!reservation.result) {
2121
2300
  throw new Error('Confirmed Drive upload is missing its durable result');
@@ -2129,6 +2308,7 @@ class GatewayToolExecutor {
2129
2308
  reservation = ledger.begin(effect.batchId, effect.actionKey, 'drive_upload', {
2130
2309
  folderId: preparation.prepared.folderId,
2131
2310
  fileName: preparation.prepared.fileName,
2311
+ ...(context ? { requestSha256 } : {}),
2132
2312
  });
2133
2313
  if (reservation.state === 'confirmed') {
2134
2314
  if (!reservation.result) {
@@ -2173,8 +2353,7 @@ class GatewayToolExecutor {
2173
2353
  case 'os_get_config':
2174
2354
  return await this.executeGetConfig(input);
2175
2355
  case 'webchat_send':
2176
- return await this.executeWebchatSend(input // session_id omitted: all files use shared outbound dir
2177
- );
2356
+ return await this.executeOwnerWorkspaceEffect('webchat_send', input, () => this.executeWebchatSend(input));
2178
2357
  // Code-Act sandbox execution
2179
2358
  case 'code_act':
2180
2359
  return await this.executeCodeAct(input);
@@ -2296,16 +2475,20 @@ class GatewayToolExecutor {
2296
2475
  if (!slotsInput || typeof slotsInput !== 'object') {
2297
2476
  throw new types_js_1.AgentError('report_publish requires slots object', 'TOOL_ERROR', undefined, false);
2298
2477
  }
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
- }
2478
+ const basisRevision = input.basis_revision;
2479
+ if (basisRevision !== undefined &&
2480
+ basisRevision !== null &&
2481
+ (typeof basisRevision !== 'string' ||
2482
+ !basisRevision.trim() ||
2483
+ basisRevision !== basisRevision.trim())) {
2484
+ throw new types_js_1.AgentError('report_publish basis_revision must be a canonical task basis from board_read', 'TOOL_ERROR', undefined, false);
2306
2485
  }
2307
2486
  if (this.reportPublisher) {
2308
- const publication = this.reportPublisher(slotsInput);
2487
+ const publication = basisRevision === undefined
2488
+ ? this.reportPublisher(slotsInput)
2489
+ : this.reportPublisher(slotsInput, {
2490
+ basisRevision: basisRevision,
2491
+ });
2309
2492
  // Backward compatibility: older injected publishers return void
2310
2493
  // or the exact changed slot array. Production distinguishes slots
2311
2494
  // accepted as present from slots whose HTML actually changed.
@@ -2632,13 +2815,23 @@ class GatewayToolExecutor {
2632
2815
  }
2633
2816
  case 'wiki_read': {
2634
2817
  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
2818
  if (!this.obsidianVaultPath) {
2639
2819
  throw new types_js_1.AgentError('Wiki vault path not configured', 'TOOL_ERROR', undefined, false);
2640
2820
  }
2641
2821
  try {
2822
+ if (!wikiAuthority) {
2823
+ const readInput = input;
2824
+ return {
2825
+ success: true,
2826
+ ...(0, wiki_read_js_1.readWikiPages)({
2827
+ root: this.obsidianVaultPath,
2828
+ paths: readInput.paths,
2829
+ contentOffset: readInput.content_offset,
2830
+ contentLimit: readInput.content_limit,
2831
+ contentVersions: readInput.content_versions,
2832
+ }),
2833
+ };
2834
+ }
2642
2835
  const attemptId = this.getExecutionState().workorderAttemptId;
2643
2836
  if (!Number.isSafeInteger(attemptId) || attemptId < 1) {
2644
2837
  throw new Error('wiki_read requires a host-issued workorder attempt');
@@ -2650,7 +2843,7 @@ class GatewayToolExecutor {
2650
2843
  }
2651
2844
  const paths = Array.isArray(readInput.paths) ? readInput.paths : [];
2652
2845
  for (const rawPath of paths) {
2653
- const path = (0, wiki_read_js_1.assertAllowedWikiWorkorderPath)(rawPath, wikiAuthority.ownerDate);
2846
+ const path = (0, wiki_read_js_1.normalizeWikiRelativePath)(rawPath);
2654
2847
  const key = this.wikiReadCoverageKey(attemptId, path);
2655
2848
  const prior = this.wikiReadCoverage.get(key);
2656
2849
  if (requestedOffset === 0) {
@@ -2662,10 +2855,17 @@ class GatewayToolExecutor {
2662
2855
  }
2663
2856
  const result = (0, wiki_read_js_1.readWikiPages)({
2664
2857
  root: this.obsidianVaultPath,
2665
- ownerDate: wikiAuthority.ownerDate,
2666
2858
  paths: readInput.paths,
2667
2859
  contentOffset: readInput.content_offset,
2668
2860
  contentLimit: readInput.content_limit,
2861
+ contentVersions: Object.fromEntries(paths.map((rawPath) => {
2862
+ const path = (0, wiki_read_js_1.normalizeWikiRelativePath)(rawPath);
2863
+ return [
2864
+ path,
2865
+ this.wikiReadCoverage.get(this.wikiReadCoverageKey(attemptId, path))
2866
+ ?.contentVersion ?? null,
2867
+ ];
2868
+ })),
2669
2869
  });
2670
2870
  for (const page of result.pages) {
2671
2871
  const key = this.wikiReadCoverageKey(attemptId, page.path);
@@ -2941,32 +3141,62 @@ class GatewayToolExecutor {
2941
3141
  };
2942
3142
  }
2943
3143
  }
3144
+ case 'task_external_candidates': {
3145
+ if (!this.taskLedger) {
3146
+ throw new Error('Task ledger not configured');
3147
+ }
3148
+ const raw = input;
3149
+ if (!Array.isArray(raw.event_ids) ||
3150
+ raw.event_ids.length < 1 ||
3151
+ raw.event_ids.length > 20 ||
3152
+ raw.event_ids.some((id) => typeof id !== 'string' || !id.trim() || id !== id.trim())) {
3153
+ throw new Error('task_external_candidates requires 1-20 observed event_ids');
3154
+ }
3155
+ const context = await this.requireOwnerActionContext();
3156
+ const adapter = this.connectorEventAdapter ?? (await getContextPacketLookupAdapter());
3157
+ const result = (0, external_lifecycle_discovery_js_1.attestOwnerExternalLifecycleCandidates)({
3158
+ eventIds: raw.event_ids,
3159
+ context,
3160
+ ledger: this.taskLedger,
3161
+ getAdapter: () => adapter,
3162
+ privateConnectorPolicy: this.privateConnectorPolicy,
3163
+ rawConnectorScope: this.getExecutionState().envelope.scope.raw_connectors,
3164
+ isPartitionVisible: this.currentOwnerPartitionVisibility(),
3165
+ });
3166
+ return { success: true, ...result };
3167
+ }
2944
3168
  case 'task_external_bind': {
2945
3169
  if (!this.taskLedger) {
2946
3170
  return { success: false, error: 'Task ledger not configured' };
2947
3171
  }
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
3172
  try {
2954
3173
  (0, external_lifecycle_candidates_js_1.validateExternalLifecycleDecision)('binding', input);
2955
3174
  }
2956
3175
  catch (error) {
2957
3176
  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
3177
  }
2959
- const modelRunId = await this.requireExternalLifecycleModelRun(attemptId);
3178
+ const source = await this.externalCandidateSource(input.candidate_id);
3179
+ const attemptId = source.kind === 'board' ? source.attemptId : source.context.workOrderAttemptId;
3180
+ const modelRunId = source.kind === 'owner_run'
3181
+ ? source.context.modelRunId
3182
+ : this.getExecutionState().modelRunId;
2960
3183
  let candidate;
2961
3184
  try {
2962
- candidate = this.taskLedger.loadBoardCandidate(attemptId, input.candidate_id, 'binding');
3185
+ candidate = this.taskLedger.loadExternalCandidate(source, input.candidate_id, 'binding');
2963
3186
  }
2964
3187
  catch (error) {
2965
3188
  throw new types_js_1.AgentError(error instanceof Error
2966
3189
  ? error.message
2967
3190
  : 'task_external_bind candidate is unavailable', 'WORKORDER_SUPERSEDED', error instanceof Error ? error : undefined, false);
2968
3191
  }
2969
- const receipt = this.taskLedger.applyExternalBindingDecision(attemptId, input, {
3192
+ if (this.getExecutionState().envelope &&
3193
+ !this.currentOwnerPartitionVisibility()({
3194
+ connector: candidate.connector,
3195
+ channel: candidate.channelPartition,
3196
+ })) {
3197
+ throw new Error('External candidate source is no longer visible under the current owner grant');
3198
+ }
3199
+ const receipt = this.taskLedger.applyExternalBindingDecision(source, input, {
2970
3200
  runId: modelRunId,
2971
3201
  workOrderAttemptId: attemptId,
2972
3202
  causeEventIds: [candidate.eventId],
@@ -2984,28 +3214,34 @@ class GatewayToolExecutor {
2984
3214
  if (!this.taskLedger) {
2985
3215
  return { success: false, error: 'Task ledger not configured' };
2986
3216
  }
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
3217
  try {
2993
3218
  (0, external_lifecycle_candidates_js_1.validateExternalLifecycleDecision)('lifecycle', input);
2994
3219
  }
2995
3220
  catch (error) {
2996
3221
  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
3222
  }
2998
- const modelRunId = await this.requireExternalLifecycleModelRun(attemptId);
3223
+ const source = await this.externalCandidateSource(input.candidate_id);
3224
+ const attemptId = source.kind === 'board' ? source.attemptId : source.context.workOrderAttemptId;
3225
+ const modelRunId = source.kind === 'owner_run'
3226
+ ? source.context.modelRunId
3227
+ : this.getExecutionState().modelRunId;
2999
3228
  let candidate;
3000
3229
  try {
3001
- candidate = this.taskLedger.loadBoardCandidate(attemptId, input.candidate_id, 'lifecycle');
3230
+ candidate = this.taskLedger.loadExternalCandidate(source, input.candidate_id, 'lifecycle');
3002
3231
  }
3003
3232
  catch (error) {
3004
3233
  throw new types_js_1.AgentError(error instanceof Error
3005
3234
  ? error.message
3006
3235
  : 'task_lifecycle_reconcile candidate is unavailable', 'WORKORDER_SUPERSEDED', error instanceof Error ? error : undefined, false);
3007
3236
  }
3008
- const receipt = this.taskLedger.applyExternalLifecycleDecision(attemptId, input, {
3237
+ if (this.getExecutionState().envelope &&
3238
+ !this.currentOwnerPartitionVisibility()({
3239
+ connector: candidate.connector,
3240
+ channel: candidate.channelPartition,
3241
+ })) {
3242
+ throw new Error('External candidate source is no longer visible under the current owner grant');
3243
+ }
3244
+ const receipt = this.taskLedger.applyExternalLifecycleDecision(source, input, {
3009
3245
  runId: modelRunId,
3010
3246
  workOrderAttemptId: attemptId,
3011
3247
  causeEventIds: [candidate.eventId],
@@ -3041,20 +3277,29 @@ class GatewayToolExecutor {
3041
3277
  if (completionCriteria.trim().length > task_ledger_js_1.TASK_COMPLETION_CRITERIA_MAX_LENGTH) {
3042
3278
  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
3279
  }
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
- };
3280
+ const { creation_key: creationKey, ...taskInput } = rawTaskCreate;
3281
+ const create = () => this.taskLedger.create(taskInput, {
3282
+ runId: this.getExecutionState().modelRunId ?? null,
3283
+ workOrderAttemptId: this.getExecutionState().workorderAttemptId,
3284
+ requiresExpectedRevision: this.getExecutionState().workorderAttemptId !== undefined,
3285
+ causeEventIds: this.getExecutionState().causeEventIds,
3286
+ causeKind: this.getExecutionState().source === 'operator' ? 'clock' : 'owner_message',
3287
+ });
3288
+ if (this.ownerActionEffectLedger) {
3289
+ if (typeof creationKey !== 'string' ||
3290
+ !creationKey.trim() ||
3291
+ creationKey.length > 200) {
3292
+ 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);
3293
+ }
3294
+ const context = await this.requireOwnerActionContext();
3295
+ const receipt = this.ownerActionEffectLedger.atomic(context, `task:${creationKey}`, 'task_create', taskInput, () => ({ taskId: create().id }));
3296
+ const task = this.taskLedger.getById(Number(receipt.taskId));
3297
+ if (!task) {
3298
+ throw new Error('Created task receipt references a missing task');
3299
+ }
3300
+ return { success: true, task: (0, task_list_views_js_1.serializeTaskToolRecord)(task) };
3301
+ }
3302
+ return { success: true, task: (0, task_list_views_js_1.serializeTaskToolRecord)(create()) };
3058
3303
  }
3059
3304
  case 'task_reclassify': {
3060
3305
  if (!this.taskLedger) {
@@ -3117,9 +3362,6 @@ class GatewayToolExecutor {
3117
3362
  'Use latest_event for input; responses return latestEvent.', 'TOOL_ERROR', undefined, false);
3118
3363
  }
3119
3364
  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
3365
  const criteria = rawTaskUpdate.completion_criteria;
3124
3366
  if (typeof criteria !== 'string' ||
3125
3367
  criteria.trim().length === 0 ||
@@ -3155,23 +3397,47 @@ class GatewayToolExecutor {
3155
3397
  }
3156
3398
  const parsed = JSON.parse(packet.packet_json);
3157
3399
  const selectedRaw = parsed.selected_evidence?.find((item) => item.ref?.kind === 'raw' && item.ref.raw_id === reviewAnchorRef)?.ref;
3400
+ if (!selectedRaw) {
3401
+ return {
3402
+ success: false,
3403
+ code: 'review_anchor_not_selected',
3404
+ error: 'The review anchor is not raw evidence selected in this context packet.',
3405
+ 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.',
3406
+ };
3407
+ }
3158
3408
  const task = this.taskLedger.getById(id);
3159
3409
  const connector = selectedRaw?.connector;
3160
3410
  const channelId = selectedRaw?.channel_id;
3161
3411
  const sourceChannel = typeof connector === 'string' && typeof channelId === 'string'
3162
3412
  ? `${connector}:${channelId}`
3163
3413
  : 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);
3414
+ if (!task || !sourceChannel) {
3415
+ throw new types_js_1.AgentError('task_update review anchor has no verified source identity', 'TOOL_ERROR', undefined, false);
3416
+ }
3417
+ const visible = this.currentOwnerPartitionVisibility();
3418
+ const taskSourceSeparator = task.sourceChannel?.indexOf(':') ?? -1;
3419
+ if (!visible({ connector: connector, channel: channelId }) ||
3420
+ (task.sourceChannel &&
3421
+ (taskSourceSeparator < 1 ||
3422
+ !visible({
3423
+ connector: task.sourceChannel.slice(0, taskSourceSeparator),
3424
+ channel: task.sourceChannel.slice(taskSourceSeparator + 1),
3425
+ })))) {
3426
+ return {
3427
+ success: false,
3428
+ code: 'review_source_not_authorized',
3429
+ error: 'Current owner grants do not authorize the task source and review evidence source.',
3430
+ };
3166
3431
  }
3167
- const adapter = (await getContextPacketLookupAdapter());
3432
+ const adapter = (this.connectorEventAdapter ??
3433
+ (await getContextPacketLookupAdapter()));
3168
3434
  const anchor = adapter
3169
3435
  .prepare(`SELECT source_connector, channel,
3170
3436
  COALESCE(event_datetime, source_timestamp_ms) AS source_timestamp_ms
3171
3437
  FROM connector_event_index
3172
3438
  WHERE event_index_id = ?
3173
3439
  LIMIT 1`)
3174
- .get(reviewAnchorRef);
3440
+ .all(reviewAnchorRef)[0];
3175
3441
  if (!anchor ||
3176
3442
  anchor.source_connector !== connector ||
3177
3443
  anchor.channel !== channelId ||
@@ -3264,8 +3530,8 @@ class GatewayToolExecutor {
3264
3530
  (effectInput.outcome === 'deferred'
3265
3531
  ? Array.isArray(packet.source_refs) &&
3266
3532
  packet.source_refs.length > 0 &&
3267
- !temporalPacketReferencesBoundSource(context, packet.source_refs, reviewWindow)
3268
- : !temporalPacketReferencesBoundSource(context, packet.source_refs, reviewWindow))) {
3533
+ !temporalPacketReferencesBoundSource(context, packet.source_refs, reviewWindow, this.temporalCorroborationVisible)
3534
+ : !temporalPacketReferencesBoundSource(context, packet.source_refs, reviewWindow, this.temporalCorroborationVisible))) {
3269
3535
  throw new types_js_1.AgentError('task_temporal_reconcile context packet is not bound to the active task source', 'TOOL_ERROR', undefined, false);
3270
3536
  }
3271
3537
  const evidence = {
@@ -3393,6 +3659,61 @@ class GatewayToolExecutor {
3393
3659
  return { success: false, error: 'Failed to read file: attachment extraction failed' };
3394
3660
  }
3395
3661
  }
3662
+ async executeOwnerWorkspaceEffect(kind, intent, execute) {
3663
+ const ledger = this.ownerActionEffectLedger;
3664
+ if (!ledger) {
3665
+ try {
3666
+ const { effectStarted: _notStarted, ...result } = await execute();
3667
+ return result;
3668
+ }
3669
+ catch (error) {
3670
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
3671
+ }
3672
+ }
3673
+ const context = await this.requireOwnerActionContext();
3674
+ const digest = (0, crypto_1.createHash)('sha256').update((0, owner_action_effects_js_1.canonicalOwnerActionJson)(intent)).digest('hex');
3675
+ const actionKey = `${kind}:${digest}`;
3676
+ const reserved = ledger.begin(context, actionKey, kind, { inputSha256: digest });
3677
+ if (reserved.state === 'confirmed') {
3678
+ return {
3679
+ success: true,
3680
+ output: 'This exact action already completed in this occurrence; it was not executed again.',
3681
+ };
3682
+ }
3683
+ if (reserved.state !== 'execute') {
3684
+ throw new types_js_1.AgentError('Prior workspace effect needs reconciliation before replay', 'CODE_ACT_MUTATION_OUTCOME_UNKNOWN', undefined, false);
3685
+ }
3686
+ try {
3687
+ const result = await execute();
3688
+ if (!result.success && result.effectStarted === false) {
3689
+ ledger.releaseUnstarted(context, actionKey, kind);
3690
+ const { effectStarted: _notStarted, ...failure } = result;
3691
+ return failure;
3692
+ }
3693
+ if (!result.success) {
3694
+ throw new Error('Workspace effect returned failure; partial effects may exist');
3695
+ }
3696
+ ledger.confirm(context, actionKey, kind, { success: true });
3697
+ return result;
3698
+ }
3699
+ catch (error) {
3700
+ // A spawn failure proves the child never ran. A nonzero exit does NOT:
3701
+ // a command may write data before exiting with an error.
3702
+ const spawnError = error;
3703
+ if ((kind === 'Bash' || kind === 'obsidian') &&
3704
+ spawnError.code === 'ENOENT' &&
3705
+ typeof spawnError.syscall === 'string' &&
3706
+ spawnError.syscall.startsWith('spawn')) {
3707
+ ledger.releaseUnstarted(context, actionKey, kind);
3708
+ return {
3709
+ success: false,
3710
+ error: 'Executable or working directory is unavailable; no process started.',
3711
+ };
3712
+ }
3713
+ ledger.markUnknown(context, actionKey, kind, 'workspace effect did not settle successfully');
3714
+ throw new types_js_1.AgentError('Workspace effect outcome requires reconciliation', 'CODE_ACT_MUTATION_OUTCOME_UNKNOWN', error instanceof Error ? error : undefined, false);
3715
+ }
3716
+ }
3396
3717
  /**
3397
3718
  * Execute Write tool - Write content to a file
3398
3719
  * Checks path permissions based on current AgentContext
@@ -3422,15 +3743,12 @@ class GatewayToolExecutor {
3422
3743
  return { success: false, error: `Access denied: Can only write files to ${mamaDir}` };
3423
3744
  }
3424
3745
  }
3425
- try {
3746
+ return this.executeOwnerWorkspaceEffect('Write', { path: (0, path_1.resolve)(expandedPath), content }, async () => {
3426
3747
  const dir = (0, path_1.dirname)(expandedPath);
3427
3748
  (0, fs_1.mkdirSync)(dir, { recursive: true });
3428
3749
  (0, fs_1.writeFileSync)(expandedPath, content, 'utf-8');
3429
3750
  return { success: true };
3430
- }
3431
- catch (err) {
3432
- return { success: false, error: `Failed to write file: ${err}` };
3433
- }
3751
+ });
3434
3752
  }
3435
3753
  /**
3436
3754
  * Execute Bash tool - Execute bash command
@@ -3545,36 +3863,25 @@ class GatewayToolExecutor {
3545
3863
  };
3546
3864
  }
3547
3865
  }
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 {
3866
+ return this.executeOwnerWorkspaceEffect('Bash', { command, cwd: (0, path_1.resolve)(cwd) }, async () => {
3867
+ // A deferred restart is still a non-replayable shell effect.
3868
+ const restartPattern = /systemctl\s+--user\s+restart\s+mama-os/i;
3869
+ if (restartPattern.test(command)) {
3870
+ const child = (0, child_process_1.spawn)('bash', ['-c', 'sleep 3 && systemctl --user restart mama-os'], {
3871
+ detached: true,
3872
+ stdio: 'ignore',
3873
+ });
3874
+ child.unref();
3875
+ return { success: true, output: 'mama-os restart will execute in 3 seconds.' };
3876
+ }
3562
3877
  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'),
3878
+ cwd,
3564
3879
  encoding: 'utf-8',
3565
3880
  maxBuffer: 10 * 1024 * 1024,
3566
3881
  timeout: 60000,
3567
3882
  });
3568
3883
  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
- }
3884
+ });
3578
3885
  }
3579
3886
  /**
3580
3887
  * Execute discord_send tool - Send message/file to Discord channel
@@ -3583,10 +3890,10 @@ class GatewayToolExecutor {
3583
3890
  async executeDiscordSend(input) {
3584
3891
  const { channel_id, message, image_path, file_path } = input;
3585
3892
  if (!channel_id) {
3586
- return { success: false, error: 'channel_id is required' };
3893
+ return { success: false, effectStarted: false, error: 'channel_id is required' };
3587
3894
  }
3588
3895
  if (!this.discordGateway) {
3589
- return { success: false, error: 'Discord gateway not configured' };
3896
+ return { success: false, effectStarted: false, error: 'Discord gateway not configured' };
3590
3897
  }
3591
3898
  try {
3592
3899
  // file_path takes precedence, fallback to image_path for backwards compatibility
@@ -3598,7 +3905,11 @@ class GatewayToolExecutor {
3598
3905
  await this.discordGateway.sendMessage(channel_id, message);
3599
3906
  }
3600
3907
  else {
3601
- return { success: false, error: 'Either message, file_path, or image_path is required' };
3908
+ return {
3909
+ success: false,
3910
+ effectStarted: false,
3911
+ error: 'Either message, file_path, or image_path is required',
3912
+ };
3602
3913
  }
3603
3914
  return { success: true };
3604
3915
  }
@@ -3612,10 +3923,10 @@ class GatewayToolExecutor {
3612
3923
  async executeSlackSend(input) {
3613
3924
  const { channel_id, message, file_path } = input;
3614
3925
  if (!channel_id) {
3615
- return { success: false, error: 'channel_id is required' };
3926
+ return { success: false, effectStarted: false, error: 'channel_id is required' };
3616
3927
  }
3617
3928
  if (!this.slackGateway) {
3618
- return { success: false, error: 'Slack gateway not configured' };
3929
+ return { success: false, effectStarted: false, error: 'Slack gateway not configured' };
3619
3930
  }
3620
3931
  try {
3621
3932
  if (file_path) {
@@ -3625,7 +3936,11 @@ class GatewayToolExecutor {
3625
3936
  await this.slackGateway.sendMessage(channel_id, message);
3626
3937
  }
3627
3938
  else {
3628
- return { success: false, error: 'Either message or file_path is required' };
3939
+ return {
3940
+ success: false,
3941
+ effectStarted: false,
3942
+ error: 'Either message or file_path is required',
3943
+ };
3629
3944
  }
3630
3945
  return { success: true };
3631
3946
  }
@@ -3645,7 +3960,8 @@ class GatewayToolExecutor {
3645
3960
  return { success: false, error: 'Telegram gateway not configured' };
3646
3961
  }
3647
3962
  const sourceMessageRef = this.getExecutionState().sourceMessageRef;
3648
- const ownerEvent = sourceMessageRef?.startsWith('owner-event:') === true;
3963
+ const legacyOwnerEvent = sourceMessageRef?.startsWith('owner-event:') === true;
3964
+ const ownerEvent = legacyOwnerEvent || this.ownerActionEffectLedger !== null;
3649
3965
  let ownerEffect = null;
3650
3966
  let ownerLedger = null;
3651
3967
  let ownerIntent = null;
@@ -3655,9 +3971,27 @@ class GatewayToolExecutor {
3655
3971
  // `-> error/missing receipt -> unknown -> reconcile-only
3656
3972
  try {
3657
3973
  if (ownerEvent) {
3658
- ownerEffect = this.requireOwnerEventEffect(delivery_key, 'telegram_send');
3659
- ownerLedger = this.requireOwnerEventEffectLedger();
3660
- idempotencyKey = `${sourceMessageRef}:telegram:${delivery_key}`;
3974
+ if (legacyOwnerEvent) {
3975
+ ownerEffect = this.requireOwnerEventEffect(delivery_key, 'telegram_send');
3976
+ ownerLedger = this.requireOwnerEventEffectLedger();
3977
+ idempotencyKey = `${sourceMessageRef}:telegram:${delivery_key}`;
3978
+ }
3979
+ else {
3980
+ const context = await this.requireOwnerActionContext();
3981
+ const identity = [
3982
+ sourceMessageRef ?? context.occurrenceKey,
3983
+ chat_id,
3984
+ message ?? null,
3985
+ file_path ?? null,
3986
+ sticker_emotion ?? null,
3987
+ ];
3988
+ if (delivery_key !== undefined) {
3989
+ identity.push(delivery_key);
3990
+ }
3991
+ idempotencyKey = (0, crypto_1.createHash)('sha256').update(JSON.stringify(identity)).digest('hex');
3992
+ ownerEffect = { batchId: 0, actionKey: `telegram:${delivery_key ?? idempotencyKey}` };
3993
+ ownerLedger = this.ownerEffectPort(context);
3994
+ }
3661
3995
  const existing = ownerLedger.inspect(ownerEffect.batchId, ownerEffect.actionKey, 'telegram_send');
3662
3996
  if (existing && !(0, owner_event_effects_js_1.isOwnerEventTelegramIntentV1)(existing.intent)) {
3663
3997
  return existing.state === 'confirmed'
@@ -3679,9 +4013,20 @@ class GatewayToolExecutor {
3679
4013
  if (reservation.state === 'confirmed')
3680
4014
  return { success: true };
3681
4015
  if (reservation.state === 'reconcile') {
4016
+ const variants = ownerIntent.variant === 'image' ? ['image', 'file'] : [ownerIntent.variant];
4017
+ for (const variant of variants) {
4018
+ const receipt = this.telegramGateway.readOutboundDeliveryReceipt?.(idempotencyKey, variant);
4019
+ if (isExactTelegramDeliveryReceipt(receipt, idempotencyKey, variant)) {
4020
+ ownerLedger.confirm(ownerEffect.batchId, ownerEffect.actionKey, 'telegram_send', {
4021
+ version: 1,
4022
+ ...receipt,
4023
+ });
4024
+ return { success: true };
4025
+ }
4026
+ }
3682
4027
  return {
3683
4028
  success: false,
3684
- error: 'owner-event Telegram effect is reconcile-only; automatic replay is disabled',
4029
+ error: 'Telegram delivery outcome is unconfirmed; reconcile the existing delivery before sending again.',
3685
4030
  };
3686
4031
  }
3687
4032
  }
@@ -3895,11 +4240,14 @@ class GatewayToolExecutor {
3895
4240
  async executeWebchatSend(input) {
3896
4241
  const { message, file_path } = input;
3897
4242
  if (!message && !file_path) {
3898
- return { success: false, error: 'Either message or file_path is required' };
4243
+ return {
4244
+ success: false,
4245
+ effectStarted: false,
4246
+ error: 'Either message or file_path is required',
4247
+ };
3899
4248
  }
3900
4249
  try {
3901
4250
  const outboundDir = (0, path_1.join)((0, os_1.homedir)(), '.mama', 'workspace', 'media', 'outbound');
3902
- (0, fs_1.mkdirSync)(outboundDir, { recursive: true });
3903
4251
  if (file_path) {
3904
4252
  // Expand ~ to home directory
3905
4253
  const homeDir = (0, os_1.homedir)();
@@ -3909,7 +4257,7 @@ class GatewayToolExecutor {
3909
4257
  // Check path permission based on role
3910
4258
  const pathPermission = this.checkPathPermission(expandedPath);
3911
4259
  if (!pathPermission.allowed) {
3912
- return { success: false, error: pathPermission.error };
4260
+ return { success: false, effectStarted: false, error: pathPermission.error };
3913
4261
  }
3914
4262
  // Fallback security for contexts without path restrictions:
3915
4263
  // Only allow reading from ~/.mama/ directory
@@ -3922,17 +4270,19 @@ class GatewayToolExecutor {
3922
4270
  if (rel.startsWith('..') || (0, path_1.isAbsolute)(rel)) {
3923
4271
  return {
3924
4272
  success: false,
4273
+ effectStarted: false,
3925
4274
  error: `Access denied: Can only copy files from ${mamaDir}`,
3926
4275
  };
3927
4276
  }
3928
4277
  }
3929
4278
  if (!(0, fs_1.existsSync)(expandedPath)) {
3930
- return { success: false, error: `File not found: ${expandedPath}` };
4279
+ return { success: false, effectStarted: false, error: `File not found: ${expandedPath}` };
3931
4280
  }
3932
4281
  // Copy file to outbound directory with timestamp prefix
3933
4282
  const baseName = (0, path_1.basename)(expandedPath) || 'file';
3934
4283
  const outName = `${Date.now()}_${baseName}`;
3935
4284
  const outPath = (0, path_1.join)(outboundDir, outName);
4285
+ (0, fs_1.mkdirSync)(outboundDir, { recursive: true });
3936
4286
  (0, fs_1.copyFileSync)(expandedPath, outPath);
3937
4287
  const viewerPath = `~/.mama/workspace/media/outbound/${outName}`;
3938
4288
  return {
@@ -4063,28 +4413,45 @@ class GatewayToolExecutor {
4063
4413
  catch (error) {
4064
4414
  throw new types_js_1.AgentError(error instanceof Error ? error.message : 'Invalid Obsidian CLI arguments', 'TOOL_ERROR', undefined, false);
4065
4415
  }
4066
- try {
4416
+ const invoke = async () => {
4067
4417
  const { stdout } = await execFileAsync('obsidian', cliArgs, {
4068
4418
  timeout: 15000,
4069
4419
  encoding: 'utf-8',
4070
4420
  maxBuffer: 1024 * 1024,
4071
4421
  });
4072
- return {
4073
- success: true,
4074
- data: { output: stdout.trim() },
4075
- };
4422
+ return { success: true, output: stdout.trim() };
4423
+ };
4424
+ const readCommands = new Set([
4425
+ 'read',
4426
+ 'daily:read',
4427
+ 'search',
4428
+ 'search:context',
4429
+ 'files',
4430
+ 'folders',
4431
+ 'vault',
4432
+ 'vaults',
4433
+ 'links',
4434
+ 'backlinks',
4435
+ 'properties',
4436
+ 'property:read',
4437
+ 'aliases',
4438
+ 'tags',
4439
+ ]);
4440
+ try {
4441
+ const result = readCommands.has(command)
4442
+ ? await invoke()
4443
+ : await this.executeOwnerWorkspaceEffect('obsidian', { command, args: args ?? {}, vault: this.obsidianVaultPath }, invoke);
4444
+ return result.success
4445
+ ? { success: true, data: { output: result.output } }
4446
+ : result;
4076
4447
  }
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
- };
4448
+ catch (error) {
4449
+ if (error instanceof types_js_1.AgentError) {
4450
+ throw error;
4084
4451
  }
4085
4452
  return {
4086
4453
  success: false,
4087
- error: `Obsidian CLI error: ${msg.substring(0, 500)}`,
4454
+ error: 'Obsidian CLI unavailable or command failed; inspect the configured vault before retrying.',
4088
4455
  };
4089
4456
  }
4090
4457
  }
@@ -4461,7 +4828,9 @@ class GatewayToolExecutor {
4461
4828
  // prefix and causeEventIds: the agent never restates what the host knows.
4462
4829
  if (temporalContext && this.taskLedger) {
4463
4830
  const boundTask = this.taskLedger.getById(temporalContext.taskId);
4464
- const rawChannel = boundTask?.sourceChannel ?? null;
4831
+ const rawChannel = (boundTask?.status === 'review'
4832
+ ? (boundTask.reviewAnchorSourceChannel ?? boundTask.sourceChannel)
4833
+ : boundTask?.sourceChannel) ?? null;
4465
4834
  // Trimmed: a whitespace-only event id is truthy but fails ref
4466
4835
  // normalization downstream - which would fail the WHOLE compile,
4467
4836
  // exactly what "strictly additive" forbids.
@@ -4528,7 +4897,7 @@ class GatewayToolExecutor {
4528
4897
  : undefined,
4529
4898
  });
4530
4899
  if (temporalContext &&
4531
- !temporalPacketRawSourcesWithinBoundSource(temporalContext, result.packet.source_refs, reviewWindow)) {
4900
+ !temporalPacketRawSourcesWithinBoundSource(temporalContext, result.packet.source_refs, reviewWindow, this.temporalCorroborationVisible)) {
4532
4901
  throw new Error('context_compile packet exceeds the active temporal task source');
4533
4902
  }
4534
4903
  if (wikiAuthority) {