maka-agent 0.2.0-dev.37.20260915 → 0.2.0-dev.39.20260916

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 (94) hide show
  1. package/dist/pi-tui-pickers.js +18 -4
  2. package/dist/pi-tui-runner.js +310 -76
  3. package/dist/runtime-host-installed-update-bootstrap.js +3 -1
  4. package/dist/runtime-host-run-command.js +18 -4
  5. package/dist/runtime-host-session-channel.js +8 -17
  6. package/dist/runtime-host-session-driver.js +5 -13
  7. package/dist/runtime-host-tui-command.js +3 -3
  8. package/dist/runtime-host-tui-context.js +20 -0
  9. package/dist/tui-copy-catalog.js +81 -3
  10. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  11. package/node_modules/@maka/core/dist/external-session.js +43 -0
  12. package/node_modules/@maka/core/dist/model-metadata.js +15 -6
  13. package/node_modules/@maka/core/dist/provider-registry.js +15 -0
  14. package/node_modules/@maka/core/dist/recall.js +956 -0
  15. package/node_modules/@maka/core/dist/runtime-policy/connection-catalog-codec.js +11 -15
  16. package/node_modules/@maka/core/dist/runtime-policy.js +1 -1
  17. package/node_modules/@maka/core/dist/session-name.js +2 -2
  18. package/node_modules/@maka/core/dist/session.js +16 -0
  19. package/node_modules/@maka/core/dist/text-sanitize.js +2 -2
  20. package/node_modules/@maka/core/dist/tool-result-record-schema.js +7 -0
  21. package/node_modules/@maka/core/dist/usage-record-schema.js +0 -1
  22. package/node_modules/@maka/core/package.json +1 -1
  23. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +46 -22
  24. package/node_modules/@maka/runtime/dist/builtin-tools.js +3 -3
  25. package/node_modules/@maka/runtime/dist/commandcode-cli-language-model.js +836 -0
  26. package/node_modules/@maka/runtime/dist/continuation-replay.js +3 -1
  27. package/node_modules/@maka/runtime/dist/memory-extraction.js +7 -1
  28. package/node_modules/@maka/runtime/dist/model-factory.js +19 -0
  29. package/node_modules/@maka/runtime/dist/model-fetcher.js +38 -81
  30. package/node_modules/@maka/runtime/dist/model-history.js +30 -3
  31. package/node_modules/@maka/runtime/dist/model-runtime.js +8 -0
  32. package/node_modules/@maka/runtime/dist/provider-error-classification.js +1 -0
  33. package/node_modules/@maka/runtime/dist/read-page.js +3 -3
  34. package/node_modules/@maka/runtime/dist/recall-candidates.js +53 -0
  35. package/node_modules/@maka/runtime/dist/recall-tools.js +210 -0
  36. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +18 -14
  37. package/node_modules/@maka/runtime/dist/runtime-kernel.js +3 -1
  38. package/node_modules/@maka/runtime/dist/runtime-ledger-repair.js +56 -44
  39. package/node_modules/@maka/runtime/dist/runtime-read-model.js +1 -2
  40. package/node_modules/@maka/runtime/dist/session-manager.js +52 -1
  41. package/node_modules/@maka/runtime/dist/session-recap.js +6 -5
  42. package/node_modules/@maka/runtime/dist/test-connection.js +59 -2
  43. package/node_modules/@maka/runtime/dist/tool-availability.js +4 -0
  44. package/node_modules/@maka/runtime/dist/tool-runtime.js +8 -40
  45. package/node_modules/@maka/runtime/dist/workers/filesystem-worker.js +2 -2
  46. package/node_modules/@maka/runtime/package.json +3 -2
  47. package/node_modules/@maka/runtime-host/dist/client/connect-or-spawn.js +19 -14
  48. package/node_modules/@maka/runtime-host/dist/client/connection.js +1 -7
  49. package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
  50. package/node_modules/@maka/runtime-host/dist/client/session-subscription.js +26 -100
  51. package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +6 -5
  52. package/node_modules/@maka/runtime-host/dist/protocol/index.js +5 -1
  53. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -1
  54. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +21 -5
  55. package/node_modules/@maka/runtime-host/dist/protocol/session-transcript.js +25 -103
  56. package/node_modules/@maka/runtime-host/dist/protocol/usage-pricing.js +19 -1
  57. package/node_modules/@maka/runtime-host/dist/protocol/usage-screen.js +304 -0
  58. package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +4 -9
  59. package/node_modules/@maka/runtime-host/dist/server/connection-session.js +0 -5
  60. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +70 -14
  61. package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +69 -40
  62. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +10 -0
  63. package/node_modules/@maka/runtime-host/dist/server/interaction-coordinator.js +22 -3
  64. package/node_modules/@maka/runtime-host/dist/server/operation-dispatcher.js +15 -2
  65. package/node_modules/@maka/runtime-host/dist/server/plan-coordinator.js +41 -11
  66. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +5 -7
  67. package/node_modules/@maka/runtime-host/dist/server/sandbox-boundary-graph-wake.js +3 -5
  68. package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +289 -476
  69. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +24 -11
  70. package/node_modules/@maka/runtime-host/dist/server/session-transcript-pager.js +11 -189
  71. package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +58 -177
  72. package/node_modules/@maka/runtime-host/dist/server/usage-pricing-coordinator.js +41 -1
  73. package/node_modules/@maka/storage/dist/claude-code-session-adapter.js +292 -72
  74. package/node_modules/@maka/storage/dist/codex-session-adapter.js +348 -124
  75. package/node_modules/@maka/storage/dist/execution-stores.js +20 -2
  76. package/node_modules/@maka/storage/dist/external-session-importer.js +25 -3
  77. package/node_modules/@maka/storage/dist/offset-external-session-catalog.js +69 -0
  78. package/node_modules/@maka/storage/dist/opencode-session-adapter.js +278 -73
  79. package/node_modules/@maka/storage/dist/operational-state-store.js +18 -5
  80. package/node_modules/@maka/storage/dist/plan-authority.js +48 -0
  81. package/node_modules/@maka/storage/dist/recall-fold.js +65 -0
  82. package/node_modules/@maka/storage/dist/runtime-event-persistence.js +2 -0
  83. package/node_modules/@maka/storage/dist/runtime-transcript-query.js +69 -124
  84. package/node_modules/@maka/storage/dist/session-store.js +10 -1
  85. package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +97 -5
  86. package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +139 -0
  87. package/node_modules/@maka/storage/dist/sqlite-usage-schema.js +57 -1
  88. package/node_modules/@maka/storage/dist/usage-screen.js +289 -0
  89. package/node_modules/@maka/storage/dist/usage-stores.js +8 -3
  90. package/node_modules/@maka/storage/package.json +0 -1
  91. package/package.json +1 -1
  92. package/node_modules/@maka/core/dist/foreign-session.js +0 -596
  93. package/node_modules/@maka/runtime/dist/history-tools.js +0 -429
  94. package/node_modules/@maka/storage/dist/foreign-session-store.js +0 -641
