maka-agent 0.2.0-dev.76.20261009 → 0.2.0-dev.77.20261009

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 (40) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  4. package/node_modules/@maka/runtime/dist/agent-graph-supervisor-wake.js +115 -17
  5. package/node_modules/@maka/runtime/dist/agent-run.js +11 -0
  6. package/node_modules/@maka/runtime/dist/computer-use-observation-text.js +22 -0
  7. package/node_modules/@maka/runtime/dist/conversation-copy-agent-output.js +227 -0
  8. package/node_modules/@maka/runtime/dist/conversation-copy.js +176 -46
  9. package/node_modules/@maka/runtime/dist/filesystem-worker/resource-resolver.js +3 -1
  10. package/node_modules/@maka/runtime/dist/owned-child-process.js +209 -0
  11. package/node_modules/@maka/runtime/dist/owned-process-main.js +183 -0
  12. package/node_modules/@maka/runtime/dist/pipe-process-driver.js +19 -25
  13. package/node_modules/@maka/runtime/dist/runtime-kernel.js +124 -3
  14. package/node_modules/@maka/runtime/dist/session-manager.js +125 -12
  15. package/node_modules/@maka/runtime/dist/shell-exec.js +18 -20
  16. package/node_modules/@maka/runtime/dist/shell-run-manager.js +2 -1
  17. package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +279 -8
  18. package/node_modules/@maka/runtime/dist/stream-graph-schedule-reconcile.js +209 -37
  19. package/node_modules/@maka/runtime/dist/tool-availability.js +37 -6
  20. package/node_modules/@maka/runtime/dist/tool-runtime.js +2 -1
  21. package/node_modules/@maka/runtime-host/dist/client/reconnect-lifecycle.js +44 -13
  22. package/node_modules/@maka/runtime-host/dist/client/session-catalog-summary.js +6 -0
  23. package/node_modules/@maka/runtime-host/dist/client/wsl-environment.js +4 -0
  24. package/node_modules/@maka/runtime-host/dist/protocol/index.js +4 -1
  25. package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +46 -9
  26. package/node_modules/@maka/runtime-host/dist/server/candidate.js +47 -40
  27. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +70 -24
  28. package/node_modules/@maka/runtime-host/dist/server/execution-service.js +5 -1
  29. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +41 -5
  30. package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +20 -3
  31. package/node_modules/@maka/runtime-host/dist/server/session-background-activity.js +71 -0
  32. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +8 -4
  33. package/node_modules/@maka/runtime-host/dist/server/session-interaction-activity.js +78 -0
  34. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +56 -0
  35. package/node_modules/@maka/runtime-host/dist/server/session-revision-graph-references.js +16 -2
  36. package/node_modules/@maka/runtime-host/dist/server/workhub-result-runtime.js +3 -1
  37. package/node_modules/@maka/storage/dist/context-offload-store.js +8 -3
  38. package/node_modules/@maka/storage/dist/session-bundle-policy.js +54 -19
  39. package/package.json +2 -2
  40. /package/node_modules/@maka/runtime/dist/workers/{filesystem-worker.js → filesystem-worker.mjs} +0 -0
