maka-agent 0.2.0-dev.23.20260906 → 0.2.0-dev.25.20260908

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 (215) hide show
  1. package/README.md +38 -19
  2. package/README.zh-CN.md +34 -19
  3. package/dist/acp/maka-acp-agent.js +2 -1
  4. package/dist/acp/session-configuration.js +143 -0
  5. package/dist/acp/session-registry.js +90 -4
  6. package/dist/cli-core.js +2 -0
  7. package/dist/pi-transcript-tools.js +6 -0
  8. package/dist/pi-transcript.js +5 -12
  9. package/dist/pi-tui-layout.js +56 -10
  10. package/dist/pi-tui-pickers.js +102 -1
  11. package/dist/pi-tui-runner.js +230 -46
  12. package/dist/pi-tui-todo.js +221 -0
  13. package/dist/pi-tui-transcript-viewer.js +134 -7
  14. package/dist/runtime-host-cli-context.js +224 -78
  15. package/dist/runtime-host-cli-installation.js +8 -2
  16. package/dist/runtime-host-cli.js +31 -1
  17. package/dist/runtime-host-handoff-surface.js +106 -0
  18. package/dist/runtime-host-installed-update-bootstrap.js +13 -0
  19. package/dist/runtime-host-installed-update-coordinator.js +33 -0
  20. package/dist/runtime-host-lifecycle-transaction.js +9 -1
  21. package/dist/runtime-host-local-handoff.js +48 -5
  22. package/dist/runtime-host-local-managed-activation.js +84 -0
  23. package/dist/runtime-host-local-source-retirement.js +18 -2
  24. package/dist/runtime-host-managed-lifecycle-manager.js +1 -0
  25. package/dist/runtime-host-operator-retirement.js +50 -0
  26. package/dist/runtime-host-service-manager.js +3 -3
  27. package/dist/runtime-host-session-channel.js +13 -0
  28. package/dist/runtime-host-session-driver.js +46 -26
  29. package/dist/runtime-host-session-update.js +62 -0
  30. package/dist/runtime-host-tui-command.js +38 -73
  31. package/dist/runtime-host-tui-context.js +1 -0
  32. package/dist/runtime-host-tui-owner.js +226 -0
  33. package/dist/tui-copy-catalog.js +219 -6
  34. package/dist/tui-host-owner-copy.js +24 -0
  35. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  36. package/node_modules/@maka/core/dist/agent-run.js +1 -0
  37. package/node_modules/@maka/core/dist/attachments.js +3 -0
  38. package/node_modules/@maka/core/dist/canonical-json.js +27 -0
  39. package/node_modules/@maka/core/dist/canonical-runtime-event.js +3 -1
  40. package/node_modules/@maka/core/dist/capabilities.js +35 -0
  41. package/node_modules/@maka/core/dist/computer-use.js +0 -1
  42. package/node_modules/@maka/core/dist/display-redaction.js +18 -0
  43. package/node_modules/@maka/core/dist/llm-connections.js +13 -0
  44. package/node_modules/@maka/core/dist/model-call-attempt.js +2 -0
  45. package/node_modules/@maka/core/dist/model-failure.js +40 -0
  46. package/node_modules/@maka/core/dist/model-metadata.generated.js +4 -4
  47. package/node_modules/@maka/core/dist/model-metadata.js +7 -3
  48. package/node_modules/@maka/core/dist/model-web-search.js +5 -4
  49. package/node_modules/@maka/core/dist/provider-registry.js +45 -4
  50. package/node_modules/@maka/core/dist/record-schema.js +9 -2
  51. package/node_modules/@maka/core/dist/redaction.js +35 -0
  52. package/node_modules/@maka/core/dist/run-composition.js +77 -2
  53. package/node_modules/@maka/core/dist/runtime-boundary.js +31 -11
  54. package/node_modules/@maka/core/dist/runtime-event.js +37 -6
  55. package/node_modules/@maka/core/dist/runtime-handoff.js +98 -0
  56. package/node_modules/@maka/core/dist/runtime-invocation.js +1 -1
  57. package/node_modules/@maka/core/dist/runtime-logical-execution.js +112 -0
  58. package/node_modules/@maka/core/dist/session.js +84 -56
  59. package/node_modules/@maka/core/dist/slash-command-catalog.js +3 -0
  60. package/node_modules/@maka/core/dist/tool-result-archive-evidence.js +20 -0
  61. package/node_modules/@maka/core/dist/tool-result-record-schema.js +2 -1
  62. package/node_modules/@maka/core/dist/usage-record-schema.js +26 -35
  63. package/node_modules/@maka/core/package.json +5 -0
  64. package/node_modules/@maka/eval/dist/experiment-directory.js +4 -13
  65. package/node_modules/@maka/mcp/dist/index.js +131 -1
  66. package/node_modules/@maka/runtime/dist/agent-run-inspect.js +0 -13
  67. package/node_modules/@maka/runtime/dist/agent-run.js +351 -105
  68. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +29 -13
  69. package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +109 -34
  70. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +184 -183
  71. package/node_modules/@maka/runtime/dist/archive-read-tool.js +2 -2
  72. package/node_modules/@maka/runtime/dist/context-budget-helpers.js +0 -16
  73. package/node_modules/@maka/runtime/dist/context-budget.js +14 -9
  74. package/node_modules/@maka/runtime/dist/conversation-copy.js +158 -15
  75. package/node_modules/@maka/runtime/dist/github-copilot-oauth-enrollment.js +3 -3
  76. package/node_modules/@maka/runtime/dist/goal-continuation.js +36 -4
  77. package/node_modules/@maka/runtime/dist/history-compact-checkpoint.js +18 -9
  78. package/node_modules/@maka/runtime/dist/history-compact-summarizer.js +30 -22
  79. package/node_modules/@maka/runtime/dist/history-compaction.js +29 -4
  80. package/node_modules/@maka/runtime/dist/ledger-tool-result-archive-reader.js +174 -0
  81. package/node_modules/@maka/runtime/dist/mcp-tools.js +7 -2
  82. package/node_modules/@maka/runtime/dist/message-authority.js +9 -0
  83. package/node_modules/@maka/runtime/dist/model-adapter.js +19 -98
  84. package/node_modules/@maka/runtime/dist/model-factory.js +19 -81
  85. package/node_modules/@maka/runtime/dist/model-fetcher.js +49 -2
  86. package/node_modules/@maka/runtime/dist/model-history.js +0 -112
  87. package/node_modules/@maka/runtime/dist/model-runtime.js +85 -111
  88. package/node_modules/@maka/runtime/dist/native-web-search-tool.js +2 -1
  89. package/node_modules/@maka/runtime/dist/openai-codex-history-compactor.js +2 -2
  90. package/node_modules/@maka/runtime/dist/opencode-session-header.js +4 -2
  91. package/node_modules/@maka/runtime/dist/plugin-runtime.js +20 -10
  92. package/node_modules/@maka/runtime/dist/plugin-tool-service.js +205 -0
  93. package/node_modules/@maka/runtime/dist/provider-error-classification.js +84 -123
  94. package/node_modules/@maka/runtime/dist/provider-request-telemetry.js +76 -109
  95. package/node_modules/@maka/runtime/dist/request-shape.js +23 -0
  96. package/node_modules/@maka/runtime/dist/run-handoff-gate.js +97 -0
  97. package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +83 -55
  98. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +121 -159
  99. package/node_modules/@maka/runtime/dist/runtime-kernel.js +157 -112
  100. package/node_modules/@maka/runtime/dist/runtime-ledger-repair.js +160 -79
  101. package/node_modules/@maka/runtime/dist/runtime-read-model.js +30 -89
  102. package/node_modules/@maka/runtime/dist/runtime-resume.js +80 -4
  103. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +5 -0
  104. package/node_modules/@maka/runtime/dist/session-manager.js +171 -288
  105. package/node_modules/@maka/runtime/dist/session-projection-helpers.js +0 -27
  106. package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +21 -0
  107. package/node_modules/@maka/runtime/dist/stream-graph-supervisor-tools.js +0 -5
  108. package/node_modules/@maka/runtime/dist/subscription-model-fetch.js +3 -4
  109. package/node_modules/@maka/runtime/dist/test-connection.js +16 -17
  110. package/node_modules/@maka/runtime/dist/tool-activation-identity.js +41 -0
  111. package/node_modules/@maka/runtime/dist/tool-availability.js +11 -1
  112. package/node_modules/@maka/runtime/dist/tool-result-archive-encoding.js +38 -0
  113. package/node_modules/@maka/runtime/dist/tool-result-archive-resource.js +63 -1
  114. package/node_modules/@maka/runtime/dist/tool-result-archive-transition.js +42 -12
  115. package/node_modules/@maka/runtime/dist/tool-result-archive.js +27 -4
  116. package/node_modules/@maka/runtime/dist/tool-runtime.js +3 -61
  117. package/node_modules/@maka/runtime/package.json +2 -0
  118. package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +35 -0
  119. package/node_modules/@maka/runtime-host/dist/client/connect-or-spawn.js +9 -1
  120. package/node_modules/@maka/runtime-host/dist/client/connection.js +3 -0
  121. package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +179 -0
  122. package/node_modules/@maka/runtime-host/dist/client/host-handoff.js +275 -0
  123. package/node_modules/@maka/runtime-host/dist/client/host-retirement.js +26 -5
  124. package/node_modules/@maka/runtime-host/dist/client/index.js +2 -0
  125. package/node_modules/@maka/runtime-host/dist/client/launcher.js +3 -0
  126. package/node_modules/@maka/runtime-host/dist/client/remote-compatibility-error.js +2 -0
  127. package/node_modules/@maka/runtime-host/dist/client/session-subscription.js +46 -5
  128. package/node_modules/@maka/runtime-host/dist/control/endpoint.js +1 -3
  129. package/node_modules/@maka/runtime-host/dist/node-error.js +22 -0
  130. package/node_modules/@maka/runtime-host/dist/operator/index.js +2 -1
  131. package/node_modules/@maka/runtime-host/dist/operator/managed-deployment.js +1 -3
  132. package/node_modules/@maka/runtime-host/dist/operator/retirement-cancellation.js +20 -0
  133. package/node_modules/@maka/runtime-host/dist/peer-mesh/limits.js +0 -1
  134. package/node_modules/@maka/runtime-host/dist/peer-mesh/model.js +1 -1
  135. package/node_modules/@maka/runtime-host/dist/peer-reachability/publisher.js +1 -3
  136. package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +21 -12
  137. package/node_modules/@maka/runtime-host/dist/protocol/index.js +29 -1
  138. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
  139. package/node_modules/@maka/runtime-host/dist/protocol/plan.js +1 -0
  140. package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +27 -5
  141. package/node_modules/@maka/runtime-host/dist/protocol/project-catalog.js +2 -2
  142. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +40 -20
  143. package/node_modules/@maka/runtime-host/dist/protocol/session-transcript.js +0 -5
  144. package/node_modules/@maka/runtime-host/dist/protocol/session-turns.js +29 -58
  145. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +11 -1
  146. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +31 -2
  147. package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +1 -0
  148. package/node_modules/@maka/runtime-host/dist/server/agent-graph-coordinator.js +0 -3
  149. package/node_modules/@maka/runtime-host/dist/server/canonical-turn-snapshot.js +19 -4
  150. package/node_modules/@maka/runtime-host/dist/server/connection-session.js +7 -6
  151. package/node_modules/@maka/runtime-host/dist/server/execution-artifacts.js +32 -32
  152. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +144 -33
  153. package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +5 -5
  154. package/node_modules/@maka/runtime-host/dist/server/execution-model-composition.js +45 -16
  155. package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +40 -1
  156. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +96 -17
  157. package/node_modules/@maka/runtime-host/dist/server/host-residency-registry.js +13 -7
  158. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-admission-registry.js +3 -1
  159. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +124 -62
  160. package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +47 -22
  161. package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +76 -6
  162. package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +6 -0
  163. package/node_modules/@maka/runtime-host/dist/server/plugin-platform.js +6 -0
  164. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +325 -73
  165. package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +37 -2
  166. package/node_modules/@maka/runtime-host/dist/server/serial-outbound-writer.js +45 -3
  167. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +50 -7
  168. package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +87 -25
  169. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +5 -2
  170. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +12 -55
  171. package/node_modules/@maka/runtime-host/dist/server/session-sidecar-purge.js +1 -16
  172. package/node_modules/@maka/runtime-host/dist/server/session-transcript-pager.js +0 -1
  173. package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +460 -70
  174. package/node_modules/@maka/runtime-host/dist/server/shared-session-transcript.js +1 -1
  175. package/node_modules/@maka/runtime-host/dist/server/storage-maintenance.js +109 -0
  176. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +20 -2
  177. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +13 -4
  178. package/node_modules/@maka/runtime-host/dist/server/workhub-message-attachments.js +46 -0
  179. package/node_modules/@maka/storage/dist/artifact-store.js +21 -10
  180. package/node_modules/@maka/storage/dist/artifact-stores.js +1 -1
  181. package/node_modules/@maka/storage/dist/claude-code-session-adapter.js +0 -5
  182. package/node_modules/@maka/storage/dist/codex-session-adapter.js +284 -198
  183. package/node_modules/@maka/storage/dist/context-offload-snapshot.js +325 -0
  184. package/node_modules/@maka/storage/dist/execution-stores.js +9 -7
  185. package/node_modules/@maka/storage/dist/file-session-repository.js +1012 -0
  186. package/node_modules/@maka/storage/dist/git-exec.js +61 -0
  187. package/node_modules/@maka/storage/dist/git-worktree-child-executor.js +8 -35
  188. package/node_modules/@maka/storage/dist/goal-authority.js +2 -2
  189. package/node_modules/@maka/storage/dist/interaction-store.js +2 -2
  190. package/node_modules/@maka/storage/dist/opencode-session-adapter.js +0 -4
  191. package/node_modules/@maka/storage/dist/operational-state-backup.js +9 -5
  192. package/node_modules/@maka/storage/dist/project-catalog.js +2 -23
  193. package/node_modules/@maka/storage/dist/runtime-event-invariants.js +2 -2
  194. package/node_modules/@maka/storage/dist/runtime-event-persistence.js +1 -0
  195. package/node_modules/@maka/storage/dist/runtime-policy/connection-catalog-document.js +8 -3
  196. package/node_modules/@maka/storage/dist/runtime-transcript-query.js +308 -0
  197. package/node_modules/@maka/storage/dist/scheduled-task-store.js +249 -283
  198. package/node_modules/@maka/storage/dist/session-bundle-manifest.js +5 -17
  199. package/node_modules/@maka/storage/dist/session-bundle-policy.js +26 -5
  200. package/node_modules/@maka/storage/dist/session-copy-cleanup.js +2 -1
  201. package/node_modules/@maka/storage/dist/session-message-projection.js +15 -0
  202. package/node_modules/@maka/storage/dist/session-repository.js +767 -0
  203. package/node_modules/@maka/storage/dist/session-store.js +25 -99
  204. package/node_modules/@maka/storage/dist/sqlite-artifact-metadata.js +10 -3
  205. package/node_modules/@maka/storage/dist/sqlite-context-offload-store.js +24 -8
  206. package/node_modules/@maka/storage/dist/sqlite-core-execution-schema.js +7 -1
  207. package/node_modules/@maka/storage/dist/sqlite-runtime-schema.js +44 -1
  208. package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +130 -27
  209. package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +109 -892
  210. package/node_modules/@maka/storage/dist/storage-id.js +28 -0
  211. package/node_modules/@maka/storage/dist/tool-result-archive-evidence.js +138 -0
  212. package/node_modules/@maka/storage/dist/workspace-identity.js +2 -15
  213. package/node_modules/@maka/storage/dist/workspace-version-authority-internal.js +1 -8
  214. package/node_modules/@maka/storage/package.json +2 -0
  215. package/package.json +1 -1