@@ -558,6 +558,7 @@ export class SessionSearchOverlay {
558
558
  list;
559
559
  selectedValue;
560
560
  scopeLabel;
561
+ notice;
561
562
  constructor(tui, input) {
562
563
  this.tui = tui;
563
564
  this.input = input;
@@ -565,14 +566,22 @@ export class SessionSearchOverlay {
565
566
  this.choices = input.choices;
566
567
  this.filtered = input.choices;
567
568
  this.scopeLabel = input.scopeLabel;
569
+ this.notice = input.notice;
568
570
  this.list = this.buildList();
569
571
  this.searchEditor = new Editor(tui, editorTheme(), { paddingX: 0 });
570
572
  this.searchEditor.onChange = (text) => this.applyQuery(text);
571
573
  }
572
- updateChoices(choices, scopeLabel) {
574
+ updateChoices(choices, scopeLabel, notice) {
573
575
  this.choices = choices;
574
576
  this.scopeLabel = scopeLabel;
575
- this.applyQuery(this.searchEditor.getText());
577
+ this.notice = notice;
578
+ if (this.input.onQuery) {
579
+ this.filtered = choices;
580
+ this.list = this.buildList();
581
+ }
582
+ else {
583
+ this.applyQuery(this.searchEditor.getText());
584
+ }
576
585
  }
577
586
  buildList() {
578
587
  const list = new SelectList(this.filtered.map(({ item }) => item), 10, selectListTheme(), {
@@ -594,6 +603,10 @@ export class SessionSearchOverlay {
594
603
  }
595
604
  applyQuery(text) {
596
605
  const query = text.trim().toLocaleLowerCase();
606
+ if (this.input.onQuery) {
607
+ this.input.onQuery(query);
608
+ return;
609
+ }
597
610
  this.filtered = query
598
611
  ? this.choices.filter((choice) => choice.searchText.includes(query))
599
612
  : this.choices;
@@ -628,13 +641,14 @@ export class SessionSearchOverlay {
628
641
  }
629
642
  this.searchEditor.focused = true;
630
643
  return [
631
- padLine(`${this.copy.resumeSessionTitle} ${ansi.accent(this.scopeLabel)}`, safeWidth),
644
+ padLine(`${this.input.title ?? this.copy.resumeSessionTitle} ${ansi.accent(this.scopeLabel)}`, safeWidth),
632
645
  padLine(ansi.dim(this.copy.sessionSearchHint), safeWidth),
633
646
  padLine('', safeWidth),
634
647
  ...this.renderFieldRow(safeWidth),
635
648
  padLine('', safeWidth),
649
+ ...(this.notice ? [padLine(ansi.dim(this.notice), safeWidth)] : []),
636
650
  ...(this.filtered.length === 0
637
- ? [padLine(ansi.dim(this.copy.noMatchingSessions), safeWidth)]
651
+ ? [padLine(ansi.dim(this.input.emptyText ?? this.copy.noMatchingSessions), safeWidth)]
638
652
  : this.list.render(safeWidth).map((line) => formatPickerItemLine(line, safeWidth))),
639
653
  padLine(ansi.accent('-'.repeat(safeWidth)), safeWidth),
640
654
  ];
@@ -25,7 +25,6 @@ import { projectRevisionLinkedSessionTree } from '@maka/core/session-revisions';
25
25
  import { slashCommandsForSurface, } from '@maka/core/slash-command-catalog';
26
26
  import { latestAssistantModelId, } from '@maka/core/session';
27
27
  import { defineUiMessageCatalog, formatUiMessage, resolveUiMessageCatalog, } from '@maka/core/ui-locale';
28
- import { buildForeignSessionHandoffMessage, foreignSessionHandoffDisplayText, foreignSourceLabel, } from '@maka/core/foreign-session';
29
28
  import { listApiKeyOnboardableProviders } from './onboarding-catalog.js';
30
29
  import { AUTO_RECAP_DISPLAY_LIMIT_BYTES, shouldAutoRecap } from './session-recap.js';
31
30
  import { MakaSkillHighlightEditor } from './skill-highlight-editor.js';
@@ -54,6 +53,37 @@ import { formatMakaResumeCommand } from './cli-invocation.js';
54
53
  import { goalAttachedNoticeText, goalPausedNoticeText, goalStatusLabel, goalSummaryLines, isLiveGoalStatus, } from './pi-goal.js';
55
54
  import { getTuiPrimaryGuidance } from './tui-primary-guidance.js';
56
55
  import { TUI_COPY_RESOURCES } from './tui-copy-catalog.js';
56
+ function externalSourceLabel(adapterId) {
57
+ switch (adapterId) {
58
+ case 'claude-code':
59
+ return 'Claude Code';
60
+ case 'codex':
61
+ return 'Codex';
62
+ case 'opencode':
63
+ return 'OpenCode';
64
+ default:
65
+ return adapterId;
66
+ }
67
+ }
68
+ function externalImportErrorCode(error) {
69
+ if (typeof error !== 'object' || error === null)
70
+ return 'commit_outcome_unknown';
71
+ const value = error;
72
+ if (value.operation !== undefined && value.operation !== 'external-session.import') {
73
+ return undefined;
74
+ }
75
+ if (value.mode === 'command' && value.dispatch === 'not_dispatched')
76
+ return undefined;
77
+ if (value.mode === 'command' && value.dispatch === 'dispatched') {
78
+ return 'commit_outcome_unknown';
79
+ }
80
+ if (value.code === 'commit_outcome_unknown' ||
81
+ value.code === 'model_unavailable' ||
82
+ value.code === 'source_unreadable') {
83
+ return value.code;
84
+ }
85
+ return value.code === undefined ? 'commit_outcome_unknown' : undefined;
86
+ }
57
87
  export function resolveTaskbarProgress(setting, environment = {
58
88
  platform: process.platform,
59
89
  override: process.env.MAKA_TASKBAR_PROGRESS,
@@ -96,6 +126,27 @@ const TUI_REWIND_COPY = resolveUiMessageCatalog(defineUiMessageCatalog()(TUI_COP
96
126
  const TUI_SKILLS_COPY = resolveUiMessageCatalog(defineUiMessageCatalog()(TUI_COPY_RESOURCES.skills));
97
127
  const TUI_CONNECTION_IDENTITY_COPY = resolveUiMessageCatalog(defineUiMessageCatalog()(TUI_COPY_RESOURCES['connection-identity']));
98
128
  const TUI_SESSION_ACTIONS_COPY = resolveUiMessageCatalog(defineUiMessageCatalog()(TUI_COPY_RESOURCES['session-actions']));
129
+ /**
130
+ * What the exhausted bound is called, in the user's language.
131
+ *
132
+ * The limit kinds are protocol tokens; a reader told the session exceeds the
133
+ * `record_bytes` limit has been handed an implementation detail instead of an
134
+ * explanation.
135
+ */
136
+ function externalImportLimitLabel(copy, kind) {
137
+ switch (kind) {
138
+ case 'transcript_bytes':
139
+ return copy.externalImportLimitTranscriptBytes;
140
+ case 'record_bytes':
141
+ return copy.externalImportLimitRecordBytes;
142
+ case 'records':
143
+ return copy.externalImportLimitRecords;
144
+ case 'converted_bytes':
145
+ return copy.externalImportLimitConvertedBytes;
146
+ case 'messages':
147
+ return copy.externalImportLimitMessages;
148
+ }
149
+ }
99
150
  function sessionConnectionIdentityNotice(session, identities, locale) {
100
151
  if (!identities)
101
152
  return undefined;
@@ -2445,14 +2496,258 @@ export async function runMakaPiTui(input) {
2445
2496
  throw error;
2446
2497
  }
2447
2498
  };
2499
+ const openImportedExternalSession = async (sessionId, failureText) => {
2500
+ try {
2501
+ await switchSession(sessionId);
2502
+ }
2503
+ catch {
2504
+ state.entries.push({ kind: 'notice', level: 'error', text: failureText });
2505
+ requestRender();
2506
+ return;
2507
+ }
2508
+ try {
2509
+ await discardCurrentSidePair();
2510
+ }
2511
+ catch (error) {
2512
+ reportError(error);
2513
+ }
2514
+ };
2515
+ const importExternalSession = async (adapterId, source) => {
2516
+ if (!input.externalSessions)
2517
+ return;
2518
+ const copy = TUI_SESSION_ACTIONS_COPY[locale];
2519
+ if (source.importState.isImporting) {
2520
+ state.entries.push({
2521
+ kind: 'notice',
2522
+ level: 'error',
2523
+ text: copy.externalUnavailable,
2524
+ });
2525
+ requestRender();
2526
+ return;
2527
+ }
2528
+ let importedSessionId;
2529
+ try {
2530
+ const result = await input.externalSessions.importSession({
2531
+ adapterId,
2532
+ sourceSessionId: source.id,
2533
+ });
2534
+ if (result.kind === 'source_limit_exceeded') {
2535
+ state.entries.push({
2536
+ kind: 'notice',
2537
+ level: 'error',
2538
+ text: formatUiMessage(copy.externalImportLimit, {
2539
+ kind: externalImportLimitLabel(copy, result.limit.kind),
2540
+ max: result.limit.max,
2541
+ }, locale),
2542
+ });
2543
+ return;
2544
+ }
2545
+ importedSessionId = result.session.id;
2546
+ }
2547
+ catch (error) {
2548
+ const code = externalImportErrorCode(error);
2549
+ if (code === 'model_unavailable') {
2550
+ state.entries.push({
2551
+ kind: 'notice',
2552
+ level: 'error',
2553
+ text: copy.externalImportModelUnavailable,
2554
+ });
2555
+ return;
2556
+ }
2557
+ if (code === 'source_unreadable') {
2558
+ state.entries.push({
2559
+ kind: 'notice',
2560
+ level: 'error',
2561
+ text: copy.externalImportSourceUnreadable,
2562
+ });
2563
+ return;
2564
+ }
2565
+ if (code !== 'commit_outcome_unknown') {
2566
+ state.entries.push({ kind: 'notice', level: 'error', text: copy.externalImportFailed });
2567
+ return;
2568
+ }
2569
+ state.entries.push({ kind: 'notice', level: 'error', text: copy.externalImportUncertain });
2570
+ return;
2571
+ }
2572
+ await openImportedExternalSession(importedSessionId, formatUiMessage(copy.externalOpenFailed, { sessionId: importedSessionId }, locale));
2573
+ };
2574
+ const showExternalSessionPage = async (adapterId, initialScope) => {
2575
+ if (!input.externalSessions || closed)
2576
+ return;
2577
+ const copy = TUI_SESSION_ACTIONS_COPY[locale];
2578
+ let scope = initialScope;
2579
+ let query = '';
2580
+ let sessions = [];
2581
+ let nextCursor = null;
2582
+ let revision = 0;
2583
+ let overlay;
2584
+ let search;
2585
+ let byValue = new Map();
2586
+ const closeOverlay = () => overlay?.hide();
2587
+ const toggleScope = () => {
2588
+ const alternate = input.externalSessions
2589
+ ?.listScopes()
2590
+ .find((candidate) => candidate !== scope);
2591
+ if (!alternate)
2592
+ return;
2593
+ scope = alternate;
2594
+ void load(false);
2595
+ };
2596
+ const render = () => {
2597
+ // SelectList has no disabled-row contract. A Host-owned in-flight import
2598
+ // disables another import, but it does not disable opening an already
2599
+ // published task.
2600
+ const selectable = sessions.filter((session) => !session.importState.isImporting || session.importState.importedSessionIds.length > 0);
2601
+ byValue = new Map(selectable.map((session) => [`external:${adapterId}:${session.id}`, session]));
2602
+ const choices = selectable.map((session) => ({
2603
+ item: {
2604
+ value: `external:${adapterId}:${session.id}`,
2605
+ label: session.name,
2606
+ description: [
2607
+ session.hostCwd,
2608
+ session.importState.importedCount > 0
2609
+ ? formatUiMessage(copy.externalImportedCount, { count: session.importState.importedCount }, locale)
2610
+ : undefined,
2611
+ ]
2612
+ .filter(Boolean)
2613
+ .join(' · '),
2614
+ },
2615
+ searchText: '',
2616
+ }));
2617
+ if (nextCursor) {
2618
+ choices.push({
2619
+ item: { value: 'external:load-more', label: copy.externalLoadMore },
2620
+ searchText: '',
2621
+ });
2622
+ }
2623
+ const alternateScope = input.externalSessions
2624
+ ?.listScopes()
2625
+ .find((candidate) => candidate !== scope);
2626
+ if (alternateScope) {
2627
+ choices.push({
2628
+ item: {
2629
+ value: 'external:toggle-workspace',
2630
+ label: alternateScope === 'all' ? copy.externalAllWorkspaces : copy.externalCurrentWorkspace,
2631
+ },
2632
+ searchText: '',
2633
+ });
2634
+ }
2635
+ const scopeLabel = scope === 'all' ? copy.externalAllWorkspaces : copy.externalCurrentWorkspace;
2636
+ const notice = sessions.length > 0 && selectable.length === 0 ? copy.externalUnavailable : undefined;
2637
+ if (search) {
2638
+ search.updateChoices(choices, scopeLabel, notice);
2639
+ search.invalidate();
2640
+ return;
2641
+ }
2642
+ search = new SessionSearchOverlay(tui, {
2643
+ locale,
2644
+ choices,
2645
+ scopeLabel,
2646
+ title: formatUiMessage(copy.externalSessionTitle, { source: externalSourceLabel(adapterId) }, locale),
2647
+ emptyText: sessions.length === 0 ? copy.externalEmpty : copy.externalUnavailable,
2648
+ notice,
2649
+ onQuery: (text) => {
2650
+ query = text;
2651
+ void load(false);
2652
+ },
2653
+ onSelect: (item) => {
2654
+ if (item.value === 'external:load-more' && nextCursor) {
2655
+ void load(true, nextCursor);
2656
+ return;
2657
+ }
2658
+ if (item.value === 'external:toggle-workspace' && alternateScope) {
2659
+ toggleScope();
2660
+ return;
2661
+ }
2662
+ const source = byValue.get(item.value);
2663
+ if (!source)
2664
+ return;
2665
+ closeOverlay();
2666
+ if (busy || turnRunning) {
2667
+ state.entries.push({ kind: 'notice', level: 'error', text: copy.externalImportBusy });
2668
+ requestRender();
2669
+ return;
2670
+ }
2671
+ const latestImportedSessionId = source.importState.importedSessionIds[0];
2672
+ if (latestImportedSessionId === undefined) {
2673
+ void runControl(() => importExternalSession(adapterId, source));
2674
+ return;
2675
+ }
2676
+ const actionTitle = formatUiMessage(copy.externalImportedActionsTitle, { source: source.name }, locale);
2677
+ const actions = [
2678
+ { value: 'open-latest', label: copy.externalOpenLatestImported },
2679
+ ...(!source.importState.isImporting
2680
+ ? [{ value: 'import-again', label: copy.externalImportAgain }]
2681
+ : []),
2682
+ ];
2683
+ showSelectPicker(actionTitle, source.name, actions, (action) => {
2684
+ if (busy || turnRunning) {
2685
+ state.entries.push({
2686
+ kind: 'notice',
2687
+ level: 'error',
2688
+ text: copy.externalImportBusy,
2689
+ });
2690
+ requestRender();
2691
+ return;
2692
+ }
2693
+ if (action.value === 'import-again') {
2694
+ void runControl(() => importExternalSession(adapterId, source));
2695
+ return;
2696
+ }
2697
+ void runControl(() => openImportedExternalSession(latestImportedSessionId, formatUiMessage(copy.externalOpenLatestFailed, { sessionId: latestImportedSessionId }, locale)));
2698
+ }, { minPrimaryColumnWidth: 24, maxPrimaryColumnWidth: 48 });
2699
+ },
2700
+ onCancel: closeOverlay,
2701
+ onToggleScope: toggleScope,
2702
+ });
2703
+ overlay = showBottomPicker(search);
2704
+ };
2705
+ const load = async (append, cursor) => {
2706
+ const requestRevision = ++revision;
2707
+ try {
2708
+ const page = await input.externalSessions.listSessions({
2709
+ adapterId,
2710
+ scope,
2711
+ ...(cursor ? { cursor } : {}),
2712
+ ...(query ? { text: query } : {}),
2713
+ });
2714
+ if (requestRevision !== revision || closed || turnRunning)
2715
+ return;
2716
+ sessions = append ? [...sessions, ...page.sessions] : page.sessions;
2717
+ nextCursor = page.nextCursor;
2718
+ render();
2719
+ }
2720
+ catch {
2721
+ if (requestRevision !== revision)
2722
+ return;
2723
+ state.entries.push({ kind: 'notice', level: 'error', text: copy.externalCatalogFailed });
2724
+ requestRender();
2725
+ }
2726
+ };
2727
+ await load(false);
2728
+ };
2729
+ const showExternalSourcePicker = (adapterIds) => {
2730
+ if (!input.externalSessions)
2731
+ return;
2732
+ const initialScope = input.externalSessions.listScopes()[0];
2733
+ if (adapterIds.length === 1) {
2734
+ void showExternalSessionPage(adapterIds[0], initialScope);
2735
+ return;
2736
+ }
2737
+ const copy = TUI_SESSION_ACTIONS_COPY[locale];
2738
+ showSelectPicker(copy.externalSourceTitle, copy.externalSourceTitle, adapterIds.map((adapterId) => ({
2739
+ value: adapterId,
2740
+ label: externalSourceLabel(adapterId),
2741
+ })), (item) => void showExternalSessionPage(item.value, initialScope), { minPrimaryColumnWidth: 20, maxPrimaryColumnWidth: 40 });
2742
+ };
2448
2743
  const showSessionList = async () => {
2449
2744
  const sessions = await input.driver.listSessions();
2450
2745
  const sessionTree = projectRevisionLinkedSessionTree(sessions, input.driver.getSessionId() ?? undefined);
2451
2746
  const projectedSessions = flattenLinkedSessionTree(sessionTree.roots, sessionTree.childrenByParentId);
2452
- // Maka-session availability and the foreign scan are independent I/O; run
2747
+ // Maka-session availability and Host source discovery are independent I/O; run
2453
2748
  // them concurrently so the picker's open latency is the slower of the two,
2454
2749
  // not their sum.
2455
- const [availabilityEntries, foreignScan] = await Promise.all([
2750
+ const [availabilityEntries, externalSourceQuery] = await Promise.all([
2456
2751
  Promise.all(sessions.map(async (session) => {
2457
2752
  return [
2458
2753
  session.id,
@@ -2460,36 +2755,18 @@ export async function runMakaPiTui(input) {
2460
2755
  (await inspectSessionResumeAvailability(session)),
2461
2756
  ];
2462
2757
  })),
2463
- // Foreign (Claude Code / Codex) rows are an import flow: it starts a NEW
2464
- // Session and hands off a turn, which cannot detach from the running
2465
- // one (#3380). Skip the scan mid-turn instead of offering rows whose
2466
- // selection would silently no-op on importForeignSession's busy guard.
2467
- input.foreignSessions && !turnRunning
2468
- ? input.foreignSessions.listSessions({ cwd }).then((summaries) => ({ summaries }), (error) => ({ error }))
2469
- : Promise.resolve({ summaries: [] }),
2758
+ input.externalSessions && !turnRunning
2759
+ ? input.externalSessions.listSources().then((adapterIds) => ({ adapterIds }), () => ({ error: true }))
2760
+ : Promise.resolve({ adapterIds: [] }),
2470
2761
  ]);
2471
2762
  const availability = new Map(availabilityEntries);
2472
- // Foreign (Claude Code / Codex) sessions for the current cwd, keyed by a
2473
- // prefixed select value so they never collide with Maka session ids. A scan
2474
- // error is surfaced (not silently swallowed): degrade to no rows but tell
2475
- // the user why, so a real store bug isn't mistaken for "no sessions".
2476
- // Deliberate #2672 exception: the interpolated detail is a local
2477
- // file-scan diagnostic (not backend copy) and the TUI has no log channel
2478
- // to carry it, so dropping it would hide the only debugging signal.
2479
- const foreignByValue = new Map();
2480
- if ('error' in foreignScan) {
2481
- const detail = foreignScan.error instanceof Error ? foreignScan.error.message : String(foreignScan.error);
2763
+ if ('error' in externalSourceQuery) {
2482
2764
  state.entries.push({
2483
2765
  kind: 'notice',
2484
2766
  level: 'error',
2485
- text: formatUiMessage(TUI_SESSION_ACTIONS_COPY[locale].foreignScanFailed, { detail }, locale),
2767
+ text: TUI_SESSION_ACTIONS_COPY[locale].externalCatalogFailed,
2486
2768
  });
2487
2769
  }
2488
- else {
2489
- for (const summary of foreignScan.summaries) {
2490
- foreignByValue.set(`foreign:${summary.source}:${summary.id}`, summary);
2491
- }
2492
- }
2493
2770
  let overlay;
2494
2771
  let sessionSearch;
2495
2772
  const renderScope = () => {
@@ -2525,16 +2802,14 @@ export async function runMakaPiTui(input) {
2525
2802
  .toLocaleLowerCase(),
2526
2803
  };
2527
2804
  });
2528
- // Foreign sessions are cwd-scoped; show them in both scope views (they
2529
- // belong to this project) so a Tab toggle never makes them vanish.
2530
- for (const [value, summary] of foreignByValue) {
2805
+ if ('adapterIds' in externalSourceQuery && externalSourceQuery.adapterIds.length > 0) {
2531
2806
  choices.push({
2532
2807
  item: {
2533
- value,
2534
- label: summary.title,
2535
- description: `↩ resume from ${foreignSourceLabel(summary.source)}`,
2808
+ value: 'external:import',
2809
+ label: TUI_SESSION_ACTIONS_COPY[locale].externalImport,
2810
+ description: TUI_SESSION_ACTIONS_COPY[locale].externalImportDescription,
2536
2811
  },
2537
- searchText: `${summary.title} ${summary.id} ${summary.cwd} ${summary.source}`.toLocaleLowerCase(),
2812
+ searchText: TUI_SESSION_ACTIONS_COPY[locale].externalImport.toLocaleLowerCase(),
2538
2813
  });
2539
2814
  }
2540
2815
  const closeOverlay = () => {
@@ -2542,10 +2817,9 @@ export async function runMakaPiTui(input) {
2542
2817
  overlay?.hide();
2543
2818
  };
2544
2819
  const onSelect = (item) => {
2545
- const foreign = foreignByValue.get(item.value);
2546
- if (foreign) {
2820
+ if (item.value === 'external:import' && 'adapterIds' in externalSourceQuery) {
2547
2821
  closeOverlay();
2548
- void importForeignSession(foreign);
2822
+ showExternalSourcePicker(externalSourceQuery.adapterIds);
2549
2823
  return;
2550
2824
  }
2551
2825
  if (availability.get(item.value)?.available === false)
@@ -2654,46 +2928,6 @@ export async function runMakaPiTui(input) {
2654
2928
  requestRender();
2655
2929
  return true;
2656
2930
  };
2657
- // Import a foreign (Claude Code / Codex) session: read its digest, open a
2658
- // fresh Maka session, and seed the first turn with an untrusted handoff
2659
- // envelope. Mirrors submitPreparedUserPrompt: claim `busy` + an activity lease
2660
- // SYNCHRONOUSLY before the async read so no other turn (a Goal auto-
2661
- // continuation, or a user Enter) can start during it and make the import a
2662
- // silent no-op. runAgentTurn re-asserts busy for the turn; on any failure the
2663
- // finally releases the lease. The handoff is the model-facing `sendText`; a
2664
- // short line shows in the transcript.
2665
- const importForeignSession = async (summary) => {
2666
- if (busy || input.foreignSessions === undefined)
2667
- return;
2668
- busy = true;
2669
- const activity = beginActivity();
2670
- editor.disableSubmit = true;
2671
- let handedOff = false;
2672
- try {
2673
- const digest = await input.foreignSessions.readDigest(summary);
2674
- if (closed)
2675
- return;
2676
- if (!(await newSession()))
2677
- return;
2678
- submitMessage(foreignSessionHandoffDisplayText(digest), 'current_turn', {
2679
- modelText: buildForeignSessionHandoffMessage(digest),
2680
- });
2681
- handedOff = true;
2682
- }
2683
- catch (error) {
2684
- if (closed)
2685
- return;
2686
- reportError(error);
2687
- }
2688
- finally {
2689
- if (!handedOff) {
2690
- busy = false;
2691
- editor.disableSubmit = false;
2692
- requestRender();
2693
- }
2694
- activity.finish();
2695
- }
2696
- };
2697
2931
  const showHelp = () => {
2698
2932
  // Derive the command list from the registry so /help never drifts from the
2699
2933
  // real commands. Keybindings are not commands, so they are listed by hand.
@@ -51,7 +51,8 @@ export async function runRuntimeHostInstalledUpdateBootstrap(input, overrides =
51
51
  if (!(await stat(coordinatorCliPath)).isFile()) {
52
52
  throw new Error('The copied Maka update coordinator has no CLI entry point');
53
53
  }
54
- return deps.runCoordinator({
54
+ // The copied entry must outlive the coordinator that is executing it.
55
+ const exitCode = await deps.runCoordinator({
55
56
  coordinatorCliPath,
56
57
  rootPath: input.rootPath,
57
58
  archivePath,
@@ -66,6 +67,7 @@ export async function runRuntimeHostInstalledUpdateBootstrap(input, overrides =
66
67
  allowInterruptActiveTasks: input.allowInterruptActiveTasks,
67
68
  ...(input.expectedSource ? { expectedSource: input.expectedSource } : {}),
68
69
  });
70
+ return exitCode;
69
71
  }
70
72
  finally {
71
73
  await rm(temporaryRoot, { recursive: true, force: true }).catch(() => undefined);
@@ -347,7 +347,10 @@ class RuntimeHostRunRuntime {
347
347
  const active = this.#activeTurn;
348
348
  if (!active || active.sessionId !== sessionId || active.turnId !== turnId)
349
349
  return;
350
- active.outcome = classifierFromStoredTurn(messages, turnId, active.runId);
350
+ // A read of a running Turn stops wherever the transcript has been
351
+ // committed, so it can restore what the live stream missed but never
352
+ // proves that what the stream already delivered is gone.
353
+ acceptStoredTurn(active.outcome, messages, turnId);
351
354
  }
352
355
  #waitForGraphTurnTerminal(turnId) {
353
356
  if (this.#closed)
@@ -409,6 +412,7 @@ class TurnOutcomeClassifier {
409
412
  #outcomeId;
410
413
  #unresolvedSandboxFailures = new Set();
411
414
  #finalOutput;
415
+ #finalOutputFromLive = false;
412
416
  #terminal;
413
417
  constructor(outcomeId) {
414
418
  this.#outcomeId = outcomeId;
@@ -418,7 +422,14 @@ class TurnOutcomeClassifier {
418
422
  case undefined:
419
423
  return;
420
424
  case 'output':
425
+ // A subscriber is delivered in Host order, so the stream's last answer
426
+ // is the Turn's last answer. A transcript read stops wherever the Host
427
+ // had committed, so it can supply an answer the stream never carried
428
+ // but can never overrule one it did.
429
+ if (observation.source === 'stored' && this.#finalOutputFromLive)
430
+ return;
421
431
  this.#finalOutput = observation.text;
432
+ this.#finalOutputFromLive = observation.source === 'live';
422
433
  return;
423
434
  case 'terminal':
424
435
  if (observation.update === 'replace' || this.#terminal === undefined) {
@@ -459,7 +470,7 @@ class TurnOutcomeClassifier {
459
470
  }
460
471
  function observationFromSessionEvent(event) {
461
472
  if (event.type === 'text_complete' && event.text.trim().length > 0) {
462
- return { kind: 'output', text: event.text };
473
+ return { kind: 'output', text: event.text, source: 'live' };
463
474
  }
464
475
  if (event.type === 'error') {
465
476
  return {
@@ -484,7 +495,7 @@ function observationFromSessionEvent(event) {
484
495
  }
485
496
  function observationFromStoredMessage(message) {
486
497
  if (message.type === 'assistant' && message.text.trim().length > 0) {
487
- return { kind: 'output', text: message.text };
498
+ return { kind: 'output', text: message.text, source: 'stored' };
488
499
  }
489
500
  if (message.type === 'turn_state' && message.status === 'completed') {
490
501
  return { kind: 'terminal', update: 'replace', status: 'completed' };
@@ -556,11 +567,14 @@ function outcomeFromStoredTurn(messages, turnId) {
556
567
  }
557
568
  function classifierFromStoredTurn(messages, turnId, outcomeId) {
558
569
  const classifier = new TurnOutcomeClassifier(outcomeId);
570
+ acceptStoredTurn(classifier, messages, turnId);
571
+ return classifier;
572
+ }
573
+ function acceptStoredTurn(classifier, messages, turnId) {
559
574
  for (const message of messages) {
560
575
  if (message.turnId === turnId)
561
576
  classifier.accept(observationFromStoredMessage(message));
562
577
  }
563
- return classifier;
564
578
  }
565
579
  class NonInteractiveInteractionController {
566
580
  #driver;