@@ -40,6 +40,8 @@ import { baseToolResultProjection, decodeLedgerTransition, reduceEffectiveModelP
40
40
  import { archivedToolResultProjection } from './tool-result-archive-transition.js';
41
41
  import { serializeToolResultProjectionV1 } from './tool-result-archive-encoding.js';
42
42
  import { createHash } from 'node:crypto';
43
+ import { encodeCanonicalRuntimeEvent } from '@maka/core/canonical-runtime-event';
44
+ import { conversationCopyAgentOutput, isConversationCopyAgentOutputSnapshot, } from './conversation-copy-agent-output.js';
43
45
  import { readToolResultPage, readableToolResult, resolveReadInput, READ_PAGE_MAX_BYTES, } from './read-page.js';
44
46
  /** Walks every typed StorageRef site reached by conversation-copy rewriting. */
45
47
  function collectConversationCopyStorageRefs(input) {
@@ -52,7 +54,7 @@ function collectConversationCopyStorageRefs(input) {
52
54
  if (isArchivedToolResultPlaceholder(value))
53
55
  return;
54
56
  try {
55
- addContent(decodePersistedToolResultContent(markPersisted(value)));
57
+ addContent(decodeConversationCopyToolResult(value));
56
58
  }
57
59
  catch {
58
60
  // Opaque tool results carry no typed StorageRef.
@@ -141,7 +143,7 @@ export function rewriteConversationCopyMessage(message, references) {
141
143
  if (message.type === 'tool_result') {
142
144
  return {
143
145
  ...message,
144
- content: rewriteToolResultContent(message.content, references),
146
+ content: rewriteToolResultContent(message.content, references, references.toolNamesByCall?.get(conversationCopyToolCallKey(message.turnId, message.toolUseId))),
145
147
  };
146
148
  }
147
149
  if (message.type === 'token_usage' && message.providerRequestTraceId) {
@@ -153,7 +155,8 @@ export function rewriteConversationCopyMessage(message, references) {
153
155
  return message;
154
156
  }
155
157
  function rewriteAttachmentResourceRefs(text, artifactIds) {
156
- return text.replace(/maka:\/\/runtime\/attachments\/[^\s)\]}>`'",;:!]+/g, (candidate) => {
158
+ // Snapshot tool evidence can contain JSON-escaped quotes and newlines.
159
+ return text.replace(/maka:\/\/runtime\/attachments\/[^\\\s)\]}>`'",;:!]+/g, (candidate) => {
157
160
  const parsed = parseAttachmentResourceRef(candidate);
158
161
  const artifactId = parsed ? artifactIds.get(parsed.artifactId) : undefined;
159
162
  return artifactId ? `maka://runtime/attachments/${artifactId}` : candidate;
@@ -320,6 +323,10 @@ export async function cloneConversationRuntimeLedger(input) {
320
323
  const operationIds = toolOperationIdMap(flattenedPlans, targetInvocationIds);
321
324
  const references = {
322
325
  ...input.referenceMap,
326
+ toolNamesByCall: conversationCopyToolNames({
327
+ messages: input.copiedMessages,
328
+ runtimeEvents: flattenedPlans.flatMap((plan) => plan.events),
329
+ }),
323
330
  runIds,
324
331
  invocationIds,
325
332
  operationIds,
@@ -589,6 +596,14 @@ function rewriteReadInput(input, references, clonedEvents) {
589
596
  : rewriteLedgerArchiveText(rewriteAttachmentResourceRefs(original, references.artifactIds), references);
590
597
  if (!input.path.startsWith('maka://read/'))
591
598
  return { ...input, path };
599
+ if (path !== original &&
600
+ !eventId &&
601
+ references.ledgerArchives?.has(original) &&
602
+ parseToolResultArchiveResourceRef(original)?.storage !== 'ledger') {
603
+ // A retired v1 body has become a rewritten ledger snapshot. Its previous
604
+ // byte offset and digest cannot address the new resource.
605
+ return { path };
606
+ }
592
607
  const url = new URL(input.path);
593
608
  if (eventId && path !== original) {
594
609
  const copied = clonedEvents.get(eventId);
@@ -674,7 +689,21 @@ async function loadConversationCopyRunEvents(sourceRuns, sourceEvents, copyTurnI
674
689
  ];
675
690
  }));
676
691
  }
677
- export function conversationCopyLinkedChildReferences(content) {
692
+ // Legacy archive bodies and provider projections can store the raw result;
693
+ // RuntimeEvents and transcript messages normally carry the JSON wrapper.
694
+ function decodeConversationCopyToolResult(value, toolName) {
695
+ if (toolName === 'agent_output' &&
696
+ (conversationCopyAgentOutput(value) || isConversationCopyAgentOutputSnapshot(value)))
697
+ return { kind: 'json', value };
698
+ return decodePersistedToolResultContent(markPersisted(value));
699
+ }
700
+ export function conversationCopyLinkedChildReferences(content, toolName) {
701
+ if (content.kind === 'json') {
702
+ if (toolName !== 'agent_output')
703
+ return [];
704
+ const output = conversationCopyAgentOutput(content.value);
705
+ return output ? [output.reference] : [];
706
+ }
678
707
  if (content.kind === 'subagent') {
679
708
  if (!content.childSessionId)
680
709
  return [];
@@ -706,27 +735,56 @@ export function conversationCopyLinkedChildReferences(content) {
706
735
  : []);
707
736
  }
708
737
  export function collectConversationCopyLinkedChildReferences(input) {
709
- const references = [];
710
- const add = (value) => {
738
+ return conversationCopyToolResults(input).flatMap(({ content, toolName }) => conversationCopyLinkedChildReferences(content, toolName));
739
+ }
740
+ /** Pair transcript results with call metadata, including historical ledger-only calls. */
741
+ function conversationCopyToolNames(input) {
742
+ const names = new Map();
743
+ const add = (turnId, callId, name) => {
744
+ const key = conversationCopyToolCallKey(turnId, callId);
745
+ names.set(key, names.has(key) && names.get(key) !== name ? undefined : name);
746
+ };
747
+ for (const message of input.messages) {
748
+ if (message.type === 'tool_call')
749
+ add(message.turnId, message.id, message.toolName);
750
+ }
751
+ for (const event of input.runtimeEvents) {
752
+ if (event.content?.kind === 'function_call' || event.content?.kind === 'function_response') {
753
+ add(event.turnId, event.content.id, event.content.name);
754
+ }
755
+ }
756
+ return names;
757
+ }
758
+ function conversationCopyToolCallKey(turnId, callId) {
759
+ return JSON.stringify([turnId, callId]);
760
+ }
761
+ function conversationCopyToolResults(input) {
762
+ const results = [];
763
+ const toolNames = conversationCopyToolNames(input);
764
+ const add = (value, toolName) => {
711
765
  if (isArchivedToolResultPlaceholder(value))
712
766
  return;
713
767
  try {
714
- references.push(...conversationCopyLinkedChildReferences(decodePersistedToolResultContent(markPersisted(value))));
768
+ results.push({ content: decodeConversationCopyToolResult(value, toolName), toolName });
715
769
  }
716
770
  catch {
717
- // Opaque tool results have no typed linked-child references.
771
+ // Opaque tool results have no typed conversation-copy references.
718
772
  }
719
773
  };
720
774
  for (const message of input.messages) {
721
775
  if (message.type === 'tool_result') {
722
- references.push(...conversationCopyLinkedChildReferences(message.content));
776
+ results.push({
777
+ content: message.content,
778
+ toolName: toolNames.get(conversationCopyToolCallKey(message.turnId, message.toolUseId)),
779
+ });
723
780
  }
724
781
  }
725
782
  for (const event of input.runtimeEvents) {
726
- if (event.content?.kind === 'function_response')
727
- add(event.content.result);
783
+ if (event.content?.kind === 'function_response') {
784
+ add(event.content.result, toolNames.get(conversationCopyToolCallKey(event.turnId, event.content.id)));
785
+ }
728
786
  }
729
- return references;
787
+ return results;
730
788
  }
731
789
  /**
732
790
  * Collects the `relativePath` of every `session_file` StorageRef that the copied
@@ -737,6 +795,8 @@ export function collectConversationCopyLinkedChildReferences(input) {
737
795
  * their refs resolve in `rewriteStorageRef`. Walks exactly the ref sites reached
738
796
  * by `rewriteStorageRef`: user-message attachments, tool_result image refs, text
739
797
  * runtime-event attachments, and function_response / archived tool-result images.
798
+ * Static agent_output snapshots also own their Artifacts; those can retain a
799
+ * child Turn id outside the copied parent's turn closure and need explicit inclusion.
740
800
  */
741
801
  export function collectConversationCopySessionFileRefs(input) {
742
802
  const refs = new Set();
@@ -745,6 +805,14 @@ export function collectConversationCopySessionFileRefs(input) {
745
805
  refs.add(ref.relativePath);
746
806
  }
747
807
  }
808
+ for (const { content, toolName } of conversationCopyToolResults(input)) {
809
+ if (toolName === 'agent_output' &&
810
+ content.kind === 'json' &&
811
+ isConversationCopyAgentOutputSnapshot(content.value)) {
812
+ for (const artifactId of content.value.artifactIds)
813
+ refs.add(artifactId);
814
+ }
815
+ }
748
816
  return refs;
749
817
  }
750
818
  function cloneAgentRunEvent(event, ids, references, sourceCompactableEvents, clonedRuntimeEvents, checkpointIds, providerTraceIds, logicalCallIds, clonedTransitions) {
@@ -830,10 +898,9 @@ function cloneAgentRunEvent(event, ids, references, sourceCompactableEvents, clo
830
898
  }
831
899
  else if (event.type === MODEL_PROJECTION_TRANSITION_EVENT_TYPE) {
832
900
  const cloned = clonedTransitions.get(event);
833
- // Every transition whose target is in the copied slice was gathered into
834
- // this run's ledger, wherever it was recorded. So a transition that finds no
835
- // cloned target has genuinely lost its target as well, and dropping it
836
- // cannot bring replaced content back.
901
+ // A transition is omitted only when its target was excluded or its archive
902
+ // wrapper was collapsed into an already copied archive. Neither case can
903
+ // bring replaced content back into the model-visible history.
837
904
  if (!cloned)
838
905
  return null;
839
906
  data = { ...event.data, transition: cloned, runtimeEventId: cloned.target.runtimeEventId };
@@ -873,37 +940,57 @@ function cloneModelProjectionTransition(event, references, clonedRuntimeEvents,
873
940
  }
874
941
  const placeholder = rawPlaceholder;
875
942
  const existing = transitionState.get(clonedTarget.id);
876
- const sourceProjection = existing?.projection ?? baseToolResultProjection(clonedTarget);
943
+ // RuntimeEvent storage canonicalizes JSON key order. Hash the bytes a later
944
+ // archive read will reconstruct, including newly built snapshot projections.
945
+ const sourceProjection = existing?.projection ??
946
+ baseToolResultProjection(encodeCanonicalRuntimeEvent(clonedTarget).event);
877
947
  if (!sourceProjection) {
878
948
  throw new Error(`Cannot copy model projection transition ${event.id} onto its target`);
879
949
  }
880
- // The copied event already holds the target's rewritten result, so every
881
- // archive, including a legacy Artifact-backed one, is rebuilt as a ledger
882
- // archive of that event instead of pointing at the source's archived body.
950
+ const previousTransitionId = source.previousTransitionId
951
+ ? requiredMappedId(transitionIds, source.previousTransitionId, 'model projection transition')
952
+ : undefined;
953
+ // Persisted placeholders can use a public event address or an exact legacy
954
+ // ledger identity. Both must resolve to the same rebuilt archive on recopy.
955
+ const sourceRefs = new Set([
956
+ buildToolResultArchiveResourceRef(placeholder),
957
+ ...(placeholder.resourceRef ? [placeholder.resourceRef] : []),
958
+ ]);
959
+ const previousPlaceholder = existing?.projection.kind === 'json' ? existing.projection.value : undefined;
960
+ if (existing &&
961
+ isArchivedToolResultPlaceholder(previousPlaceholder) &&
962
+ previousPlaceholder.rewriteVersion === 2) {
963
+ if (previousTransitionId !== existing.transitionId)
964
+ throw new Error(`Cannot collapse disconnected archive transition ${event.id}`);
965
+ // Public Read addresses identify an event, not a transition. Archiving
966
+ // this placeholder again would make that address return itself. Keep the
967
+ // first archive of the copied body and alias every later wrapper to it.
968
+ for (const sourceRef of sourceRefs)
969
+ references.ledgerArchives?.set(sourceRef, previousPlaceholder);
970
+ transitionIds.set(source.transitionId, existing.transitionId);
971
+ return null;
972
+ }
973
+ if (clonedTarget.content?.kind === 'function_response' && !clonedTarget.content.modelProjection)
974
+ clonedTarget.content.modelProjection = sourceProjection;
975
+ // Rebuild every archive over the copied ledger, including legacy v1 bodies.
883
976
  const serialized = serializeToolResultProjectionV1(sourceProjection);
884
977
  const rewritten = buildLedgerArchivedToolResultPlaceholder({
885
978
  storage: 'ledger',
886
979
  runtimeEventId: clonedTarget.id,
887
980
  toolCallId: placeholder.toolCallId,
888
981
  toolName: placeholder.toolName,
982
+ originalEstimatedTokens: placeholder.originalEstimatedTokens,
983
+ reason: placeholder.reason,
889
984
  sourceProjectionDigest: durableToolResultProjectionDigest(sourceProjection),
890
985
  bodySha256: createHash('sha256').update(serialized).digest('hex'),
891
986
  originalBytes: Buffer.byteLength(serialized),
892
- originalEstimatedTokens: placeholder.originalEstimatedTokens,
893
- reason: placeholder.reason,
894
- ...(source.previousTransitionId
895
- ? {
896
- previousTransitionId: requiredMappedId(transitionIds, source.previousTransitionId, 'model projection transition'),
897
- }
898
- : {}),
987
+ ...(previousTransitionId ? { previousTransitionId } : {}),
899
988
  });
900
989
  if (placeholder.page) {
901
990
  rewritten.page = readToolResultPage(serialized, { path: rewritten.resourceRef }, READ_PAGE_MAX_BYTES - Buffer.byteLength(JSON.stringify(rewritten)) - 32);
902
991
  }
903
- // Earlier Reads of a legacy archive keep naming its copied Artifact.
904
- if (placeholder.rewriteVersion === 2) {
905
- references.ledgerArchives?.set(placeholder.resourceRef ?? buildToolResultArchiveResourceRef(placeholder), rewritten);
906
- }
992
+ for (const sourceRef of sourceRefs)
993
+ references.ledgerArchives?.set(sourceRef, rewritten);
907
994
  const transition = buildModelProjectionTransition({
908
995
  sessionId: references.targetSessionId,
909
996
  target: {
@@ -917,11 +1004,7 @@ function cloneModelProjectionTransition(event, references, clonedRuntimeEvents,
917
1004
  // The applied chain is copied in fold order, so a predecessor is always
918
1005
  // rebuilt before its successor. An unmapped one means the chain broke, and
919
1006
  // rooting the successor instead would change what the fold decides.
920
- ...(source.previousTransitionId
921
- ? {
922
- previousTransitionId: requiredMappedId(transitionIds, source.previousTransitionId, 'model projection transition'),
923
- }
924
- : {}),
1007
+ ...(previousTransitionId ? { previousTransitionId } : {}),
925
1008
  now: source.createdAt,
926
1009
  });
927
1010
  transitionIds.set(source.transitionId, transition.transitionId);
@@ -1135,6 +1218,9 @@ function rewriteInvocationOpening(opening, references) {
1135
1218
  };
1136
1219
  }
1137
1220
  function rewriteRuntimeEventReferences(event, references) {
1221
+ const toolName = event.content?.kind === 'function_response'
1222
+ ? references.toolNamesByCall?.get(conversationCopyToolCallKey(event.turnId, event.content.id))
1223
+ : undefined;
1138
1224
  const content = event.content?.kind === 'text'
1139
1225
  ? {
1140
1226
  ...event.content,
@@ -1151,10 +1237,10 @@ function rewriteRuntimeEventReferences(event, references) {
1151
1237
  : event.content?.kind === 'function_response'
1152
1238
  ? {
1153
1239
  ...event.content,
1154
- result: rewriteRuntimeToolResult(event.content.result, references),
1240
+ result: rewriteRuntimeToolResult(event.content.result, references, toolName),
1155
1241
  ...(event.content.modelProjection
1156
1242
  ? {
1157
- modelProjection: rewriteDurableToolResultProjectionArtifactRefs(event.content.modelProjection, (ref) => rewriteProjectionArtifactRef(ref, references)),
1243
+ modelProjection: rewriteConversationCopyModelProjection(event.content.modelProjection, references, toolName),
1158
1244
  }
1159
1245
  : {}),
1160
1246
  }
@@ -1249,7 +1335,24 @@ function rewriteToolRecoveryFact(recovery, operationId, references) {
1249
1335
  },
1250
1336
  };
1251
1337
  }
1252
- function rewriteToolResultContent(content, references) {
1338
+ function rewriteToolResultContent(content, references, toolName) {
1339
+ if (toolName === 'agent_output' && content.kind === 'json') {
1340
+ if (isConversationCopyAgentOutputSnapshot(content.value)) {
1341
+ return rewriteAgentOutputSnapshot(content.value, references);
1342
+ }
1343
+ const output = conversationCopyAgentOutput(content.value);
1344
+ if (output) {
1345
+ const shared = sharedChild(output.reference.childSessionId, references);
1346
+ if (!shared) {
1347
+ return rewriteAgentOutputSnapshot(output.snapshot, references);
1348
+ }
1349
+ // Revision children remain owned by their original physical Session.
1350
+ // Validate those external references without rewriting their identities.
1351
+ rewriteLinkedRunId(output.reference.runId, shared, references, 'AgentRun');
1352
+ rewriteLinkedArtifactIds(output.reference.artifactIds, shared, references);
1353
+ return content;
1354
+ }
1355
+ }
1253
1356
  if (content.kind === 'image') {
1254
1357
  return { ...content, ref: rewriteStorageRef(content.ref, references) };
1255
1358
  }
@@ -1320,17 +1423,44 @@ function rewriteToolResultContent(content, references) {
1320
1423
  }
1321
1424
  return content;
1322
1425
  }
1323
- function rewriteRuntimeToolResult(value, references) {
1426
+ function rewriteAgentOutputSnapshot(snapshot, references) {
1427
+ return {
1428
+ kind: 'json',
1429
+ value: {
1430
+ ...snapshot,
1431
+ ...(snapshot.text !== undefined
1432
+ ? { text: rewriteAttachmentResourceRefs(snapshot.text, references.artifactIds) }
1433
+ : {}),
1434
+ artifactIds: rewriteArtifactIds(snapshot.artifactIds, references),
1435
+ },
1436
+ };
1437
+ }
1438
+ function rewriteConversationCopyModelProjection(projection, references, toolName) {
1439
+ if (toolName === 'agent_output' &&
1440
+ projection.kind === 'json' &&
1441
+ (conversationCopyAgentOutput(projection.value) ||
1442
+ isConversationCopyAgentOutputSnapshot(projection.value))) {
1443
+ const content = rewriteToolResultContent({ kind: 'json', value: projection.value }, references, toolName);
1444
+ if (content.kind === 'json')
1445
+ return { ...projection, value: content.value };
1446
+ }
1447
+ return rewriteDurableToolResultProjectionArtifactRefs(projection, (ref) => rewriteProjectionArtifactRef(ref, references));
1448
+ }
1449
+ function rewriteRuntimeToolResult(value, references, toolName) {
1324
1450
  if (isArchivedToolResultPlaceholder(value))
1325
1451
  return rewriteArchivedToolResult(value, references);
1326
1452
  let content;
1327
1453
  try {
1328
- content = decodePersistedToolResultContent(markPersisted(value));
1454
+ content = decodeConversationCopyToolResult(value, toolName);
1329
1455
  }
1330
1456
  catch {
1331
1457
  return value;
1332
1458
  }
1333
- return rewriteToolResultContent(content, references);
1459
+ const rewritten = rewriteToolResultContent(content, references, toolName);
1460
+ return (conversationCopyAgentOutput(value) || isConversationCopyAgentOutputSnapshot(value)) &&
1461
+ rewritten.kind === 'json'
1462
+ ? rewritten.value
1463
+ : rewritten;
1334
1464
  }
1335
1465
  function rewriteArtifactIds(artifactIds, references) {
1336
1466
  return artifactIds.flatMap((artifactId) => {
@@ -1401,11 +1531,11 @@ function rewriteProjectionArtifactRef(ref, references) {
1401
1531
  return rewritten;
1402
1532
  }
1403
1533
  function rewriteArchivedToolResult(value, references) {
1534
+ const migrated = references.ledgerArchives?.get(buildToolResultArchiveResourceRef(value));
1535
+ if (migrated)
1536
+ return migrated;
1404
1537
  if (value.rewriteVersion === 2) {
1405
- const rewritten = references.ledgerArchives?.get(buildToolResultArchiveResourceRef(value));
1406
- if (!rewritten)
1407
- throw new Error('Cannot copy ledger archive without its committed transition');
1408
- return rewritten;
1538
+ throw new Error('Cannot copy ledger archive without its committed transition');
1409
1539
  }
1410
1540
  const artifactId = rewriteOwnedArtifactId(value.artifactId, references);
1411
1541
  const resource = value.resourceRef
@@ -19,7 +19,9 @@
19
19
  import { realpath, stat } from 'node:fs/promises';
20
20
  import { dirname, join, resolve } from 'node:path';
21
21
  import { fileURLToPath } from 'node:url';
22
- export const FILESYSTEM_WORKER_BUNDLE_NAME = 'filesystem-worker.js';
22
+ // `.mjs` so Node never looks for a package.json to decide the module type:
23
+ // the Windows AppContainer sandbox denies that read and the worker crashes.
24
+ export const FILESYSTEM_WORKER_BUNDLE_NAME = 'filesystem-worker.mjs';
23
25
  export function filesystemWorkerBundleCandidate(location) {
24
26
  if (location.kind === 'desktop-packaged') {
25
27
  return join(location.resourcesPath, 'workers', FILESYSTEM_WORKER_BUNDLE_NAME);
@@ -0,0 +1,209 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ import { spawn } from 'node:child_process';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { buildSpawnStdio } from './child-fd-input.js';
22
+ import { terminateProcessTree } from './process-tree-terminator.js';
23
+ /** The returned PID owns the command group. Its private IPC channel is a
24
+ * lifetime lease: only the supervisor can spawn the command, and losing the
25
+ * owner tears down the group even when no Host cleanup callback can run.
26
+ * Output and descriptor payloads flow directly through inherited OS handles.
27
+ * `ready` resolves with the command's own PID once the supervisor admits it.
28
+ */
29
+ export function spawnOwnedProcess(input) {
30
+ const stdio = buildSpawnStdio(input.fdInputs, input.stdin);
31
+ const child = spawn(process.execPath, [fileURLToPath(new URL('./owned-process-main.js', import.meta.url))], {
32
+ cwd: input.cwd,
33
+ // Caller-supplied environment belongs to the command, not this trusted supervisor.
34
+ env: {
35
+ ...process.env,
36
+ NODE_OPTIONS: '',
37
+ ...(process.versions.electron ? { ELECTRON_RUN_AS_NODE: '1' } : {}),
38
+ },
39
+ stdio: [...stdio, 'ipc'],
40
+ // POSIX: the supervisor leads a new process group that the command joins.
41
+ // Termination signals the group and removes descendants visible outside
42
+ // it at each process-table snapshot. Windows has no process groups;
43
+ // taskkill /T owns the equivalent cleanup. There, detaching also keeps
44
+ // the supervisor out of the Host's libuv job, which would kill it with
45
+ // the Host before it could stop the command's descendants; the command
46
+ // itself stays in the supervisor's job.
47
+ detached: true,
48
+ windowsHide: true,
49
+ });
50
+ let started = false;
51
+ let completed = false;
52
+ let failureReported = false;
53
+ let resolveReady;
54
+ let rejectReady;
55
+ const ready = new Promise((resolve, reject) => {
56
+ resolveReady = resolve;
57
+ rejectReady = reject;
58
+ });
59
+ // Some callers observe ChildProcess errors instead of awaiting admission.
60
+ void ready.catch(() => { });
61
+ const timer = setTimeout(() => fail(new Error('Command supervisor startup timed out')), 10_000);
62
+ timer.unref();
63
+ const terminate = () => {
64
+ if (child.pid)
65
+ void terminateProcessTree({ pid: child.pid, signal: 'SIGKILL' }).catch(() => { });
66
+ };
67
+ // Once the supervisor is reaped its PID can be reused, so a tree walk from it
68
+ // could reach unrelated processes. Its POSIX process group cannot be
69
+ // reallocated while members remain, so it alone still names the command.
70
+ // Windows has no such handle; taskkill /T on a reaped PID is skipped.
71
+ const terminateOrphanedGroup = () => {
72
+ if (process.platform === 'win32' || !child.pid)
73
+ return;
74
+ try {
75
+ process.kill(-child.pid, 'SIGKILL');
76
+ }
77
+ catch {
78
+ // The group is already empty.
79
+ }
80
+ };
81
+ // Command output reaches these pipes directly, racing the IPC admission
82
+ // message. Hold it until callers have observed `ready`, so output never
83
+ // precedes admission, as with a direct spawn. Callers attach 'data'
84
+ // listeners synchronously; an explicit pause keeps those from resuming.
85
+ const outputs = [child.stdout, child.stderr].filter((stream) => stream !== null);
86
+ for (const stream of outputs)
87
+ stream.pause();
88
+ let outputReleased = false;
89
+ const releaseOutput = () => {
90
+ if (outputReleased)
91
+ return;
92
+ outputReleased = true;
93
+ for (const stream of outputs)
94
+ stream.resume();
95
+ };
96
+ /**
97
+ * `stop` says who stops the tree: `tree` walks it from the live supervisor;
98
+ * `group` signals only its process group, because the supervisor is exiting
99
+ * on its own and its PID may be reaped before a walk could start; `none`
100
+ * leaves the stop to the supervisor, which walks its own tree.
101
+ */
102
+ function fail(error, stop = 'tree') {
103
+ if (failureReported)
104
+ return;
105
+ failureReported = true;
106
+ clearTimeout(timer);
107
+ rejectReady(error);
108
+ releaseOutput();
109
+ if (stop === 'tree')
110
+ terminate();
111
+ else if (stop === 'group')
112
+ terminateOrphanedGroup();
113
+ child.emit('error', error);
114
+ }
115
+ child.once('error', (error) => {
116
+ clearTimeout(timer);
117
+ rejectReady(error);
118
+ releaseOutput();
119
+ });
120
+ child.once('spawn', () => {
121
+ const request = {
122
+ kind: 'launch',
123
+ program: input.program,
124
+ args: input.args,
125
+ cwd: input.cwd,
126
+ env: input.env ?? process.env,
127
+ shell: input.shell,
128
+ inheritedFds: (input.fdInputs ?? []).map(({ fd }) => fd),
129
+ };
130
+ child.send(request, (error) => {
131
+ if (error && child.exitCode === null && child.signalCode === null)
132
+ fail(error);
133
+ });
134
+ });
135
+ child.on('message', (value) => {
136
+ const message = value;
137
+ if (message.kind === 'started') {
138
+ started = true;
139
+ clearTimeout(timer);
140
+ resolveReady(message.pid);
141
+ // Promise continuations of `ready` run before the check phase; a
142
+ // nextTick resume would deliver output ahead of them.
143
+ setImmediate(releaseOutput);
144
+ }
145
+ else if (message.kind === 'completed') {
146
+ completed = true;
147
+ }
148
+ else if (message.kind === 'failed') {
149
+ // A result already arrived; a later report cannot replace it.
150
+ if (completed)
151
+ return;
152
+ const error = Object.assign(new Error(message.message), { code: message.code });
153
+ if (!started) {
154
+ // The command never spawned; the supervisor is exiting on its own.
155
+ completed = true;
156
+ fail(error, 'group');
157
+ }
158
+ else {
159
+ // A supervisor fault after admission: it stops its own tree, including
160
+ // descendants that left its group, before exiting. Signalling the group
161
+ // now would kill it mid-walk. The group kill on its exit stays the
162
+ // backstop, so `completed` remains unset.
163
+ fail(error, 'none');
164
+ }
165
+ }
166
+ });
167
+ child.once('exit', (code) => {
168
+ clearTimeout(timer);
169
+ releaseOutput();
170
+ // `exit` is not ordered after IPC messages still being read; the channel's
171
+ // close is. Decide a missing result only once the channel has drained.
172
+ if (completed || !child.connected)
173
+ settleExit(code);
174
+ else
175
+ child.once('disconnect', () => settleExit(code));
176
+ });
177
+ function settleExit(code) {
178
+ if (!started)
179
+ rejectReady(new Error('Command supervisor exited before admission'));
180
+ if (completed)
181
+ return;
182
+ // The group can outlive its leader. An unexpected supervisor exit must
183
+ // not leave an admitted command running while the Host is still alive.
184
+ terminateOrphanedGroup();
185
+ // A clean exit without `completed` lost the command's result. On POSIX the
186
+ // Host's own stops end the supervisor by signal, so any exit code there is
187
+ // a supervisor fault too. On Windows taskkill, the Host's forced stop, also
188
+ // ends with an ordinary code.
189
+ const lost = code === 0 || (code !== null && process.platform !== 'win32');
190
+ if (lost && !failureReported) {
191
+ child.emit('error', new Error('Command supervisor lost its result'));
192
+ }
193
+ }
194
+ return { child, ready };
195
+ }
196
+ /** A supervisor PID denotes the whole command group, including during startup. */
197
+ export function signalOwnedProcess(child, signal) {
198
+ if (process.platform === 'win32' || !child.pid)
199
+ return child.kill(signal);
200
+ try {
201
+ process.kill(-child.pid, signal);
202
+ return true;
203
+ }
204
+ catch (error) {
205
+ if (error.code === 'ESRCH')
206
+ return false;
207
+ throw error;
208
+ }
209
+ }