maka-agent 0.2.0-dev.67.20261002 → 0.2.0-dev.69.20261004

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 (28) hide show
  1. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  2. package/node_modules/@maka/acp-executor-plugin/dist/acp-catalog.js +132 -0
  3. package/node_modules/@maka/acp-executor-plugin/dist/index.js +251 -113
  4. package/node_modules/@maka/acp-executor-plugin/dist/plugin.mjs +431 -108
  5. package/node_modules/@maka/core/dist/executor-catalog.js +25 -3
  6. package/node_modules/@maka/core/dist/recall.js +13 -14
  7. package/node_modules/@maka/runtime/dist/admission-limiter.js +3 -2
  8. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +0 -3
  9. package/node_modules/@maka/runtime/dist/model-history.js +4 -14
  10. package/node_modules/@maka/runtime/dist/plugin-data-services.js +7 -0
  11. package/node_modules/@maka/runtime/dist/plugin-executor-service.js +19 -3
  12. package/node_modules/@maka/runtime/dist/runtime-read-model.js +23 -4
  13. package/node_modules/@maka/runtime/dist/session-manager.js +1 -0
  14. package/node_modules/@maka/runtime/package.json +1 -0
  15. package/node_modules/@maka/runtime-host/dist/antigravity-program.js +49 -0
  16. package/node_modules/@maka/runtime-host/dist/protocol/index.js +3 -1
  17. package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +8 -2
  18. package/node_modules/@maka/runtime-host/dist/server/acp/antigravity.js +5 -19
  19. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +14 -3
  20. package/node_modules/@maka/runtime-host/dist/server/external-agent-setup-coordinator.js +3 -0
  21. package/node_modules/@maka/runtime-host/dist/server/plugin-data-runtime.js +6 -0
  22. package/node_modules/@maka/runtime-host/dist/server/plugin-scratch-directory.js +55 -0
  23. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +40 -6
  24. package/node_modules/@maka/runtime-host/package.json +1 -0
  25. package/node_modules/@maka/storage/dist/execution-stores.js +10 -0
  26. package/node_modules/@maka/storage/dist/runtime-event-persistence.js +1 -0
  27. package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +74 -5
  28. package/package.json +1 -1
@@ -23,12 +23,17 @@ export function isExecutorConfiguration(value) {
23
23
  if (!value || typeof value !== 'object' || Array.isArray(value))
24
24
  return false;
25
25
  const record = value;
26
- return (Object.keys(record).every((key) => key === 'model') &&
26
+ return (Object.keys(record).every((key) => key === 'model' || key === 'mode') &&
27
27
  (record.model === undefined ||
28
28
  (typeof record.model === 'string' &&
29
29
  record.model.length > 0 &&
30
30
  record.model.length <= 1024 &&
31
- !/[\0\r\n]/u.test(record.model))));
31
+ !/[\0\r\n]/u.test(record.model))) &&
32
+ (record.mode === undefined ||
33
+ (typeof record.mode === 'string' &&
34
+ record.mode.length > 0 &&
35
+ record.mode.length <= 1024 &&
36
+ !/[\0\r\n]/u.test(record.mode))));
32
37
  }
33
38
  export function normalizeCatalogEntry(value, id) {
34
39
  if (!value ||
@@ -54,8 +59,18 @@ export function normalizeCatalogEntry(value, id) {
54
59
  (model.providerType === undefined || CATALOG_PROVIDER_TYPES.includes(model.providerType))) ||
55
60
  new Set(value.models.map((model) => model.id)).size !== value.models.length ||
56
61
  !isExecutorConfiguration({ model: value.currentModel }) ||
62
+ (value.modes !== undefined &&
63
+ (!Array.isArray(value.modes) ||
64
+ value.modes.length > 64 ||
65
+ !value.modes.every((mode) => mode &&
66
+ typeof mode.id === 'string' &&
67
+ isExecutorConfiguration({ mode: mode.id }) &&
68
+ isCatalogText(mode.name)) ||
69
+ new Set(value.modes.map((mode) => mode.id)).size !== value.modes.length)) ||
70
+ !isExecutorConfiguration({ mode: value.currentMode }) ||
57
71
  typeof value.supportsAttachments !== 'boolean' ||
58
- typeof value.supportsModelChange !== 'boolean')
72
+ typeof value.supportsModelChange !== 'boolean' ||
73
+ (value.supportsModeChange !== undefined && typeof value.supportsModeChange !== 'boolean'))
59
74
  throw new TypeError('Executor catalog is invalid');
