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

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 (78) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +3 -3
  3. package/dist/pi-tui-pickers.js +5 -2
  4. package/dist/pi-tui-runner.js +211 -33
  5. package/dist/runtime-host-session-driver.js +91 -13
  6. package/dist/session-catalog-limits.js +22 -0
  7. package/dist/session-driver.js +10 -0
  8. package/dist/tui-copy-catalog.js +15 -0
  9. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  10. package/node_modules/@maka/core/dist/bot-chat-settings.js +3 -0
  11. package/node_modules/@maka/core/dist/bot-events.js +1 -8
  12. package/node_modules/@maka/core/dist/local-memory.js +38 -1
  13. package/node_modules/@maka/core/dist/scheduled-task.js +14 -15
  14. package/node_modules/@maka/core/dist/session.js +3 -0
  15. package/node_modules/@maka/core/dist/text-sanitize.js +8 -3
  16. package/node_modules/@maka/runtime/dist/agent-graph-supervisor-wake.js +115 -17
  17. package/node_modules/@maka/runtime/dist/agent-run.js +11 -0
  18. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +11 -0
  19. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +13 -5
  20. package/node_modules/@maka/runtime/dist/bots/proxied-fetch.js +29 -1
  21. package/node_modules/@maka/runtime/dist/computer-use-observation-text.js +22 -0
  22. package/node_modules/@maka/runtime/dist/conversation-copy-agent-output.js +227 -0
  23. package/node_modules/@maka/runtime/dist/conversation-copy.js +176 -46
  24. package/node_modules/@maka/runtime/dist/file-stable-write.js +15 -3
  25. package/node_modules/@maka/runtime/dist/filesystem-worker/resource-resolver.js +3 -1
  26. package/node_modules/@maka/runtime/dist/model-adapter.js +20 -1
  27. package/node_modules/@maka/runtime/dist/network/proxy-test.js +28 -1
  28. package/node_modules/@maka/runtime/dist/network/scoped-fetch-transport.js +13 -2
  29. package/node_modules/@maka/runtime/dist/owned-child-process.js +209 -0
  30. package/node_modules/@maka/runtime/dist/owned-process-main.js +183 -0
  31. package/node_modules/@maka/runtime/dist/pipe-process-driver.js +19 -25
  32. package/node_modules/@maka/runtime/dist/runtime-kernel.js +216 -8
  33. package/node_modules/@maka/runtime/dist/sandbox-boundary-tool.js +12 -0
  34. package/node_modules/@maka/runtime/dist/session-manager.js +156 -17
  35. package/node_modules/@maka/runtime/dist/shell-exec.js +18 -20
  36. package/node_modules/@maka/runtime/dist/shell-run-manager.js +2 -1
  37. package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +279 -8
  38. package/node_modules/@maka/runtime/dist/stream-graph-schedule-reconcile.js +209 -37
  39. package/node_modules/@maka/runtime/dist/tool-availability.js +55 -18
  40. package/node_modules/@maka/runtime/dist/tool-call-snapshot.js +142 -0
  41. package/node_modules/@maka/runtime/dist/tool-runtime.js +27 -84
  42. package/node_modules/@maka/runtime/dist/workers/{filesystem-worker.js → filesystem-worker.mjs} +12 -1
  43. package/node_modules/@maka/runtime-host/dist/client/connect-or-spawn.js +30 -6
  44. package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
  45. package/node_modules/@maka/runtime-host/dist/client/reconnect-lifecycle.js +44 -13
  46. package/node_modules/@maka/runtime-host/dist/client/session-catalog-summary.js +6 -0
  47. package/node_modules/@maka/runtime-host/dist/client/wsl-environment.js +4 -0
  48. package/node_modules/@maka/runtime-host/dist/protocol/index.js +5 -1
  49. package/node_modules/@maka/runtime-host/dist/protocol/message.js +1 -13
  50. package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +46 -9
  51. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +1 -13
  52. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +1 -13
  53. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +4 -7
  54. package/node_modules/@maka/runtime-host/dist/server/archive-retention-coordinator.js +81 -105
  55. package/node_modules/@maka/runtime-host/dist/server/candidate.js +47 -40
  56. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +87 -67
  57. package/node_modules/@maka/runtime-host/dist/server/execution-service.js +5 -1
  58. package/node_modules/@maka/runtime-host/dist/server/host-maintenance-guards.js +95 -0
  59. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +18 -8
  60. package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +32 -3
  61. package/node_modules/@maka/runtime-host/dist/server/project-catalog-coordinator.js +2 -0
  62. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +109 -8
  63. package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +20 -3
  64. package/node_modules/@maka/runtime-host/dist/server/session-background-activity.js +71 -0
  65. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +8 -4
  66. package/node_modules/@maka/runtime-host/dist/server/session-interaction-activity.js +78 -0
  67. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +56 -0
  68. package/node_modules/@maka/runtime-host/dist/server/session-revision-graph-references.js +16 -2
  69. package/node_modules/@maka/runtime-host/dist/server/skill-catalog-coordinator.js +2 -0
  70. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +19 -24
  71. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +74 -19
  72. package/node_modules/@maka/runtime-host/dist/server/workhub-result-coordinator.js +1 -1
  73. package/node_modules/@maka/runtime-host/dist/server/workhub-result-runtime.js +6 -99
  74. package/node_modules/@maka/runtime-host/dist/server/workhub-target-execution-authority.js +14 -126
  75. package/node_modules/@maka/storage/dist/context-offload-store.js +8 -3
  76. package/node_modules/@maka/storage/dist/git-entry.js +142 -5
  77. package/node_modules/@maka/storage/dist/session-bundle-policy.js +54 -19
  78. package/package.json +2 -2