@@ -16,8 +16,9 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { Key, matchesKey, truncateToWidth, visibleWidth, } from '@earendil-works/pi-tui';
20
- import { ansi } from './tui-ansi.js';
19
+ import { Key, Input, isKeyRelease, isKeyRepeat, matchesKey, truncateToWidth, visibleWidth, } from '@earendil-works/pi-tui';
20
+ import { ansi, stripAnsi } from './tui-ansi.js';
21
+ import { TUI_COPY_RESOURCES } from './tui-copy-catalog.js';
21
22
  const VIEWER_CHROME_ROWS = 2;
22
23
  /**
23
24
  * Full-screen, read-only navigation over the CLI transcript projection.
@@ -28,19 +29,100 @@ const VIEWER_CHROME_ROWS = 2;
28
29
  */
29
30
  export class TranscriptViewerOverlay {
30
31
  input;
32
+ focused = false;
31
33
  top = 0;
32
34
  documentRows = 0;
33
35
  bodyRows = 0;
34
36
  followsEnd = true;
37
+ document = { lines: [], anchors: [] };
38
+ expanded = true;
39
+ anchor;
40
+ search = new Input();
41
+ searching = false;
42
+ query = '';
43
+ matches = [];
44
+ searchOrigin;
45
+ matchedLine;
35
46
  constructor(input) {
36
47
  this.input = input;
37
48
  }
38
49
  invalidate() { }
39
50
  handleInput(data) {
51
+ if (isKeyRelease(data))
52
+ return;
53
+ if (matchesKey(data, Key.ctrl('o'))) {
54
+ if (!isKeyRepeat(data))
55
+ this.input.onClose();
56
+ return;
57
+ }
58
+ if (this.searching) {
59
+ if (matchesKey(data, Key.escape)) {
60
+ this.searching = false;
61
+ this.query = '';
62
+ this.matches = [];
63
+ if (this.searchOrigin)
64
+ Object.assign(this, this.searchOrigin);
65
+ this.input.onChange();
66
+ return;
67
+ }
68
+ if (matchesKey(data, Key.enter)) {
69
+ this.searching = false;
70
+ this.input.onChange();
71
+ return;
72
+ }
73
+ if (matchesKey(data, Key.up) || matchesKey(data, Key.down)) {
74
+ this.nextMatch(matchesKey(data, Key.up) ? -1 : 1);
75
+ return;
76
+ }
77
+ this.search.handleInput(data);
78
+ const query = this.search.getValue().trim().toLocaleLowerCase();
79
+ if (query !== this.query) {
80
+ this.query = query;
81
+ this.findMatches();
82
+ const target = this.matches.find((line) => line >= (this.searchOrigin?.top ?? 0)) ?? this.matches[0];
83
+ if (target !== undefined)
84
+ this.goToMatch(target);
85
+ }
86
+ this.input.onChange();
87
+ return;
88
+ }
89
+ if (matchesKey(data, Key.escape) && this.query) {
90
+ this.query = '';
91
+ this.matches = [];
92
+ if (this.searchOrigin)
93
+ Object.assign(this, this.searchOrigin);
94
+ this.input.onChange();
95
+ return;
96
+ }
40
97
  if (matchesKey(data, Key.escape) || matchesKey(data, 'q')) {
41
98
  this.input.onClose();
42
99
  return;
43
100
  }
101
+ if (matchesKey(data, '/')) {
102
+ this.searchOrigin = {
103
+ top: this.top,
104
+ followsEnd: this.followsEnd,
105
+ anchor: this.anchor ? { ...this.anchor } : undefined,
106
+ };
107
+ this.search.setValue(this.query);
108
+ this.searching = true;
109
+ this.input.onChange();
110
+ return;
111
+ }
112
+ if (this.query && (matchesKey(data, 'n') || matchesKey(data, Key.shift('n')))) {
113
+ this.nextMatch(matchesKey(data, Key.shift('n')) ? -1 : 1);
114
+ return;
115
+ }
116
+ if (matchesKey(data, Key.ctrl('e'))) {
117
+ if (!isKeyRepeat(data)) {
118
+ this.expanded = !this.expanded;
119
+ // A detail that disappears on collapse returns to its own heading.
120
+ if (!this.expanded && this.anchor)
121
+ this.anchor.offset = 0;
122
+ this.input.onChange();
123
+ }
124
+ return;
125
+ }
44
126
  if (matchesKey(data, Key.up)) {
45
127
  this.scrollBy(-1);
46
128
  return;
@@ -60,12 +142,14 @@ export class TranscriptViewerOverlay {
60
142
  if (matchesKey(data, Key.home)) {
61
143
  this.followsEnd = false;
62
144
  this.top = 0;
145
+ this.captureAnchor();
63
146
  this.input.onChange();
64
147
  return;
65
148
  }
66
149
  if (matchesKey(data, Key.end)) {
67
150
  this.followsEnd = true;
68
151
  this.top = this.maxTop();
152
+ this.captureAnchor();
69
153
  this.input.onChange();
70
154
  }
71
155
  }
@@ -76,31 +160,74 @@ export class TranscriptViewerOverlay {
76
160
  // navigation chrome only. The footer appears once header + body + footer fit.
77
161
  const showFooter = viewportRows > 2;
78
162
  this.bodyRows = Math.max(0, viewportRows - (showFooter ? VIEWER_CHROME_ROWS : 1));
79
- const document = [...this.input.renderTranscript(safeWidth)];
163
+ this.document = this.input.renderTranscript(safeWidth, this.expanded);
164
+ const document = this.document.lines;
80
165
  this.documentRows = document.length;
166
+ if (!this.followsEnd && this.anchor) {
167
+ const index = this.document.anchors.findIndex((entry) => entry.id === this.anchor.id);
168
+ if (index >= 0) {
169
+ const entry = this.document.anchors[index];
170
+ const end = this.document.anchors[index + 1]?.line ?? document.length;
171
+ this.top = entry.line + Math.min(this.anchor.offset, Math.max(0, end - entry.line - 1));
172
+ }
173
+ }
81
174
  const maxTop = this.maxTop();
82
175
  this.top = this.followsEnd ? maxTop : clamp(this.top, 0, maxTop);
83
- this.followsEnd = this.top === maxTop;
176
+ this.captureAnchor();
177
+ this.findMatches();
84
178
  const visible = document.slice(this.top, this.top + this.bodyRows);
85
179
  const start = visible.length === 0 ? 0 : this.top + 1;
86
180
  const end = visible.length === 0 ? 0 : this.top + visible.length;
87
- const header = padLine(`${ansi.bold('TRANSCRIPT')} ${ansi.dim(`${start}-${end} of ${document.length}`)}`, safeWidth);
181
+ const copy = TUI_COPY_RESOURCES['transcript-reader'][this.input.locale ?? 'en'];
182
+ const header = padLine(`${ansi.bold(copy.title)} ${ansi.dim(`${start}-${end}/${document.length} · ${copy.scope}`)}`, safeWidth);
183
+ const matchingLines = new Set(this.matches);
88
184
  const body = [
89
- ...visible.map((line) => padLine(line, safeWidth)),
185
+ ...visible.map((line, index) => padLine(matchingLines.has(this.top + index) ? ansi.reverse(stripAnsi(line)) : line, safeWidth)),
90
186
  ...Array.from({ length: Math.max(0, this.bodyRows - visible.length) }, () => ' '.repeat(safeWidth)),
91
187
  ];
92
188
  if (!showFooter)
93
189
  return [header, ...body];
94
- const footer = padLine(ansi.dim('↑/↓ scroll · PgUp/PgDn page · Home/End jump · q/Esc close'), safeWidth);
190
+ this.search.focused = this.focused && this.searching;
191
+ const footer = padLine(this.searching
192
+ ? `/ ${this.search.render(Math.max(1, safeWidth - 3))[0] ?? ''}`
193
+ : ansi.dim(this.query
194
+ ? `${this.matches.length} ${copy.matches} · n/N · Esc ${copy.back}`
195
+ : copy.hint), safeWidth);
95
196
  return [header, ...body, footer];
96
197
  }
97
198
  scrollBy(delta) {
199
+ this.matchedLine = undefined;
98
200
  const maxTop = this.maxTop();
99
201
  this.top = clamp(this.top + delta, 0, maxTop);
100
202
  // Follow the tail whenever the clamped position is the end, including the
101
203
  // no-op case where a short transcript cannot move at all: a stray Up key
102
204
  // must not pin the viewer at the head once the transcript grows.
103
205
  this.followsEnd = this.top === maxTop;
206
+ this.captureAnchor();
207
+ this.input.onChange();
208
+ }
209
+ captureAnchor() {
210
+ const entry = [...this.document.anchors].reverse().find((entry) => entry.line <= this.top);
211
+ this.anchor = entry ? { id: entry.id, offset: this.top - entry.line } : undefined;
212
+ }
213
+ findMatches() {
214
+ this.matches = this.query
215
+ ? this.document.lines.flatMap((line, index) => stripAnsi(line).toLocaleLowerCase().includes(this.query) ? [index] : [])
216
+ : [];
217
+ }
218
+ goToMatch(line) {
219
+ this.matchedLine = line;
220
+ this.top = clamp(line, 0, this.maxTop());
221
+ this.followsEnd = false;
222
+ this.captureAnchor();
223
+ }
224
+ nextMatch(direction) {
225
+ const current = this.matchedLine ?? this.top;
226
+ const line = direction > 0
227
+ ? (this.matches.find((line) => line > current) ?? this.matches[0])
228
+ : ([...this.matches].reverse().find((line) => line < current) ?? this.matches.at(-1));
229
+ if (line !== undefined)
230
+ this.goToMatch(line);
104
231
  this.input.onChange();
105
232
  }
106
233
  maxTop() {
@@ -16,13 +16,16 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
+ import { activateLocalManagedRuntimeHost } from './runtime-host-local-managed-activation.js';
19
20
  import { randomUUID } from 'node:crypto';
20
21
  import { join } from 'node:path';
21
22
  import { NO_REAL_CONNECTION_CODE } from '@maka/core/connection-error-copy';
22
- import { connectOrSpawnRuntimeHost, connectRuntimeHostProfile, createClientRuntimeHostProfileCatalog, createRuntimeHostPeerClientFromEnvironment, createRuntimeHostReconnectingConnection, loadOrCreateRuntimeHostClientInstanceId, LOCAL_RUNTIME_HOST_PROFILE, readRuntimeHostConnectionCatalog, RuntimeHostPermanentReconnectError, runtimeHostStartupError, } from '@maka/runtime-host/client';
23
- import { INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID, RUNTIME_HOST_COMPATIBILITY_EPOCH, RUNTIME_HOST_PROTOCOL_VERSION, } from '@maka/runtime-host/protocol';
24
- import { readLocalHostDeploymentRecord } from '@maka/runtime-host/operator';
23
+ import { connectOrSpawnRuntimeHost, forceTerminateObservedRegisteredRuntimeHost, connectRuntimeHost, connectRuntimeHostProfile, createClientRuntimeHostProfileCatalog, createRuntimeHostPeerClientFromEnvironment, createRuntimeHostReconnectingConnection, loadOrCreateRuntimeHostClientInstanceId, LOCAL_RUNTIME_HOST_PROFILE, readRuntimeHostConnectionCatalog, RuntimeHostPermanentReconnectError, RuntimeHostRemoteCompatibilityError, runHostHandoff, runtimeHostStartupError, } from '@maka/runtime-host/client';
24
+ import { INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID, RUNTIME_HOST_PROTOCOL_VERSION, RUNTIME_HOST_COMPATIBILITY_EPOCH, } from '@maka/runtime-host/protocol';
25
+ import { readLocalHostDeploymentRecord, resolveRuntimeHostManagedDeploymentAuthority, } from '@maka/runtime-host/operator';
25
26
  import { resolveMakaClientDataRoot } from '@maka/storage/workspace-root';
27
+ import { isTemporaryNpxInstallation, resolveRuntimeHostNpmGlobalInstallation, } from './runtime-host-cli-installation.js';
28
+ import { restartRuntimeHostNpmGlobalDeployment, runtimeHostNpmGlobalSourceRetirementAvailable, } from './runtime-host-local-handoff.js';
26
29
  /**
27
30
  * The mode a new Session starts in belongs to the Host: `session.create`
28
31
  * falls back to `chatDefaults.permissionMode` in the Runtime Policy whenever a
@@ -42,17 +45,6 @@ import { resolveMakaClientDataRoot } from '@maka/storage/workspace-root';
42
45
  export async function readHostChatDefaultPermissionMode(connection) {
43
46
  return (await connection.request('runtime.policy.query', {})).policy.chatDefaults.permissionMode;
44
47
  }
45
- export class RuntimeHostCliConflictError extends RuntimeHostPermanentReconnectError {
46
- handshake;
47
- registration;
48
- code = 'RUNTIME_HOST_RESTART_REQUIRED';
49
- constructor(handshake, registration) {
50
- super(formatRuntimeHostCliConflict(handshake, registration));
51
- this.handshake = handshake;
52
- this.registration = registration;
53
- this.name = 'RuntimeHostCliConflictError';
54
- }
55
- }
56
48
  export async function connectRuntimeHostCli(input, overrides = {}) {
57
49
  const context = await connectRuntimeHostCliConnection(input, overrides);
58
50
  try {
@@ -67,12 +59,20 @@ export async function connectRuntimeHostCli(input, overrides = {}) {
67
59
  export async function connectRuntimeHostCliConnection(input, overrides = {}) {
68
60
  const deps = {
69
61
  connectOrSpawn: connectOrSpawnRuntimeHost,
62
+ activateLocalManagedHost: activateLocalManagedRuntimeHost,
63
+ connectActivatedHost: connectRuntimeHost,
70
64
  connectProfile: connectRuntimeHostProfile,
71
65
  readConnectionCatalog: readRuntimeHostConnectionCatalog,
72
66
  loadClientInstanceId: loadOrCreateRuntimeHostClientInstanceId,
73
67
  executionCandidateEntrypoint: new URL(import.meta.resolve('@maka/runtime-host/execution-candidate-main')),
74
68
  readDeploymentRecord: readLocalHostDeploymentRecord,
69
+ resolveManagedAuthority: resolveRuntimeHostManagedDeploymentAuthority,
75
70
  createPeerClient: createRuntimeHostPeerClientFromEnvironment,
71
+ resolveInstallation: resolveRuntimeHostNpmGlobalInstallation,
72
+ isTemporaryNpxInstallation,
73
+ terminateObservedHost: forceTerminateObservedRegisteredRuntimeHost,
74
+ restartDeployment: restartRuntimeHostNpmGlobalDeployment,
75
+ sourceRetirementAvailable: runtimeHostNpmGlobalSourceRetirementAvailable,
76
76
  ...overrides,
77
77
  };
78
78
  const resolvedProfile = await resolveHostProfile(input, deps);
@@ -83,45 +83,227 @@ export async function connectRuntimeHostCliConnection(input, overrides = {}) {
83
83
  const peerClient = profile.kind === 'remote' && profile.transport.kind === 'libp2p-direct'
84
84
  ? deps.createPeerClient()
85
85
  : undefined;
86
+ // A temporary package is evidence for invocation lifetime, never deployment
87
+ // authority. This only guards candidates we create; using an existing Host
88
+ // leaves that Host's ownership and lifetime unchanged.
89
+ const invocationOwned = profile.kind === 'local' && (await deps.isTemporaryNpxInstallation());
86
90
  const connectInput = {
87
91
  rootPath: input.rootPath,
88
92
  protocol: { min: RUNTIME_HOST_PROTOCOL_VERSION, max: RUNTIME_HOST_PROTOCOL_VERSION },
89
93
  clientInstanceId,
90
94
  compositionId: INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
91
95
  candidateEntrypoint: deps.executionCandidateEntrypoint,
96
+ ...(invocationOwned ? { closeOnLauncherExit: true } : {}),
92
97
  };
93
98
  const connect = async (signal, sshInteraction = 'batch') => {
94
- if (profile.kind !== 'local') {
95
- return deps.connectProfile({
96
- profile,
97
- ...(resolvedProfile.credential ? { credential: resolvedProfile.credential } : {}),
98
- clientInstanceId,
99
- sshInteraction,
100
- ...(peerClient ? { peerClient } : {}),
99
+ const observe = async () => {
100
+ if (profile.kind !== 'local') {
101
+ try {
102
+ const connection = await deps.connectProfile({
103
+ profile,
104
+ ...(resolvedProfile.credential ? { credential: resolvedProfile.credential } : {}),
105
+ clientInstanceId,
106
+ sshInteraction,
107
+ ...(peerClient ? { peerClient } : {}),
108
+ ...(signal ? { signal } : {}),
109
+ });
110
+ return { kind: 'ready', value: connection };
111
+ }
112
+ catch (error) {
113
+ if (!(error instanceof RuntimeHostRemoteCompatibilityError))
114
+ throw error;
115
+ return {
116
+ kind: 'blocked',
117
+ blocker: {
118
+ identity: JSON.stringify([profile, error.hostEpoch, error.details]),
119
+ target: {
120
+ name: profile.name,
121
+ location: 'remote',
122
+ rootId: profile.rootId,
123
+ hostEpoch: error.hostEpoch,
124
+ },
125
+ reason: 'upgrade',
126
+ mayExitNaturally: false,
127
+ diagnostic: error.message,
128
+ },
129
+ };
130
+ }
131
+ }
132
+ let connected = await deps.connectOrSpawn({
133
+ ...connectInput,
101
134
  ...(signal ? { signal } : {}),
102
135
  });
103
- }
104
- const connected = await deps.connectOrSpawn({
105
- ...connectInput,
106
- ...(signal ? { signal } : {}),
107
- });
108
- if (connected.kind === 'incompatible') {
109
- throw new RuntimeHostCliConflictError(connected.handshake, connected.registration);
110
- }
111
- if (connected.kind === 'upgrade_required') {
112
- throw new RuntimeHostPermanentReconnectError('RUNTIME_HOST_RESTART_REQUIRED: An older Runtime Host build is still running. Restart it, or wait for its background work to finish.');
113
- }
114
- if (connected.kind === 'failed') {
115
- throw runtimeHostStartupError(connected.reason, connected.diagnostic);
116
- }
117
- if (connected.registration.generation?.startsWith('npm-global-handoff:')) {
118
- const record = await deps.readDeploymentRecord(connected.registration.rootId);
119
- if (record?.state.kind !== 'owned' || record.state.owner.kind !== 'cli') {
120
- await connected.connection.close().catch(() => undefined);
121
- throw new RuntimeHostPermanentReconnectError('RUNTIME_HOST_RECOVERY_REQUIRED: The staged local Runtime Host is Ready, but its installation ownership was not durably committed.');
136
+ if (connected.kind === 'failed' && connected.reason === 'managed_root_requires_operator') {
137
+ await deps.activateLocalManagedHost({
138
+ rootPath: input.rootPath,
139
+ ...(signal ? { signal } : {}),
140
+ });
141
+ signal?.throwIfAborted();
142
+ // The installed operator owns launch. Compatibility is still projected
143
+ // by the same handoff journey as an already-running managed Host.
144
+ const activated = await deps.connectActivatedHost(connectInput);
145
+ if (activated.kind === 'unavailable' || activated.kind === 'draining') {
146
+ throw new Error(`The installed Runtime Host was activated but the local CLI could not join it (${activated.kind === 'unavailable' ? activated.reason : activated.kind}). Use a compatible CLI or update the managed Host through its configured operator.`);
147
+ }
148
+ if (activated.kind === 'connected' && signal?.aborted) {
149
+ await activated.connection.close();
150
+ signal.throwIfAborted();
151
+ }
152
+ connected = activated;
122
153
  }
123
- }
124
- return connected.connection;
154
+ if (connected.kind === 'incompatible' || connected.kind === 'upgrade_required') {
155
+ const managed = await deps.resolveManagedAuthority(connected.registration.rootId);
156
+ const record = managed
157
+ ? undefined
158
+ : await deps.readDeploymentRecord(connected.registration.rootId);
159
+ let replacement;
160
+ let installation;
161
+ let installationFailure;
162
+ // On-demand managed Hosts are also ephemeral processes. Their durable
163
+ // operator authority, not the process lifetime label, decides who may
164
+ // replace them (including when the Host was already running).
165
+ if (!managed && connected.registration.lifecycleMode === 'ephemeral') {
166
+ try {
167
+ installation = await deps.resolveInstallation();
168
+ }
169
+ catch (error) {
170
+ installationFailure = error instanceof Error ? error.message : String(error);
171
+ }
172
+ const owner = record?.state.kind === 'handoff' ? record.state.from : record?.state.owner;
173
+ if (installation &&
174
+ (!owner ||
175
+ (owner.kind === installation.owner.kind &&
176
+ owner.installationId === installation.owner.installationId))) {
177
+ const expectedInstallation = installation;
178
+ const canInterrupt = connected.processIdentity !== undefined ||
179
+ (record
180
+ ? await deps
181
+ .sourceRetirementAvailable({
182
+ rootId: connected.registration.rootId,
183
+ owner: installation.owner,
184
+ source: record.state.selected,
185
+ })
186
+ .catch(() => false)
187
+ : false);
188
+ replacement = {
189
+ kind: record?.state.kind === 'handoff' ? 'repair' : 'replace',
190
+ canReplaceIdle: true,
191
+ canInterrupt,
192
+ execute: async (activeWorkPolicy, _progress, _consent, attemptSignal) => {
193
+ const result = await deps.restartDeployment({
194
+ rootPath: input.rootPath,
195
+ registration: connected.registration,
196
+ ...(connected.processIdentity
197
+ ? { processIdentity: connected.processIdentity }
198
+ : {}),
199
+ activeWorkPolicy,
200
+ expectedInstallation,
201
+ ...(attemptSignal ? { signal: attemptSignal } : {}),
202
+ });
203
+ if (result.kind === 'completed')
204
+ return { kind: 'completed' };
205
+ if (result.kind === 'active_work')
206
+ return { kind: 'active_work' };
207
+ if (result.kind === 'changed' ||
208
+ result.kind === 'rejected' ||
209
+ result.kind === 'operator_required')
210
+ return { kind: 'changed' };
211
+ return {
212
+ kind: 'recovery_required',
213
+ diagnostic: `Local service recovery is required at ${result.phase}`,
214
+ };
215
+ },
216
+ };
217
+ }
218
+ }
219
+ // The same observed-process recovery used by Desktop is available to
220
+ // persistent local invocations without a deployment owner. This is an
221
+ // explicit interruption, never an idle inference or an installation claim.
222
+ const processIdentity = connected.processIdentity;
223
+ if (!replacement &&
224
+ !managed &&
225
+ !record &&
226
+ !invocationOwned &&
227
+ connected.registration.lifecycleMode === 'ephemeral' &&
228
+ processIdentity) {
229
+ const registration = connected.registration;
230
+ replacement = {
231
+ kind: 'replace',
232
+ canReplaceIdle: false,
233
+ canInterrupt: true,
234
+ execute: async (policy, progress, consent, attemptSignal) => {
235
+ if (policy !== 'interrupt_active_work' || consent !== 'explicit') {
236
+ return { kind: 'active_work' };
237
+ }
238
+ attemptSignal?.throwIfAborted();
239
+ if ((await deps.resolveManagedAuthority(registration.rootId)) ||
240
+ (await deps.readDeploymentRecord(registration.rootId)))
241
+ return { kind: 'changed' };
242
+ attemptSignal?.throwIfAborted();
243
+ progress('retiring');
244
+ const stopped = await deps.terminateObservedHost({ rootPath: input.rootPath, registration }, { processIdentity, isCurrent: () => !signal?.aborted && !attemptSignal?.aborted });
245
+ if (!stopped)
246
+ return { kind: 'changed' };
247
+ progress('verifying');
248
+ return { kind: 'completed' };
249
+ },
250
+ };
251
+ }
252
+ return {
253
+ kind: 'blocked',
254
+ blocker: {
255
+ identity: JSON.stringify([
256
+ connected.registration,
257
+ processIdentity,
258
+ managed?.record,
259
+ record,
260
+ installation,
261
+ ]),
262
+ target: {
263
+ name: profile.name,
264
+ location: 'local',
265
+ rootId: connected.registration.rootId,
266
+ hostEpoch: connected.registration.hostEpoch,
267
+ },
268
+ reason: record?.state.kind === 'handoff' ? 'repair' : 'upgrade',
269
+ ...(!replacement
270
+ ? {
271
+ recoveryBlocker: managed || connected.registration.lifecycleMode === 'service'
272
+ ? 'managed'
273
+ : record
274
+ ? 'owner'
275
+ : invocationOwned
276
+ ? 'installation'
277
+ : 'identity',
278
+ diagnostic: [
279
+ `Host compatibility: ${connected.registration.compatibilityEpoch}; client: ${RUNTIME_HOST_COMPATIBILITY_EPOCH}.`,
280
+ installationFailure,
281
+ ]
282
+ .filter(Boolean)
283
+ .join('\n'),
284
+ }
285
+ : {}),
286
+ ...(connected.handshake?.activity ? { activity: connected.handshake.activity } : {}),
287
+ mayExitNaturally: !managed &&
288
+ connected.registration.lifecycleMode === 'ephemeral' &&
289
+ connected.handshake?.replacement === 'wait_for_idle_exit',
290
+ ...(replacement ? { replacement } : {}),
291
+ },
292
+ };
293
+ }
294
+ if (connected.kind === 'failed') {
295
+ throw runtimeHostStartupError(connected.reason, connected.diagnostic);
296
+ }
297
+ if (connected.registration.generation?.startsWith('npm-global-handoff:')) {
298
+ const record = await deps.readDeploymentRecord(connected.registration.rootId);
299
+ if (record?.state.kind !== 'owned' || record.state.owner.kind !== 'cli') {
300
+ await connected.connection.close().catch(() => undefined);
301
+ throw new RuntimeHostPermanentReconnectError('RUNTIME_HOST_RECOVERY_REQUIRED: The staged local Runtime Host is Ready, but its installation ownership was not durably committed.');
302
+ }
303
+ }
304
+ return { kind: 'ready', value: connected.connection };
305
+ };
306
+ return runHostHandoff({ observe, signal, openSurface: input.handoffSurface });
125
307
  };
126
308
  let initialConnection;
127
309
  let connection;
@@ -222,42 +404,6 @@ async function resolveHostProfile(input, deps) {
222
404
  const catalog = deps.profileCatalog ?? createClientRuntimeHostProfileCatalog(root);
223
405
  return catalog.resolve(input.profileId);
224
406
  }
225
- function formatRuntimeHostCliConflict(handshake, registration) {
226
- const lines = [
227
- 'RUNTIME_HOST_RESTART_REQUIRED: An older Runtime Host is still running and cannot accept this client.',
228
- `Local Runtime Host: PID ${registration.pid}; lifecycle ${registration.lifecycleMode ?? 'unknown'}; compatibility epoch ${registration.compatibilityEpoch}.`,
229
- ];
230
- if (registration.lifecycleMode === 'ephemeral') {
231
- lines.push('The ephemeral Host is not currently idle and cannot be replaced by this Client.');
232
- }
233
- else if (registration.lifecycleMode === 'service') {
234
- lines.push('This service Host is managed by its operator and cannot be replaced by this Client.');
235
- }
236
- else {
237
- lines.push('This Host cannot be replaced by this Client.');
238
- }
239
- if (handshake.compatibilityEpoch < RUNTIME_HOST_COMPATIBILITY_EPOCH) {
240
- lines.push(registration.lifecycleMode === 'service'
241
- ? 'Use the service operator to inspect or upgrade the Host.'
242
- : 'Use a previous compatible Maka build to inspect the Host and finish or clear any retained work. Stop the Host only after deciding that interruption is safe.');
243
- }
244
- else {
245
- lines.push(`Host protocol ${handshake.protocolMin}-${handshake.protocolMax}; CLI protocol ${RUNTIME_HOST_PROTOCOL_VERSION}.`);
246
- }
247
- return lines.join('\n');
248
- }
249
- export function shouldRetryRuntimeHostConflict(answer) {
250
- const normalized = answer.trim().toLowerCase();
251
- return normalized === 'w' || normalized === 'wait';
252
- }
253
- export function resolveRuntimeHostCliConflictDecision(answer, canRestart) {
254
- const normalized = answer.trim().toLowerCase();
255
- if (canRestart && (normalized === 'r' || normalized === 'restart'))
256
- return 'restart';
257
- if (normalized === 'w' || normalized === 'wait')
258
- return 'wait';
259
- return 'cancel';
260
- }
261
407
  export function resolveRuntimeHostCliTarget(catalog, input = {}) {
262
408
  const defaultTarget = catalog.defaultTarget;
263
409
  const connection = input.connectionSlug
@@ -76,9 +76,15 @@ export async function resolveRuntimeHostNpmGlobalInstallation(options = {}, over
76
76
  observedRelease: { version: manifest.version, packageRoot, cliPath },
77
77
  };
78
78
  }
79
- export async function isTemporaryNpxInstallation(path, input) {
79
+ export async function isTemporaryNpxInstallation(path = fileURLToPath(new URL('../', import.meta.url)), input = { environment: process.env, homeDir: homedir() }) {
80
80
  const canonicalPath = await realpath(path).catch(() => resolve(path));
81
- const cacheRoots = await Promise.all([input.environment.npm_config_cache, join(input.homeDir, '.npm')].flatMap((root) => root ? [realpath(resolve(root, '_npx')).catch(() => resolve(root, '_npx'))] : []));
81
+ const defaultCache = (input.platform ?? process.platform) === 'win32'
82
+ ? join(input.environment.LOCALAPPDATA || input.homeDir, 'npm-cache')
83
+ : join(input.homeDir, '.npm');
84
+ const cacheRoots = await Promise.all([
85
+ input.environment.npm_config_cache ?? input.environment.NPM_CONFIG_CACHE,
86
+ defaultCache,
87
+ ].flatMap((root) => root ? [realpath(resolve(root, '_npx')).catch(() => resolve(root, '_npx'))] : []));
82
88
  return cacheRoots.some((root) => isWithin(root, canonicalPath));
83
89
  }
84
90
  async function readPackageManifest(url) {
@@ -136,6 +136,10 @@ function parseLocalUpdateApply(argv) {
136
136
  '--target-version',
137
137
  '--target-integrity',
138
138
  '--target-compatibility',
139
+ '--expected-root-id',
140
+ '--expected-deployment-revision',
141
+ '--expected-owner-installation-id',
142
+ '--expected-host-epoch',
139
143
  ]);
140
144
  for (let index = 0; index < argv.length; index += 1) {
141
145
  const argument = argv[index];
@@ -195,6 +199,28 @@ function parseLocalUpdateApply(argv) {
195
199
  (!Number.isSafeInteger(targetCompatibility) || targetCompatibility <= 0)) {
196
200
  return error('runtime-host local-update-apply compatibility is invalid');
197
201
  }
202
+ const expectedValues = [
203
+ '--expected-root-id',
204
+ '--expected-deployment-revision',
205
+ '--expected-owner-installation-id',
206
+ '--expected-host-epoch',
207
+ ].map((name) => values.get(name));
208
+ let expectedSource;
209
+ if (expectedValues.some((value) => value !== undefined)) {
210
+ if (!expectedValues.every((value) => value !== undefined && isSafeIdentity(value))) {
211
+ return error('runtime-host local-update-apply requires one complete expected source identity');
212
+ }
213
+ const [rootId, deploymentRevision, ownerInstallationId, hostEpoch] = expectedValues;
214
+ if (!/^[a-f0-9]{64}$/u.test(rootId)) {
215
+ return error('runtime-host local-update-apply expected Root identity is invalid');
216
+ }
217
+ expectedSource = {
218
+ rootId: rootId,
219
+ deploymentRevision: deploymentRevision,
220
+ ownerInstallationId: ownerInstallationId,
221
+ hostEpoch: hostEpoch,
222
+ };
223
+ }
198
224
  return {
199
225
  kind: 'runtime-host-local-update-apply',
200
226
  rootPath,
@@ -206,6 +232,7 @@ function parseLocalUpdateApply(argv) {
206
232
  targetIntegrity,
207
233
  ...(targetCompatibility === undefined ? {} : { targetCompatibility }),
208
234
  allowInterruptActiveTasks,
235
+ ...(expectedSource ? { expectedSource } : {}),
209
236
  };
210
237
  }
211
238
  function parseLocalUpdateActivate(argv) {
@@ -553,7 +580,7 @@ function parseServiceManagementCommand(argv) {
553
580
  },
554
581
  }
555
582
  : {}),
556
- ...(action === 'update'
583
+ ...(action === 'update' || action === 'restart'
557
584
  ? {
558
585
  '--expected-host-json': (value) => {
559
586
  if (expectedHost !== undefined)
@@ -675,10 +702,13 @@ function parseServiceManagementCommand(argv) {
675
702
  ...(allowInterruptActiveTasks ? { allowInterruptActiveTasks: true } : {}),
676
703
  };
677
704
  }
705
+ if (expectedHost && !options.managedRootId)
706
+ return error('--expected-host-json requires --managed-root-id');
678
707
  return {
679
708
  kind: 'runtime-host-service-manage',
680
709
  action,
681
710
  ...options,
711
+ ...(expectedHost ? { expectedHost } : {}),
682
712
  ...(clientDataRoot ? { clientDataRoot } : {}),
683
713
  ...(options.managedRootId ? { managedRootId: options.managedRootId } : {}),
684
714
  ...(retainManagedDeployment ? { retainManagedDeployment: true } : {}),