60
75
  const usedModels = new Set();
61
76
  const groupIds = new Set();
@@ -106,8 +121,15 @@ export function normalizeCatalogEntry(value, id) {
106
121
  }
107
122
  : {}),
108
123
  ...(value.currentModel !== undefined ? { currentModel: value.currentModel } : {}),
124
+ ...(value.modes !== undefined
125
+ ? { modes: Object.freeze(value.modes.map(({ id, name }) => Object.freeze({ id, name }))) }
126
+ : {}),
127
+ ...(value.currentMode !== undefined ? { currentMode: value.currentMode } : {}),
109
128
  supportsAttachments: value.supportsAttachments,
110
129
  supportsModelChange: value.supportsModelChange,
130
+ ...(value.supportsModeChange !== undefined
131
+ ? { supportsModeChange: value.supportsModeChange }
132
+ : {}),
111
133
  });
112
134
  }
113
135
  function isCatalogText(value) {
@@ -682,6 +682,18 @@ async function collectHits(deps, input) {
682
682
  scannedFully = false;
683
683
  }
684
684
  }
685
+ // A narrowed scan needs the corpus count to rank the same way as a full
686
+ // scan. Resolve it before reading transcripts so a declined count cannot
687
+ // discard work and reread every candidate during fallback.
688
+ const storedCorpusSize = deps.countSearchableMessages
689
+ ? await deps.countSearchableMessages({ sessionIds: input.sessionIds })
690
+ : null;
691
+ if (input.abortSignal?.aborted)
692
+ return null;
693
+ if (storedCorpusSize === null && !scannedFully) {
694
+ sessionIds = input.sessionIds;
695
+ scannedFully = true;
696
+ }
685
697
  const hits = [];
686
698
  let counted = 0;
687
699
  for (const sessionId of sessionIds) {
@@ -711,20 +723,7 @@ async function collectHits(deps, input) {
711
723
  hits.push(hit);
712
724
  }
713
725
  }
714
- // The narrowed path never sees the Sessions it skipped, so its corpus size
715
- // has to come from the store. The full scan takes the same number when the
716
- // store offers one, which is what keeps a score independent of the path.
717
- let corpusSize = counted;
718
- if (deps.countSearchableMessages) {
719
- corpusSize = await deps.countSearchableMessages({ sessionIds: input.sessionIds });
720
- if (input.abortSignal?.aborted)
721
- return null;
722
- if (corpusSize === null) {
723
- if (!scannedFully)
724
- return collectHits(deps, { ...input, forceFullScan: true });
725
- corpusSize = counted;
726
- }
727
- }
726
+ const corpusSize = storedCorpusSize ?? counted;
728
727
  return { hits, corpusSize, scannedFully, transcripts };
729
728
  }