package/README.md CHANGED
@@ -65,7 +65,7 @@ https://incubator.apache.org/projects/maka.html
65
65
 
66
66
  ## Requirements
67
67
 
68
- - Node.js 22.19.0 or newer;
68
+ - Node.js 22.19.0 or newer on the 22 line, or 24.0.0 or newer (Node 23 is not supported);
69
69
  - a terminal with interactive input for the TUI;
70
70
  - a configured model connection for agent turns; first-run setup currently supports API-key
71
71
  providers.
package/README.zh-CN.md CHANGED
@@ -1,4 +1,4 @@
1
- > **开发快照:**此 Nightly 不是 Apache Release,不用于生产环境;其版本只对应一个精确的 `apache/maka` commit。
1
+ > **开发快照**:此 Nightly 不是 Apache Release,不用于生产环境;其版本只对应一个精确的 `apache/maka` commit。
2
2
 
3
3
  <!--
4
4
  Licensed to the Apache Software Foundation (ASF) under one
@@ -35,11 +35,11 @@ release commit 中的 [DISCLAIMER-WIP](https://github.com/apache/maka/blob/main/
35
35
  [Maka podling 状态页](https://incubator.apache.org/projects/maka.html)。本段仅为中文说明,
36
36
  英文免责声明以随包发布的 `DISCLAIMER-WIP` 为准。
37
37
 
38
- > **Beta:**CLI 仍在积极开发中,稳定版发布前,命令和本地数据格式可能发生变化。
38
+ > **Beta**:CLI 仍在积极开发中,稳定版发布前,命令和本地数据格式可能发生变化。
39
39
 
40
40
  ## 环境要求
41
41
 
42
- - Node.js 22.19.0 或更高版本;
42
+ - Node.js 22.19.0 或更高的 22.x 版本,或 24.0.0 及以上版本(不支持 Node 23);
43
43
  - 使用 TUI 时需要支持交互输入的终端;
44
44
  - 执行 Agent Turn 时需要已经配置的模型连接;首次设置目前支持使用 API Key 的供应商。
45
45
 
@@ -569,6 +569,7 @@ export class SessionSearchOverlay {
569
569
  list;
570
570
  selectedValue;
571
571
  scopeLabel;
572
+ emptyText;
572
573
  notice;
573
574
  constructor(tui, input) {
574
575
  this.tui = tui;
@@ -577,15 +578,17 @@ export class SessionSearchOverlay {
577
578
  this.choices = input.choices;
578
579
  this.filtered = input.choices;
579
580
  this.scopeLabel = input.scopeLabel;
581
+ this.emptyText = input.emptyText;
580
582
  this.notice = input.notice;
581
583
  this.list = this.buildList();
582
584
  this.searchEditor = new Editor(tui, editorTheme(), { paddingX: 0 });
583
585
  this.searchEditor.onChange = (text) => this.applyQuery(text);
584
586
  }
585
- updateChoices(choices, scopeLabel, notice) {
587
+ updateChoices(choices, scopeLabel, notice, emptyText) {
586
588
  this.choices = choices;
587
589
  this.scopeLabel = scopeLabel;
588
590
  this.notice = notice;
591
+ this.emptyText = emptyText;
589
592
  if (this.input.onQuery) {
590
593
  this.filtered = choices;
591
594
  this.list = this.buildList();
@@ -659,7 +662,7 @@ export class SessionSearchOverlay {
659
662
  padLine('', safeWidth),
660
663
  ...(this.notice ? [padLine(ansi.dim(this.notice), safeWidth)] : []),
661
664
  ...(this.filtered.length === 0
662
- ? [padLine(ansi.dim(this.input.emptyText ?? this.copy.noMatchingSessions), safeWidth)]
665
+ ? [padLine(ansi.dim(this.emptyText ?? this.copy.noMatchingSessions), safeWidth)]
663
666
  : this.list.render(safeWidth).map((line) => formatPickerItemLine(line, safeWidth))),
664
667
  padLine(ansi.accent('-'.repeat(safeWidth)), safeWidth),
665
668
  ];
@@ -31,7 +31,8 @@ import { AUTO_RECAP_DISPLAY_LIMIT_BYTES, shouldAutoRecap } from './session-recap
31
31
  import { MakaSkillHighlightEditor } from './skill-highlight-editor.js';
32
32
  import { parseGraphCommand } from '@maka/core/graph-command';
33
33
  import { parseSwarmCommand } from '@maka/core/swarm-command';
34
- import { inspectSessionResumeAvailability, } from './session-driver.js';
34
+ import { inspectSessionResumeAvailability, MakaSessionCatalogIncompleteError, } from './session-driver.js';
35
+ import { SESSION_CATALOG_MAX_SCAN_SESSIONS } from './session-catalog-limits.js';
35
36
  import { SafeBoundaryResumeParkedError } from './runtime-host-session-driver.js';
36
37
  import { appendExpansionCollapseConfirmation, appendTurnFailureToTranscript, appendUserCommandToTranscript, appendUserPrompt, applyMakaSessionEventToTranscript, applyShellRunUpdateToTranscript, createMakaPiTranscriptState, hasRunningUserCommand, activeSandboxBoundaryRequest, activeFormRequest, activeUserQuestionRequest, applyExpansionDefaultToAll, completePendingInteraction, applyShellRunViewUpdateToTranscript, EXPANSION_COLLAPSE_CONFIRM_WINDOW_MS, hasExpandedEntriesAboveViewport, permissionModeLabel, retireCancelledTransientMessages, replaceTranscriptWithStoredMessages, hydrateToolsWithStoredMessages, submitCompactToTranscript, toggleAllThinkingExpansion, toggleAllToolExpansion, } from './pi-transcript.js';
37
38
  import { FormInteractionOverlay } from './pi-tui-form-interaction.js';
@@ -169,6 +170,7 @@ function sessionConnectionIdentityNotice(session, identities, locale) {
169
170
  }
170
171
  return undefined;
171
172
  }
173
+ const SESSION_RESUME_AVAILABILITY_CONCURRENCY = 8;
172
174
  export async function runMakaPiTui(input) {
173
175
  const locale = input.locale ?? 'en';
174
176
  const pickerCopy = getTuiPickerCopy(locale);
@@ -1578,25 +1580,34 @@ export async function runMakaPiTui(input) {
1578
1580
  sessionId !== input.driver.getSessionId() &&
1579
1581
  (sessionId === pair.parentSessionId || sessionId === pair.sideSessionId)) {
1580
1582
  await toggleSideConversation();
1581
- return;
1583
+ return true;
1582
1584
  }
1583
1585
  const leavesPair = pair !== undefined && sessionId !== pair.parentSessionId && sessionId !== pair.sideSessionId;
1584
1586
  if (!turnRunning) {
1587
+ let switched = false;
1585
1588
  await runControl(async () => {
1586
1589
  await switchSession(sessionId);
1590
+ switched = true;
1587
1591
  if (leavesPair)
1588
1592
  await discardCurrentSidePair();
1589
1593
  });
1590
- return;
1594
+ return switched;
1591
1595
  }
1592
1596
  // One detach at a time (#3380): a second mid-turn switch while the first
1593
1597
  // is still handing the view over would clear `detaching` early, reopen
1594
1598
  // the interrupt window, and double-apply the adoption.
1595
1599
  if (detaching)
1596
- return;
1597
- await switchAwayMidTurn(sessionId)
1598
- .then(() => (leavesPair ? discardCurrentSidePair() : undefined))
1599
- .catch(reportError);
1600
+ return false;
1601
+ try {
1602
+ await switchAwayMidTurn(sessionId);
1603
+ if (leavesPair)
1604
+ await discardCurrentSidePair();
1605
+ return true;
1606
+ }
1607
+ catch (error) {
1608
+ reportError(error);
1609
+ return false;
1610
+ }
1600
1611
  };
1601
1612
  const openSideConversation = async (prompt) => {
1602
1613
  if (sideConversation) {
@@ -2460,14 +2471,50 @@ export async function runMakaPiTui(input) {
2460
2471
  onChange: requestRender,
2461
2472
  });
2462
2473
  };
2474
+ let sessionListPromise;
2475
+ let activeResumeAvailabilityChecks = 0;
2476
+ const queuedResumeAvailabilityChecks = [];
2477
+ const runResumeAvailabilityCheck = async (task) => {
2478
+ if (activeResumeAvailabilityChecks >= SESSION_RESUME_AVAILABILITY_CONCURRENCY) {
2479
+ await new Promise((resolve) => queuedResumeAvailabilityChecks.push(resolve));
2480
+ }
2481
+ activeResumeAvailabilityChecks += 1;
2482
+ try {
2483
+ return await task();
2484
+ }
2485
+ finally {
2486
+ activeResumeAvailabilityChecks -= 1;
2487
+ queuedResumeAvailabilityChecks.shift()?.();
2488
+ }
2489
+ };
2490
+ const listSessions = (options = {}) => {
2491
+ if (sessionListPromise) {
2492
+ if (sessionListPromise.limit === options.limit && sessionListPromise.cwd === options.cwd) {
2493
+ return sessionListPromise.promise;
2494
+ }
2495
+ return sessionListPromise.promise.then(() => listSessions(options), () => listSessions(options));
2496
+ }
2497
+ {
2498
+ const promise = input.driver.listSessions(options).finally(() => {
2499
+ if (sessionListPromise?.promise === promise)
2500
+ sessionListPromise = undefined;
2501
+ });
2502
+ sessionListPromise = { limit: options.limit, cwd: options.cwd, promise };
2503
+ }
2504
+ return sessionListPromise.promise;
2505
+ };
2463
2506
  const resumeSession = async () => {
2507
+ if (!input.driver.getSessionId()) {
2508
+ await showSessionList({ onlyResumable: true });
2509
+ return;
2510
+ }
2464
2511
  if (!input.driver.resumeLatest) {
2465
- throw new Error('Safe-boundary resume is unavailable on this runtime.');
2512
+ throw new Error(pickerCopy.resumeUnavailableNotice);
2466
2513
  }
2467
2514
  state.entries.push({
2468
2515
  kind: 'notice',
2469
2516
  level: 'info',
2470
- text: 'Resuming from the latest safe boundary…',
2517
+ text: pickerCopy.resumeStartingNotice,
2471
2518
  });
2472
2519
  requestRender();
2473
2520
  try {
@@ -2784,26 +2831,97 @@ export async function runMakaPiTui(input) {
2784
2831
  label: externalSourceLabel(adapterId),
2785
2832
  })), (item) => void showExternalSessionPage(item.value, initialScope), { minPrimaryColumnWidth: 20, maxPrimaryColumnWidth: 40 });
2786
2833
  };
2787
- const showSessionList = async () => {
2788
- const sessions = await input.driver.listSessions();
2789
- const sessionTree = projectRevisionLinkedSessionTree(sessions, input.driver.getSessionId() ?? undefined);
2790
- const projectedSessions = flattenLinkedSessionTree(sessionTree.roots, sessionTree.childrenByParentId);
2834
+ const showSessionCatalogIncompleteNotice = () => {
2835
+ const text = pickerCopy.resumeCatalogIncompleteNotice;
2836
+ state.entries.push({ kind: 'notice', level: 'info', text });
2837
+ requestRender();
2838
+ };
2839
+ const readSessionCatalog = async (sessionOptions = {}, onIncomplete) => {
2840
+ try {
2841
+ return await listSessions(sessionOptions);
2842
+ }
2843
+ catch (error) {
2844
+ if (error instanceof MakaSessionCatalogIncompleteError) {
2845
+ onIncomplete?.();
2846
+ return [...error.sessions];
2847
+ }
2848
+ throw error;
2849
+ }
2850
+ };
2851
+ const showSessionList = async (options = {}) => {
2852
+ let catalogIncompleteNoticeShown = false;
2853
+ const readPickerSessionCatalog = (sessionOptions) => readSessionCatalog(sessionOptions, () => {
2854
+ if (catalogIncompleteNoticeShown)
2855
+ return;
2856
+ catalogIncompleteNoticeShown = true;
2857
+ showSessionCatalogIncompleteNotice();
2858
+ });
2859
+ let sessions = await readPickerSessionCatalog(options.onlyResumable ? { limit: SESSION_CATALOG_MAX_SCAN_SESSIONS, cwd } : {});
2860
+ let sessionTree = projectRevisionLinkedSessionTree(sessions, input.driver.getSessionId() ?? undefined);
2861
+ let projectedSessions = flattenLinkedSessionTree(sessionTree.roots, sessionTree.childrenByParentId);
2862
+ const resumeCandidateCheckFailures = new Set();
2863
+ const checkSessionAvailability = async (session) => {
2864
+ try {
2865
+ const result = await runResumeAvailabilityCheck(async () => {
2866
+ if (!session.cwd) {
2867
+ return [session.id, { available: false, reason: 'Missing working directory' }];
2868
+ }
2869
+ if (options.onlyResumable) {
2870
+ if (!input.driver.getSessionResumeCandidateAvailability) {
2871
+ resumeCandidateCheckFailures.add(session.id);
2872
+ return [
2873
+ session.id,
2874
+ {
2875
+ available: false,
2876
+ reason: pickerCopy.resumeUnavailableNotice,
2877
+ },
2878
+ ];
2879
+ }
2880
+ return [
2881
+ session.id,
2882
+ await input.driver.getSessionResumeCandidateAvailability(session),
2883
+ ];
2884
+ }
2885
+ const availability = (await input.driver.getSessionResumeAvailability?.(session)) ??
2886
+ (await inspectSessionResumeAvailability(session));
2887
+ return [session.id, availability];
2888
+ });
2889
+ if (options.onlyResumable && input.driver.getSessionResumeCandidateAvailability) {
2890
+ resumeCandidateCheckFailures.delete(session.id);
2891
+ }
2892
+ return result;
2893
+ }
2894
+ catch (error) {
2895
+ if (options.onlyResumable)
2896
+ resumeCandidateCheckFailures.add(session.id);
2897
+ const detail = error instanceof Error ? error.message : String(error);
2898
+ return [session.id, { available: false, reason: detail }];
2899
+ }
2900
+ };
2901
+ // Recovery starts in the current workspace even for Host profiles whose
2902
+ // normal /session picker starts in All. The user can still opt into the
2903
+ // full catalog with the scope toggle after the current rows are ready.
2904
+ let pickerScope = options.onlyResumable ? 'current' : sessionListScope;
2905
+ const sessionsToCheck = (pickerScope === 'current' ? sessions.filter((session) => session.cwd === cwd) : sessions).slice(0, SESSION_CATALOG_MAX_SCAN_SESSIONS);
2791
2906
  // Maka-session availability and Host source discovery are independent I/O; run
2792
2907
  // them concurrently so the picker's open latency is the slower of the two,
2793
2908
  // not their sum.
2794
2909
  const [availabilityEntries, externalSourceQuery] = await Promise.all([
2795
- Promise.all(sessions.map(async (session) => {
2796
- return [
2797
- session.id,
2798
- (await input.driver.getSessionResumeAvailability?.(session)) ??
2799
- (await inspectSessionResumeAvailability(session)),
2800
- ];
2801
- })),
2802
- input.externalSessions && !turnRunning
2910
+ Promise.all(sessionsToCheck.map(checkSessionAvailability)),
2911
+ !options.onlyResumable && input.externalSessions && !turnRunning
2803
2912
  ? input.externalSessions.listSources().then((adapterIds) => ({ adapterIds }), () => ({ error: true }))
2804
2913
  : Promise.resolve({ adapterIds: [] }),
2805
2914
  ]);
2806
2915
  const availability = new Map(availabilityEntries);
2916
+ const ensureAvailabilityForScope = async (scope) => {
2917
+ const scopeSessions = (scope === 'current' ? sessions.filter((session) => session.cwd === cwd) : sessions).slice(0, SESSION_CATALOG_MAX_SCAN_SESSIONS);
2918
+ const missingSessions = scopeSessions.filter((session) => !availability.has(session.id));
2919
+ if (missingSessions.length === 0)
2920
+ return;
2921
+ for (const [sessionId, sessionAvailability] of await Promise.all(missingSessions.map(checkSessionAvailability))) {
2922
+ availability.set(sessionId, sessionAvailability);
2923
+ }
2924
+ };
2807
2925
  if ('error' in externalSourceQuery) {
2808
2926
  state.entries.push({
2809
2927
  kind: 'notice',
@@ -2814,14 +2932,22 @@ export async function runMakaPiTui(input) {
2814
2932
  let overlay;
2815
2933
  let sessionSearch;
2816
2934
  const renderScope = () => {
2817
- const visibleSessions = sessionListScope === 'current'
2935
+ const visibleSessions = pickerScope === 'current'
2818
2936
  ? projectedSessions.filter(({ session }) => session.cwd === cwd)
2819
2937
  : projectedSessions;
2820
- const choices = visibleSessions.map(({ session, depth }) => {
2938
+ const selectableSessions = options.onlyResumable
2939
+ ? visibleSessions.filter(({ session }) => availability.get(session.id)?.available === true)
2940
+ : visibleSessions;
2941
+ const emptyText = options.onlyResumable &&
2942
+ visibleSessions.length > 0 &&
2943
+ visibleSessions.every(({ session }) => resumeCandidateCheckFailures.has(session.id))
2944
+ ? pickerCopy.resumeCandidateCheckFailed
2945
+ : undefined;
2946
+ const choices = selectableSessions.map(({ session, depth }) => {
2821
2947
  const state = availability.get(session.id);
2822
2948
  const statusBadge = sessionStatusBadge(session, locale);
2823
2949
  const statusDetail = statusBadge ? ` · ${statusBadge}` : '';
2824
- const location = sessionListScope === 'all' && session.cwd ? ` ${session.cwd}` : '';
2950
+ const location = pickerScope === 'all' && session.cwd ? ` ${session.cwd}` : '';
2825
2951
  const childDetail = session.subagentRuntime
2826
2952
  ? ` subagent:${session.subagentRuntime.profile}`
2827
2953
  : '';
@@ -2846,7 +2972,9 @@ export async function runMakaPiTui(input) {
2846
2972
  .toLocaleLowerCase(),
2847
2973
  };
2848
2974
  });
2849
- if ('adapterIds' in externalSourceQuery && externalSourceQuery.adapterIds.length > 0) {
2975
+ if (!options.onlyResumable &&
2976
+ 'adapterIds' in externalSourceQuery &&
2977
+ externalSourceQuery.adapterIds.length > 0) {
2850
2978
  choices.push({
2851
2979
  item: {
2852
2980
  value: 'external:import',
@@ -2866,16 +2994,20 @@ export async function runMakaPiTui(input) {
2866
2994
  showExternalSourcePicker(externalSourceQuery.adapterIds);
2867
2995
  return;
2868
2996
  }
2869
- if (availability.get(item.value)?.available === false)
2997
+ const selectedSession = sessions.find((session) => session.id === item.value);
2998
+ if (selectedSession && !selectedSession.cwd) {
2870
2999
  return;
3000
+ }
2871
3001
  closeOverlay();
2872
- void goToSession(item.value);
3002
+ void (async () => {
3003
+ const switched = await goToSession(item.value);
3004
+ if (switched && options.onlyResumable)
3005
+ await runControl(resumeSession);
3006
+ })().catch(reportError);
2873
3007
  };
2874
- const scopeLabel = sessionListScope === 'current'
2875
- ? pickerCopy.sessionScopeCurrent
2876
- : pickerCopy.sessionScopeAll;
3008
+ const scopeLabel = pickerScope === 'current' ? pickerCopy.sessionScopeCurrent : pickerCopy.sessionScopeAll;
2877
3009
  if (sessionSearch) {
2878
- sessionSearch.updateChoices(choices, scopeLabel);
3010
+ sessionSearch.updateChoices(choices, scopeLabel, undefined, emptyText);
2879
3011
  sessionSearch.invalidate();
2880
3012
  return;
2881
3013
  }
@@ -2883,11 +3015,28 @@ export async function runMakaPiTui(input) {
2883
3015
  locale,
2884
3016
  choices,
2885
3017
  scopeLabel,
3018
+ emptyText,
2886
3019
  onSelect,
2887
3020
  onCancel: closeOverlay,
2888
3021
  onToggleScope: () => {
2889
- sessionListScope = sessionListScope === 'current' ? 'all' : 'current';
2890
- renderScope();
3022
+ const nextScope = pickerScope === 'current' ? 'all' : 'current';
3023
+ void (async () => {
3024
+ if (options.onlyResumable) {
3025
+ sessions = await readPickerSessionCatalog(nextScope === 'current'
3026
+ ? { limit: SESSION_CATALOG_MAX_SCAN_SESSIONS, cwd }
3027
+ : { limit: SESSION_CATALOG_MAX_SCAN_SESSIONS });
3028
+ sessionTree = projectRevisionLinkedSessionTree(sessions, input.driver.getSessionId() ?? undefined);
3029
+ projectedSessions = flattenLinkedSessionTree(sessionTree.roots, sessionTree.childrenByParentId);
3030
+ }
3031
+ await ensureAvailabilityForScope(nextScope);
3032
+ })()
3033
+ .then(() => {
3034
+ pickerScope = nextScope;
3035
+ if (!options.onlyResumable)
3036
+ sessionListScope = nextScope;
3037
+ renderScope();
3038
+ })
3039
+ .catch(reportError);
2891
3040
  },
2892
3041
  });
2893
3042
  sessionPickerOverlayOpen = true;
@@ -2895,6 +3044,34 @@ export async function runMakaPiTui(input) {
2895
3044
  };
2896
3045
  renderScope();
2897
3046
  };
3047
+ const announceResumeAvailability = async () => {
3048
+ const sessionId = input.driver.getSessionId();
3049
+ try {
3050
+ if (!input.driver.getSessionResumeCandidateAvailability)
3051
+ return;
3052
+ const sessions = sessionId
3053
+ ? undefined
3054
+ : await readSessionCatalog({ limit: SESSION_CATALOG_MAX_SCAN_SESSIONS, cwd });
3055
+ const session = sessionId
3056
+ ? ((await input.driver.getSessionSummary?.(sessionId)) ??
3057
+ (await readSessionCatalog()).find((candidate) => candidate.id === sessionId))
3058
+ : sessions?.find((candidate) => candidate.cwd === cwd);
3059
+ if (!session)
3060
+ return;
3061
+ const availability = await runResumeAvailabilityCheck(() => input.driver.getSessionResumeCandidateAvailability(session));
3062
+ if (availability.available) {
3063
+ state.entries.push({
3064
+ kind: 'notice',
3065
+ level: 'info',
3066
+ text: pickerCopy.resumeAvailabilityNotice,
3067
+ });
3068
+ requestRender();
3069
+ }
3070
+ }
3071
+ catch {
3072
+ // Resume discovery is advisory and must never prevent the TUI from starting.
3073
+ }
3074
+ };
2898
3075
  const showRewindPicker = async () => {
2899
3076
  const targets = await input.driver.listRewindTargets();
2900
3077
  if (targets.length === 0) {
@@ -4283,6 +4460,7 @@ export async function runMakaPiTui(input) {
4283
4460
  terminal.write(ENABLE_FOCUS_REPORTING);
4284
4461
  if (input.firstRun)
4285
4462
  void showSetupWizard();
4463
+ setTimeout(() => void announceResumeAvailability(), 0);
4286
4464
  }
4287
4465
  catch (error) {
4288
4466
  beginClose(error instanceof Error ? error : new Error(String(error)));
@@ -28,14 +28,19 @@ import { mergeShellRunUpdate } from '@maka/core/shell-run-result';
28
28
  import { isActiveShellRunStatus } from '@maka/core/shell-run';
29
29
  import { executionBoundaryDisplayMode } from '@maka/core/sandbox-boundary';
30
30
  import { isRuntimeHostTerminalTurn as isTerminalTurn } from '@maka/runtime-host/adapter';
31
- import { projectSessionCatalogSummary, readRuntimeHostResources, readRuntimeHostSessions, RuntimeHostOperationError, RuntimeHostRequestInterruptedError, } from '@maka/runtime-host/client';
31
+ import { projectSessionCatalogSummary, readRuntimeHostSessionCatalogPage, readRuntimeHostResources, readRuntimeHostSessions, RuntimeHostSessionCatalogRevisionChangedError, RuntimeHostOperationError, RuntimeHostRequestInterruptedError, } from '@maka/runtime-host/client';
32
32
  import { SESSION_TRANSCRIPT_BOOTSTRAP_MAX_BYTES, } from '@maka/runtime-host/protocol';
33
33
  import { RuntimeHostSessionChannel } from './runtime-host-session-channel.js';
34
+ import { SESSION_CATALOG_MAX_SCAN_PAGES } from './session-catalog-limits.js';
34
35
  import { getRuntimeHostSession, requireRuntimeHostSessionProjection as requireSession, updateRuntimeHostSession, } from './runtime-host-session-update.js';
35
- import { inspectSessionResumeAvailability, skillInvocationBlockedMessage, } from './session-driver.js';
36
+ import { MakaSessionCatalogIncompleteError, inspectSessionResumeAvailability, skillInvocationBlockedMessage, } from './session-driver.js';
36
37
  import { cwdRank, firstLine, inspectGitCwdChanges, resolveMoveCwd, } from './session-driver-policy.js';
37
38
  const decodeStoredMessage = (value) => decodePersistedStoredMessage(markPersisted(value));
38
39
  const MAX_CATALOG_ATTEMPTS = 3;
40
+ // Sparse cwd or visibility matches must not turn a bounded lookup into a full Host scan.
41
+ const MAX_SESSION_CATALOG_READ_ATTEMPTS = 8;
42
+ const SESSION_CATALOG_READ_RETRY_BASE_DELAY_MS = 8;
43
+ const SESSION_CATALOG_READ_RETRY_MAX_DELAY_MS = 64;
39
44
  /**
40
45
  * The host declined to start a safe-boundary continuation and explained why.
41
46
  * `reason` is the durable park reason from the `turn.resume` protocol, so
@@ -162,14 +167,16 @@ class RuntimeHostMakaSessionDriverImpl {
162
167
  const session = await this.#createSession(input.name ?? DEFAULT_SESSION_NAME);
163
168
  return projectSessionCatalogSummary(session);
164
169
  }
165
- async listSessions() {
166
- const sessions = (await readRuntimeHostSessions(this.#connection))
167
- .flatMap(representableSession)
168
- .filter((session) => !isSideConversationSession(session.labels))
169
- .map(projectSessionCatalogSummary);
170
+ async listSessions(options = {}) {
171
+ const sessions = options.cwd !== undefined || options.limit !== undefined
172
+ ? await this.#readBoundedSessionCatalog(options.limit, options.cwd)
173
+ : (await readRuntimeHostSessions(this.#connection))
174
+ .flatMap(representableSession)
175
+ .filter((session) => !isSideConversationSession(session.labels));
176
+ const summaries = sessions.map(projectSessionCatalogSummary);
170
177
  if (this.#executionLocation.kind === 'host')
171
- return sessions;
172
- return sessions
178
+ return summaries;
179
+ return summaries
173
180
  .map((session, index) => ({ session, index }))
174
181
  .sort((left, right) => {
175
182
  const cwdDelta = cwdRank(left.session, this.#workspace.hostCwd) -
@@ -178,9 +185,80 @@ class RuntimeHostMakaSessionDriverImpl {
178
185
  })
179
186
  .map(({ session }) => session);
180
187
  }
181
- getSessionResumeAvailability(session) {
188
+ async getSessionSummary(sessionId) {
189
+ const projection = await getRuntimeHostSession(this.#connection, sessionId);
190
+ if (!projection)
191
+ return undefined;
192
+ const [session] = representableSession(projection);
193
+ return session ? projectSessionCatalogSummary(session) : undefined;
194
+ }
195
+ async #readBoundedSessionCatalog(limit, cwd) {
196
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 0)) {
197
+ throw new Error(`Session catalog limit must be a non-negative safe integer: ${limit}`);
198
+ }
199
+ if (limit === 0)
200
+ return [];
201
+ for (let attempt = 0; attempt < MAX_SESSION_CATALOG_READ_ATTEMPTS; attempt += 1) {
202
+ try {
203
+ const sessions = [];
204
+ const cursors = new Set();
205
+ let cursor;
206
+ let pagesRead = 0;
207
+ while (pagesRead < SESSION_CATALOG_MAX_SCAN_PAGES) {
208
+ const page = await readRuntimeHostSessionCatalogPage(this.#connection, cursor);
209
+ pagesRead += 1;
210
+ for (const item of page.sessions) {
211
+ for (const session of representableSession(item)) {
212
+ if ((cwd === undefined || session.workspace.hostCwd === cwd) &&
213
+ !isSideConversationSession(session.labels)) {
214
+ sessions.push(session);
215
+ if (limit !== undefined && sessions.length >= limit)
216
+ break;
217
+ }
218
+ }
219
+ if (limit !== undefined && sessions.length >= limit)
220
+ break;
221
+ }
222
+ if (!page.nextCursor) {
223
+ cursor = undefined;
224
+ break;
225
+ }
226
+ cursor = page.nextCursor;
227
+ if (limit !== undefined && sessions.length >= limit) {
228
+ throw new MakaSessionCatalogIncompleteError(pagesRead, sessions.map(projectSessionCatalogSummary));
229
+ }
230
+ if (cursors.has(cursor.cursor)) {
231
+ throw new Error('Runtime Host Session catalog returned a repeated cursor');
232
+ }
233
+ cursors.add(cursor.cursor);
234
+ }
235
+ if (cursor !== undefined && pagesRead === SESSION_CATALOG_MAX_SCAN_PAGES) {
236
+ throw new MakaSessionCatalogIncompleteError(pagesRead, sessions.map(projectSessionCatalogSummary));
237
+ }
238
+ return sessions;
239
+ }
240
+ catch (error) {
241
+ if (!(error instanceof RuntimeHostSessionCatalogRevisionChangedError) ||
242
+ attempt + 1 === MAX_SESSION_CATALOG_READ_ATTEMPTS) {
243
+ throw error;
244
+ }
245
+ await new Promise((resolve) => setTimeout(resolve, Math.min(SESSION_CATALOG_READ_RETRY_BASE_DELAY_MS * 2 ** attempt, SESSION_CATALOG_READ_RETRY_MAX_DELAY_MS)));
246
+ }
247
+ }
248
+ throw new Error('Runtime Host Session catalog could not be read consistently');
249
+ }
250
+ async getSessionResumeAvailability(session) {
182
251
  return inspectRuntimeHostSessionResumeAvailability(session, this.#executionLocation);
183
252
  }
253
+ async getSessionResumeCandidateAvailability(session) {
254
+ const availability = await inspectRuntimeHostSessionResumeAvailability(session, this.#executionLocation);
255
+ if (!availability.available)
256
+ return availability;
257
+ const plan = await this.#request('turn.resume.query', { sessionId: session.id });
258
+ return plan.disposition === 'ready'
259
+ ? { available: true }
260
+ : { available: false, reason: plan.reason };
261
+ }
184
262
  async preparePrompt(prompt, options = {}) {
185
263
  const sessionId = await this.#ensureSession();
186
264
  const sessionGeneration = this.#sessionGeneration;
@@ -1496,9 +1574,9 @@ function inspectRuntimeHostSessionResumeAvailability(summary, location) {
1496
1574
  if (!summary.cwd) {
1497
1575
  return Promise.resolve({ available: false, reason: 'Missing working directory' });
1498
1576
  }
1499
- return location.kind === 'host'
1500
- ? Promise.resolve({ available: true })
1501
- : inspectSessionResumeAvailability(summary);
1577
+ if (location.kind !== 'host')
1578
+ return inspectSessionResumeAvailability(summary);
1579
+ return Promise.resolve({ available: true });
1502
1580
  }
1503
1581
  async function assertSessionResumeAvailable(summary, location) {
1504
1582
  const availability = await inspectRuntimeHostSessionResumeAvailability(summary, location);
@@ -0,0 +1,22 @@
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 { SESSION_CATALOG_PAGE_MAX_ITEMS } from '@maka/runtime-host/protocol';
20
+ // Sparse cwd or visibility matches must not turn a bounded lookup into a full Host scan.
21
+ export const SESSION_CATALOG_MAX_SCAN_PAGES = 8;
22
+ export const SESSION_CATALOG_MAX_SCAN_SESSIONS = SESSION_CATALOG_MAX_SCAN_PAGES * SESSION_CATALOG_PAGE_MAX_ITEMS;
@@ -31,6 +31,16 @@ export function skillInvocationBlockedMessage(skillInvocation) {
31
31
  ? `Could not resolve the Skill this Turn asked for: ${reasons.join(', ')}`
32
32
  : 'Explicit Skill invocation could not be resolved';
33
33
  }
34
+ export class MakaSessionCatalogIncompleteError extends Error {
35
+ scannedPages;
36
+ sessions;
37
+ constructor(scannedPages, sessions = []) {
38
+ super(`Session catalog results may be incomplete after scanning ${scannedPages} pages.`);
39
+ this.scannedPages = scannedPages;
40
+ this.sessions = sessions;
41
+ this.name = 'MakaSessionCatalogIncompleteError';
42
+ }
43
+ }
34
44
  export async function inspectSessionResumeAvailability(session) {
35
45
  if (!session.cwd)
36
46
  return { available: false, reason: 'Missing working directory' };
@@ -638,6 +638,11 @@ export const TUI_COPY_RESOURCES = {
638
638
  sessionScopeAll: 'All',
639
639
  sessionSearchHint: 'Search name / id / path / model / connection · Tab scope · ↑↓ select · Enter open · Esc close',
640
640
  noMatchingSessions: 'No matching sessions',
641
+ resumeCandidateCheckFailed: 'Could not check whether any sessions can be resumed.',
642
+ resumeAvailabilityNotice: 'An interrupted run is available — use /resume to continue from a safe boundary.',
643
+ resumeStartingNotice: 'Resuming from the latest safe boundary…',
644
+ resumeUnavailableNotice: 'Safe-boundary resume is unavailable on this runtime.',
645
+ resumeCatalogIncompleteNotice: 'Session discovery stopped before the full catalog was checked. Showing the sessions found so far.',
641
646
  selectPickerHint: '↑↓ select · Enter confirm · Esc close',
642
647
  providerConfigured: 'configured',
643
648
  addAccount: 'add account',
@@ -758,6 +763,11 @@ export const TUI_COPY_RESOURCES = {
758
763
  sessionScopeAll: '全部',
759
764
  sessionSearchHint: '搜索名称 / ID / 路径 / 模型 / 连接 · Tab 切换范围 · ↑↓ 选择 · Enter 打开 · Esc 关闭',
760
765
  noMatchingSessions: '没有匹配的会话',
766
+ resumeCandidateCheckFailed: '无法检查是否有可恢复的会话。',
767
+ resumeAvailabilityNotice: '有可恢复的中断会话,可使用 /resume 从安全边界继续。',
768
+ resumeStartingNotice: '正在从最新的安全边界恢复……',
769
+ resumeUnavailableNotice: '当前运行环境不支持安全边界恢复。',
770
+ resumeCatalogIncompleteNotice: '会话目录尚未检查完整。当前显示已找到的会话。',
761
771
  selectPickerHint: '↑↓ 选择 · Enter 确认 · Esc 关闭',
762
772
  providerConfigured: '已设置',
763
773
  addAccount: '添加账号',
@@ -877,6 +887,11 @@ export const TUI_COPY_RESOURCES = {
877
887
  sessionScopeAll: '全部',
878
888
  sessionSearchHint: '搜尋名稱 / ID / 路徑 / 模型 / 連線 · Tab 切換範圍 · ↑↓ 選取 · Enter 開啟 · Esc 關閉',
879
889
  noMatchingSessions: '沒有符合的會話',
890
+ resumeCandidateCheckFailed: '無法檢查是否有可恢復的會話。',
891
+ resumeAvailabilityNotice: '有可恢復的中斷會話,可使用 /resume 從安全邊界繼續。',
892
+ resumeStartingNotice: '正在從最新的安全邊界恢復……',
893
+ resumeUnavailableNotice: '目前執行環境不支援安全邊界恢復。',
894
+ resumeCatalogIncompleteNotice: '會話目錄尚未檢查完整。目前顯示已找到的會話。',
880
895
  selectPickerHint: '↑↓ 選擇 · Enter 確認 · Esc 關閉',
881
896
  providerConfigured: '已設定',
882
897
  addAccount: '新增帳號',