730
729
  /**
@@ -19,12 +19,13 @@
19
19
  /**
20
20
  * Abort-aware FIFO permits, bounded by capacity.
21
21
  *
22
- * Two boundaries use it, at different lifetimes. Child-agent runs take one
22
+ * Boundaries use it at different lifetimes. Child-agent runs take one
23
23
  * instance per turn: tool-call admission and child-run capacity are separate
24
24
  * boundaries, since one admitted tool may eventually spawn multiple children,
25
25
  * so the limiter belongs at the narrow spawn capability where every caller
26
26
  * shares the same real-run budget. Code Mode cells take one per backend, which
27
- * has to outlive a turn — see `executeCodeModeCell`.
27
+ * has to outlive a turn — see `executeCodeModeCell`. ACP catalog probes share
28
+ * one per runtime Entry and hold permits through process cleanup.
28
29
  *
29
30
  * A caller that must turn work away rather than queue it reads `waitingCount`
30
31
  * before calling `acquire`. Nothing awaits between that read and the enqueue
@@ -1127,9 +1127,6 @@ export class AiSdkTurn {
1127
1127
  }
1128
1128
  const requestSystemPromptBase = joinPromptFragments([
1129
1129
  systemPrompt,
1130
- priorUnknownProjection.kind === 'projected'
1131
- ? priorUnknownProjection.systemNotice
1132
- : undefined,
1133
1130
  finalChildSummaryStep ? CHILD_STEP_BUDGET_FINALIZATION_PROMPT : undefined,
1134
1131
  toolRuntime.hasSandboxBoundaryDenial() ? SANDBOX_BOUNDARY_DENIED_FOR_TURN : undefined,
1135
1132
  sandboxBoundaryFinalizationStep ? SANDBOX_BOUNDARY_FINALIZATION_PROMPT : undefined,
@@ -177,20 +177,10 @@ export function inspectPriorUnknownToolOutcomes(events, invocations) {
177
177
  }
178
178
  if (outcomes.length === 0)
179
179
  return { kind: 'none' };
180
- const rows = outcomes
181
- .slice(0, 64)
182
- .map((outcome) => `- ${outcome.toolName} (operation ${outcome.operationId})`);
183
- if (outcomes.length > 64)
184
- rows.push(`- and ${outcomes.length - 64} more unknown operations`);
185
- return {
186
- kind: 'projected',
187
- outcomes,
188
- systemNotice: [
189
- 'A prior execution was interrupted after these tools were dispatched. No durable tool results were recorded; their side effects may or may not have happened.',
190
- 'The outcome_unknown tool responses in history are temporary context, not proof of success or failure. Inspect current state before repeating any action.',
191
- ...rows,
192
- ].join('\n'),
193
- };
180
+ // The unknown is carried only by the request-local tool responses below
181
+ // (appendPriorUnknownToolResponses): the system prompt stays byte-stable,
182
+ // so the provider's cached request prefix is never churned per event.
183
+ return { kind: 'projected', outcomes };
194
184
  }
195
185
  /** Insert request-only tool responses beside their calls so provider history stays well-formed. */
196
186
  export function appendPriorUnknownToolResponses(events, projection) {
@@ -131,6 +131,13 @@ export class PluginStorageService extends PluginNamespacedDataService {
131
131
  const result = await this.mutateValues([{ key, value, ...options }]);
132
132
  return result[key];
133
133
  }
134
+ /** The callback must drain any process using the directory before settling. */
135
+ withScratchDirectory(key, signal, use) {
136
+ const runtime = this.runtime();
137
+ if (!runtime.withScratchDirectory)
138
+ throw new Error('Plugin scratch directories are unavailable');
139
+ return runtime.withScratchDirectory(this.namespace(), validKey(key), signal, use);
140
+ }
134
141
  async delete(key, options = {}) {
135
142
  const result = await this.mutateValues([{ key, ...options }]);
136
143
  return result[key];
@@ -66,6 +66,9 @@ export class PluginExecutorService extends Service {
66
66
  ? { configureConversation: provider.configureConversation.bind(provider) }
67
67
  : {}),
68
68
  ...(provider.discover ? { discover: provider.discover.bind(provider) } : {}),
69
+ ...(provider.invalidateCatalog
70
+ ? { invalidateCatalog: provider.invalidateCatalog.bind(provider) }
71
+ : {}),
69
72
  ...(provider.inspectConversation
70
73
  ? { inspectConversation: provider.inspectConversation.bind(provider) }
71
74
  : {}),
@@ -152,7 +155,11 @@ export class PluginExecutorService extends Service {
152
155
  cwd: input.cwd,
153
156
  ...(input.configuration ? { configuration: input.configuration } : {}),
154
157
  })
155
- : await entry.provider.discover?.({ cwd: input.cwd, signal: combined });
158
+ : await entry.provider.discover?.({
159
+ cwd: input.cwd,
160
+ signal: combined,
161
+ ...(input.refresh ? { refresh: true } : {}),
162
+ });
156
163
  combined.throwIfAborted();
157
164
  if (!result)
158
165
  return fallback;
@@ -163,6 +170,12 @@ export class PluginExecutorService extends Service {
163
170
  }
164
171
  })));
165
172
  }
173
+ invalidateCatalog() {
174
+ for (const entry of this.registry.entries('profile')) {
175
+ if (!entry.retired)
176
+ entry.provider.invalidateCatalog?.();
177
+ }
178
+ }
166
179
  async configureConversation(sessionId, executorId, input) {
167
180
  const entry = this.entry(sessionId, executorId);
168
181
  const configure = entry.provider.configureConversation;
@@ -171,8 +184,11 @@ export class PluginExecutorService extends Service {
171
184
  throw new Error('Executor configuration is unavailable or busy');
172
185
  if (!isExecutorConfiguration(input.configuration))
173
186
  throw new TypeError('Invalid executor configuration');
174
- await this.withActiveOperation(entry, { conversationKey: input.conversationKey }, async (signal) => {
175
- await configure(input, AbortSignal.any([signal, AbortSignal.timeout(30_000)]));
187
+ return await this.withActiveOperation(entry, { conversationKey: input.conversationKey }, async (signal) => {
188
+ const confirmed = await configure(input, AbortSignal.any([signal, AbortSignal.timeout(30_000)]));
189
+ if (confirmed !== undefined && !isExecutorConfiguration(confirmed))
190
+ throw new TypeError('Invalid confirmed executor configuration');
191
+ return confirmed;
176
192
  });
177
193
  }
178
194
  /** Task retirement releases retained provider resources; backend refresh must not. */
@@ -42,9 +42,23 @@ export class RuntimeReadModel {
42
42
  async getSessionView(sessionId) {
43
43
  const diagnostics = [];
44
44
  const inFlightTurnIds = new Set();
45
+ let snapshot;
46
+ if (this.deps.runtimeEventStore.readSessionRuntimeSnapshot) {
47
+ try {
48
+ snapshot = await this.deps.runtimeEventStore.readSessionRuntimeSnapshot(sessionId);
49
+ }
50
+ catch (error) {
51
+ throw new RuntimeReadModelError('RuntimeEvent Session snapshot read failed', [
52
+ readModelDiagnostic('unsupported_event', 'RuntimeEventStore.readSessionRuntimeSnapshot failed', {
53
+ error: errorMessage(error),
54
+ }),
55
+ ]);
56
+ }
57
+ }
45
58
  let invocations;
46
59
  try {
47
- invocations = (await this.deps.runtimeEventStore.listSessionInvocations(sessionId)).filter((invocation) => isSessionInlineInvocation(invocation.opening));
60
+ invocations = (snapshot?.invocations ??
61
+ (await this.deps.runtimeEventStore.listSessionInvocations(sessionId))).filter((invocation) => isSessionInlineInvocation(invocation.opening));
48
62
  }
49
63
  catch (error) {
50
64
  throw new RuntimeReadModelError('RuntimeReadModel could not list Session invocations', [
@@ -56,15 +70,20 @@ export class RuntimeReadModel {
56
70
  if (invocations.length === 0) {
57
71
  return this.buildView({ invocations, events: [], diagnostics });
58
72
  }
59
- const durableEventOrdinals = await this.readSessionRuntimeEventOrdinals(sessionId);
60
- const durableEventOrdinalById = new Map(durableEventOrdinals.map(({ event, ordinal }) => [event.id, ordinal]));
73
+ const durableEventOrdinalById = snapshot?.durableEventOrdinalById ??
74
+ new Map((await this.readSessionRuntimeEventOrdinals(sessionId)).map(({ event, ordinal }) => [
75
+ event.id,
76
+ ordinal,
77
+ ]));
61
78
  const ordered = [];
62
79
  const terminalFacts = [];
63
80
  for (let runIndex = 0; runIndex < invocations.length; runIndex += 1) {
64
81
  const invocation = invocations[runIndex];
65
82
  let runEvents;
66
83
  try {
67
- runEvents = await this.deps.runtimeEventStore.readRuntimeEvents(sessionId, invocation.runId);
84
+ runEvents = snapshot
85
+ ? (snapshot.eventsByRun.get(invocation.runId) ?? [])
86
+ : await this.deps.runtimeEventStore.readRuntimeEvents(sessionId, invocation.runId);
68
87
  }
69
88
  catch (error) {
70
89
  throw new RuntimeReadModelError('RuntimeEvent ledger read failed', [
@@ -3599,6 +3599,7 @@ function sessionConfigurationMatchesExceptPermissionMode(header, configuration)
3599
3599
  return (header.backend === configuration.backend &&
3600
3600
  header.executorId === configuration.executorId &&
3601
3601
  header.executorConfig?.model === configuration.executorConfig?.model &&
3602
+ header.executorConfig?.mode === configuration.executorConfig?.mode &&
3602
3603
  header.llmConnectionId === configuration.llmConnectionId &&
3603
3604
  header.llmConnectionSlug === configuration.llmConnectionSlug &&
3604
3605
  header.connectionLocked === configuration.connectionLocked &&
@@ -5,6 +5,7 @@
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "exports": {
8
+ "./admission-limiter": "./dist/admission-limiter.js",
8
9
  "./ask-user-question-tool": "./dist/ask-user-question-tool.js",
9
10
  "./ai-sdk-backend": "./dist/ai-sdk-backend.js",
10
11
  "./mcp-tools": "./dist/mcp-tools.js",
@@ -0,0 +1,49 @@
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 { constants } from 'node:fs';
20
+ import { access, realpath, stat } from 'node:fs/promises';
21
+ import { dirname, isAbsolute, join } from 'node:path';
22
+ /** Public diagnostics survive Electron IPC without exposing filesystem error details. */
23
+ export class AntigravityProgramError extends Error {
24
+ failure;
25
+ constructor(failure) {
26
+ super(`Antigravity program: ${failure}`);
27
+ this.failure = failure;
28
+ }
29
+ }
30
+ /** Shared by native program selection and the Host's check/sign-in admission. */
31
+ export async function checkAntigravityProgram(executable) {
32
+ const resolved = await checkedFile(executable, 'executable_unavailable');
33
+ const helper = await checkedFile(join(dirname(resolved), 'localharness_external'), 'helper_unavailable');
34
+ return { executable: resolved, helper };
35
+ }
36
+ async function checkedFile(path, failure) {
37
+ try {
38
+ if (!isAbsolute(path))
39
+ throw new Error('Absolute path required');
40
+ const resolved = await realpath(path);
41
+ if (!(await stat(resolved)).isFile())
42
+ throw new Error('File required');
43
+ await access(resolved, constants.X_OK);
44
+ return resolved;
45
+ }
46
+ catch {
47
+ throw new AntigravityProgramError(failure);
48
+ }
49
+ }
@@ -65,7 +65,9 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1;
65
65
  export const RUNTIME_HOST_PROTOCOL_VERSION = 0;
66
66
  // Increment when the same protocol version no longer guarantees safe Client-Host
67
67
  // interoperability. Mismatches are rejected before domain commands are admitted.
68
- export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 202;
68
+ export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 204;
69
+ // 204: Executor catalogs and Session configuration carry opaque mode IDs;
70
+ // catalog queries may request a provider refresh. Older peers reject these fields.
69
71
  // 202: `session.remove.preview` takes a bounded list of Sessions and reports the
70
72
  // child tasks, worktrees and optionally the bytes their removal would delete;
71
73
  // `session.remove` takes `requireArchivedForMs` and may answer `too_recent`.
@@ -93,8 +93,14 @@ export const PLUGIN_PLATFORM_OPERATION_SPECS = {
93
93
  decodeInput: (value) => {
94
94
  const record = requireRecord(value, 'Executor query');
95
95
  if (record.kind === 'catalog') {
96
- const input = requireExactRecord(record, 'Executor catalog', ['kind', 'cwd']);
97
- return { kind: 'catalog', cwd: requireString(input.cwd, 'cwd', 4096) };
96
+ const input = requireShapedRecord(record, 'Executor catalog', ['kind', 'cwd'], ['refresh']);
97
+ if (input.refresh !== undefined && typeof input.refresh !== 'boolean')
98
+ throw invalidProtocolFrame('Invalid executor refresh flag');
99
+ return {
100
+ kind: 'catalog',
101
+ cwd: requireString(input.cwd, 'cwd', 4096),
102
+ ...(input.refresh ? { refresh: true } : {}),
103
+ };
98
104
  }
99
105
  const input = requireExactRecord(record, 'Executor conversation', ['kind', 'sessionId']);
100
106
  if (input.kind !== 'conversation')
@@ -16,12 +16,11 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { constants } from 'node:fs';
20
- import { access, realpath, stat } from 'node:fs/promises';
21
- import { dirname, join, isAbsolute } from 'node:path';
19
+ import { dirname } from 'node:path';
22
20
  import { StringDecoder } from 'node:string_decoder';
23
21
  import { methods, RequestError } from '@agentclientprotocol/sdk';
24
22
  import { AcpSetupError, withAcpConnection } from './connection.js';
23
+ import { checkAntigravityProgram, AntigravityProgramError } from '../../antigravity-program.js';
25
24
  const AUTH_PREFIX = 'Open the following link to authenticate the ACP server: ';
26
25
  const INITIALIZE_TIMEOUT_MS = 30_000;
27
26
  const LOGIN_TIMEOUT_MS = 5 * 60_000;
@@ -45,8 +44,7 @@ export async function runAntigravitySetup(input) {
45
44
  });
46
45
  try {
47
46
  signal.throwIfAborted();
48
- const executable = await checkedFile(input.executable, 'executable_unavailable');
49
- const helper = await checkedFile(join(dirname(executable), 'localharness_external'), 'helper_unavailable');
47
+ const { executable, helper } = await checkAntigravityProgram(input.executable);
50
48
  await withAcpConnection({
51
49
  executable,
52
50
  cwd: dirname(executable),
@@ -96,6 +94,8 @@ export async function runAntigravitySetup(input) {
96
94
  });
97
95
  }
98
96
  catch (error) {
97
+ if (error instanceof AntigravityProgramError)
98
+ throw new AcpSetupError(error.failure);
99
99
  if (error instanceof AcpSetupError)
100
100
  throw error;
101
101
  signal.throwIfAborted();
@@ -105,20 +105,6 @@ export async function runAntigravitySetup(input) {
105
105
  clearTimeout(timeout);
106
106
  }
107
107
  }
108
- async function checkedFile(path, failure) {
109
- try {
110
- if (!isAbsolute(path))
111
- throw new Error('Absolute path required');
112
- const resolved = await realpath(path);
113
- if (!(await stat(resolved)).isFile())
114
- throw new Error('File required');
115
- await access(resolved, constants.X_OK);
116
- return resolved;
117
- }
118
- catch {
119
- throw new AcpSetupError(failure);
120
- }
121
- }
122
108
  /** Official 1.1.1 prints its login link to stderr; ordinary diagnostics are discarded. */
123
109
  export function createAntigravityStderrHandler(onAuthorizationUrl) {
124
110
  const recognize = (line) => {
@@ -274,7 +274,7 @@ export async function createExecutionRuntimeHostComposition(context, options = {
274
274
  ...(header.executorConfig ? { configuration: header.executorConfig } : {}),
275
275
  });
276
276
  }
277
- const catalog = await pluginExecutors.catalog({ cwd: input.cwd });
277
+ const catalog = await pluginExecutors.catalog({ cwd: input.cwd, refresh: input.refresh });
278
278
  return withBuiltinExternalAgentCatalog(catalog);
279
279
  });
280
280
  const openedProjectCatalog = storage.projectCatalog;
@@ -1333,6 +1333,10 @@ export async function createExecutionRuntimeHostComposition(context, options = {
1333
1333
  context.retainUntilProcessExit();
1334
1334
  context.requestDrain();
1335
1335
  },
1336
+ onSucceeded: () => {
1337
+ pluginExecutors.invalidateCatalog();
1338
+ hostChanges.publishConfiguration();
1339
+ },
1336
1340
  capabilities: clientCapabilities,
1337
1341
  });
1338
1342
  oauth = new HostOAuthCoordinator({
@@ -1759,6 +1763,7 @@ export async function createExecutionRuntimeHostComposition(context, options = {
1759
1763
  });
1760
1764
  async function applyRuntimePolicyMutationEffects() {
1761
1765
  try {
1766
+ pluginExecutors.invalidateCatalog();
1762
1767
  await builtinExternalAgentPlugins.reconcile();
1763
1768
  await requireMemory(memory).refreshAfterPolicyMutation();
1764
1769
  }
@@ -1783,10 +1788,11 @@ export async function createExecutionRuntimeHostComposition(context, options = {
1783
1788
  continuity: continuityCoordinator,
1784
1789
  workspaceResolver,
1785
1790
  requestDrain: context.requestDrain,
1791
+ isTurnBusy: (sessionId) => rootCoordinator?.hasActiveOrPendingTurn(sessionId) ?? false,
1786
1792
  configureExecutor: async (header, configuration) => {
1787
1793
  if (!header.executorId)
1788
1794
  throw new Error('Session has no executor');
1789
- await pluginExecutors.configureConversation(header.id, header.executorId, {
1795
+ return await pluginExecutors.configureConversation(header.id, header.executorId, {
1790
1796
  conversationKey: header.id,
1791
1797
  cwd: header.cwd,
1792
1798
  configuration,
@@ -1804,10 +1810,15 @@ export async function createExecutionRuntimeHostComposition(context, options = {
1804
1810
  throw new Error('Executor is not ready');
1805
1811
  // Catalog-managed executors pin their confirmed configuration on every create path.
1806
1812
  // Providers without model discovery retain main's executor-specific model contract.
1807
- if (entry.supportsModelChange || entry.models.length > 0) {
1813
+ if (entry.supportsModelChange ||
1814
+ entry.models.length > 0 ||
1815
+ entry.supportsModeChange ||
1816
+ entry.modes?.length) {
1808
1817
  if (configuration?.model &&
1809
1818
  !entry.models.some((model) => model.id === configuration.model))
1810
1819
  throw new Error('Executor model is unavailable');
1820
+ if (configuration?.mode && !entry.modes?.some((mode) => mode.id === configuration.mode))
1821
+ throw new Error('Executor mode is unavailable');
1811
1822
  return configuration ?? {};
1812
1823
  }
1813
1824
  },
@@ -132,6 +132,7 @@ export class HostExternalAgentSetupCoordinator {
132
132
  });
133
133
  attempt.abort.signal.throwIfAborted();
134
134
  attempt.projection = { ...attempt.projection, phase: 'succeeded', installedExecutable };
135
+ this.deps.onSucceeded?.();
135
136
  return;
136
137
  }
137
138
  await (this.deps.run ?? runAntigravitySetup)({
@@ -157,6 +158,8 @@ export class HostExternalAgentSetupCoordinator {
157
158
  }
158
159
  },
159
160
  });
161
+ if (!attempt.abort.signal.aborted)
162
+ this.deps.onSucceeded?.();
160
163
  attempt.projection = {
161
164
  ...attempt.projection,
162
165
  phase: attempt.abort.signal.aborted ? 'cancelled' : 'succeeded',
@@ -19,6 +19,7 @@
19
19
  import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
20
20
  import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
21
21
  import { dirname, join } from 'node:path';
22
+ import { withPluginScratchDirectory } from './plugin-scratch-directory.js';
22
23
  const NAMESPACE_QUOTA_BYTES = 10 * 1024 * 1024;
23
24
  /** Durable per-extension data authority. JSON writes are atomic; secrets are AES-GCM sealed at rest. */
24
25
  export class HostPluginDataRuntime {
@@ -29,6 +30,11 @@ export class HostPluginDataRuntime {
29
30
  constructor(controlDirectory) {
30
31
  this.#root = join(controlDirectory, 'plugin-data');
31
32
  }
33
+ withScratchDirectory(namespace, key, signal, use) {
34
+ validateNamespace(namespace);
35
+ const identity = createHash('sha256').update(key).digest('hex');
36
+ return withPluginScratchDirectory(join(this.#root, digest(namespace), 'scratch', identity), signal, use);
37
+ }
32
38
  async read(namespace, domain, key) {
33
39
  return await this.#withNamespace(namespace, async () => {
34
40
  const item = (await this.#readData(namespace, domain)).values[key];
@@ -0,0 +1,55 @@
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 { lstat, mkdir, realpath, rm } from 'node:fs/promises';
20
+ import { join } from 'node:path';
21
+ import { setTimeout as delay } from 'node:timers/promises';
22
+ import { tryAcquireFileLifetimeOwner } from '@maka/storage/file-lifetime-owner';
23
+ /** Native leases cover separate runtime instances and are released by the OS on exit. */
24
+ export async function withPluginScratchDirectory(root, signal, use) {
25
+ signal.throwIfAborted();
26
+ const lease = await acquire(root, signal);
27
+ const directory = join(root, 'workspace');
28
+ try {
29
+ signal.throwIfAborted();
30
+ await rm(directory, { recursive: true, force: true });
31
+ await mkdir(directory, { mode: 0o700 });
32
+ const state = await lstat(directory);
33
+ if (!state.isDirectory() || state.isSymbolicLink())
34
+ throw new Error('Plugin scratch cwd is not a directory');
35
+ signal.throwIfAborted();
36
+ return await use(await realpath(directory));
37
+ }
38
+ finally {
39
+ try {
40
+ await rm(directory, { recursive: true, force: true });
41
+ }
42
+ finally {
43
+ await lease.close();
44
+ }
45
+ }
46
+ }
47
+ async function acquire(root, signal) {
48
+ while (true) {
49
+ signal.throwIfAborted();
50
+ const lease = await tryAcquireFileLifetimeOwner(join(root, 'owner.lease'));
51
+ if (lease)
52
+ return lease;
53
+ await delay(25, undefined, { signal });
54
+ }
55
+ }