@bitkyc08/opencodex 2.56.0 → 2.57.0
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.
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-D4zuyIxQ.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +3 -3
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +25 -7
- package/src/adapters/openai-chat.ts +8 -8
- package/src/adapters/openai-responses/passthrough.ts +32 -1
- package/src/bridge/errors.ts +26 -2
- package/src/bridge/response-json.ts +7 -1
- package/src/bridge/sse.ts +19 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-store.ts +113 -26
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/login-flow.ts +14 -2
- package/src/codex/auth-api/reset-credit-service.ts +11 -2
- package/src/codex/auth-context.ts +157 -7
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/model-visibility.ts +1 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/retained-sync.ts +9 -1
- package/src/codex/catalog/routed-gather.ts +38 -1
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/restore.ts +29 -2
- package/src/codex/inject.ts +9 -9
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +12 -3
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +10 -0
- package/src/codex/routing/selection.ts +79 -2
- package/src/codex/routing/thread-affinity.ts +50 -2
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +29 -49
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/pending-teardown.ts +31 -0
- package/src/generated/compatibility-version.json +163 -135
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +147 -21
- package/src/lib/spend-reservation-ledger.ts +18 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +77 -10
- package/src/lib/windows-elevation.ts +76 -14
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/registry/entries-core.ts +7 -0
- package/src/providers/registry/entries-extended.ts +9 -0
- package/src/providers/registry/model-seeds.ts +4 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/routing/identity-domains.ts +21 -14
- package/src/routing/probe-lease.ts +103 -1
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/live-sideband.ts +37 -1
- package/src/server/index/websocket-handler.ts +6 -2
- package/src/server/index.ts +5 -5
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log.ts +127 -3
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +74 -0
- package/src/server/responses/adapter-continuation.ts +33 -7
- package/src/server/responses/adapter-delivery.ts +5 -11
- package/src/server/responses/adapter-dispatch.ts +84 -13
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +54 -13
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-codex-account.ts +51 -3
- package/src/server/responses/core-combo.ts +103 -23
- package/src/server/responses/core-errors.ts +18 -0
- package/src/server/responses/core-replay.ts +105 -32
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +19 -6
- package/src/server/responses/passthrough-dispatch.ts +28 -10
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/request-prepare.ts +132 -22
- package/src/server/responses/request-send-budget.ts +97 -2
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +62 -3
- package/src/server/responses/run-turn-execution.ts +59 -31
- package/src/server/responses/sidecar-execution.ts +7 -13
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +4 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +1 -1
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -12,14 +12,15 @@ import {
|
|
|
12
12
|
adapterNeedsForcedContinuation,
|
|
13
13
|
adapterResponseReachedServingTerminal,
|
|
14
14
|
} from "./core-replay";
|
|
15
|
-
import { sealRequestAttemptIdentity,
|
|
15
|
+
import { sealRequestAttemptIdentity, recordAttemptCredentialSource } from "../request-log";
|
|
16
16
|
import { waitForProviderRequestSlot, RequestPacingQueueOverloadError } from "../../providers/request-pacing";
|
|
17
17
|
import type { AdapterEventQueue } from "../../adapters/run-turn-queue";
|
|
18
18
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
19
19
|
import { providerFetch } from "./fetch-helpers";
|
|
20
20
|
import { normalizeLogConversationId } from "../request-log-conversation";
|
|
21
21
|
import type { AdapterEvent, OcxProviderContinuationState } from "../../types";
|
|
22
|
-
import { adapterFailureFromMessage } from "../../lib/errors";
|
|
22
|
+
import { adapterFailureFromMessage, SEND_BUDGET_EXHAUSTED_CODE } from "../../lib/errors";
|
|
23
|
+
import { SendBudgetExhaustedError } from "../../lib/upstream-retry";
|
|
23
24
|
import {
|
|
24
25
|
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
25
26
|
isGenericOAuthFailoverEnabled,
|
|
@@ -65,6 +66,8 @@ export async function executeResponsesRunTurn(
|
|
|
65
66
|
| "applyFailoverSnapshot"
|
|
66
67
|
| "resolveSelectionAdapter"
|
|
67
68
|
| "adapter"
|
|
69
|
+
| "noteRoutedAttemptSend"
|
|
70
|
+
| "bindKeyUsageFromBridge"
|
|
68
71
|
>,
|
|
69
72
|
sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction">,
|
|
70
73
|
responseEffects: Pick<
|
|
@@ -74,7 +77,10 @@ export async function executeResponsesRunTurn(
|
|
|
74
77
|
| "continuationStateForResponse"
|
|
75
78
|
| "notifyResponseComplete"
|
|
76
79
|
>,
|
|
77
|
-
sendBudgetState: Pick<
|
|
80
|
+
sendBudgetState: Pick<
|
|
81
|
+
ResponsesSendBudget,
|
|
82
|
+
"adapterDispatchBudget" | "reserveCredentialHop" | "pendingHopPermit"
|
|
83
|
+
>,
|
|
78
84
|
completionPolicy: Pick<ResponsesCompletionPolicy, "emptyCompletionGuardEnabled">,
|
|
79
85
|
): Promise<Response> {
|
|
80
86
|
const { options, logCtx, config } = requestContext;
|
|
@@ -94,7 +100,7 @@ export async function executeResponsesRunTurn(
|
|
|
94
100
|
rememberKiroDeliveredFinalAnswer,
|
|
95
101
|
responseStateOptions,
|
|
96
102
|
} = requestState;
|
|
97
|
-
const {
|
|
103
|
+
const { adapterDispatchBudget, reserveCredentialHop } = sendBudgetState;
|
|
98
104
|
const { emptyCompletionGuardEnabled } = completionPolicy;
|
|
99
105
|
const {
|
|
100
106
|
cancelResponseCompletion,
|
|
@@ -140,7 +146,7 @@ export async function executeResponsesRunTurn(
|
|
|
140
146
|
await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
|
|
141
147
|
}
|
|
142
148
|
await refreshRunTurnSelection();
|
|
143
|
-
|
|
149
|
+
transportState.noteRoutedAttemptSend(logCtx.usageLogInputTokens, recovery);
|
|
144
150
|
const runTurnProviderFetch = providerFetch(
|
|
145
151
|
route.provider,
|
|
146
152
|
options.codexWsRuntimeIdentity,
|
|
@@ -162,7 +168,7 @@ export async function executeResponsesRunTurn(
|
|
|
162
168
|
providerFetch: runTurnProviderFetch,
|
|
163
169
|
// The only way the request budget reaches a transport the adapter owns. Without it
|
|
164
170
|
// a Cursor turn's inner ladder was three physical sends the cap read as one.
|
|
165
|
-
...(
|
|
171
|
+
...(adapterDispatchBudget ? { sendBudget: adapterDispatchBudget } : {}),
|
|
166
172
|
},
|
|
167
173
|
targetQueue.push,
|
|
168
174
|
);
|
|
@@ -175,10 +181,22 @@ export async function executeResponsesRunTurn(
|
|
|
175
181
|
retryable: true,
|
|
176
182
|
message: err.message,
|
|
177
183
|
}
|
|
178
|
-
:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
: err instanceof SendBudgetExhaustedError
|
|
185
|
+
// A structured terminal, not a bare message. The turn is already committed to an
|
|
186
|
+
// SSE response by the time most of these arrive, so the only way to carry "this
|
|
187
|
+
// proxy refused" to the client is on the event itself -- an unstructured message
|
|
188
|
+
// is inferred back to 502, which the Codex client retries.
|
|
189
|
+
? {
|
|
190
|
+
type: "error",
|
|
191
|
+
status: 429,
|
|
192
|
+
errorType: "rate_limit_error",
|
|
193
|
+
code: SEND_BUDGET_EXHAUSTED_CODE,
|
|
194
|
+
message: err.message,
|
|
195
|
+
}
|
|
196
|
+
: {
|
|
197
|
+
type: "error",
|
|
198
|
+
message: err instanceof Error ? err.message : String(err),
|
|
199
|
+
});
|
|
182
200
|
} finally {
|
|
183
201
|
// Cursor assigns a stable conversation id inside runTurn on the first headerless
|
|
184
202
|
// turn; backfill so Logs can filter/total that opening request (#330 / #522).
|
|
@@ -192,6 +210,11 @@ export async function executeResponsesRunTurn(
|
|
|
192
210
|
const rotateRunTurnAdapterOnPreflight429 = async (
|
|
193
211
|
error: Extract<AdapterEvent, { type: "error" }>,
|
|
194
212
|
): Promise<boolean> => {
|
|
213
|
+
// Our own refusal wears a 429 now, and rotating on it would record a cooldown against an
|
|
214
|
+
// account that never rate-limited anything -- a fake quota signal that outlives the
|
|
215
|
+
// request and misroutes later ones. The passthrough path has never had this problem
|
|
216
|
+
// because it answers before any rotation arm is reached.
|
|
217
|
+
if (error.code === SEND_BUDGET_EXHAUSTED_CODE) return false;
|
|
195
218
|
const status = error.status ?? adapterFailureFromMessage(error.message).httpStatus;
|
|
196
219
|
if (
|
|
197
220
|
status !== 429
|
|
@@ -258,8 +281,11 @@ export async function executeResponsesRunTurn(
|
|
|
258
281
|
});
|
|
259
282
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, rotatedAdapter.name, logCtx.accountLogLabel);
|
|
260
283
|
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, rotatedAdapter.name);
|
|
261
|
-
// The caller replays the turn on this rotation,
|
|
262
|
-
|
|
284
|
+
// The caller replays the turn on this rotation, and a runTurn adapter dispatches through
|
|
285
|
+
// its own reservation ladder -- Cursor reserves once per physical send. Confirming here
|
|
286
|
+
// would leave that ladder to charge the same replay a second time (#4709), so hand the
|
|
287
|
+
// reservation down and let the send that actually happens spend it.
|
|
288
|
+
sendBudgetState.pendingHopPermit = hop.permit;
|
|
263
289
|
return true;
|
|
264
290
|
} catch {
|
|
265
291
|
hop.permit?.release();
|
|
@@ -270,16 +296,24 @@ export async function executeResponsesRunTurn(
|
|
|
270
296
|
firstSource: AsyncIterable<AdapterEvent>,
|
|
271
297
|
): Promise<AsyncIterable<AdapterEvent>> => {
|
|
272
298
|
let source = firstSource;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
299
|
+
try {
|
|
300
|
+
while (true) {
|
|
301
|
+
const preflight = await preflightAdapterEvents(source);
|
|
302
|
+
if (!preflight.error || !(await rotateRunTurnAdapterOnPreflight429(preflight.error))) {
|
|
303
|
+
return preflight.stream;
|
|
304
|
+
}
|
|
305
|
+
const retryQueue = createAdapterEventQueue({
|
|
306
|
+
onBacklogExceeded: () => runTurnAbort.abort(),
|
|
307
|
+
});
|
|
308
|
+
void runTurnAttempt(retryQueue, "oauth-account-429");
|
|
309
|
+
source = retryQueue.stream();
|
|
277
310
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
311
|
+
} finally {
|
|
312
|
+
// A handed-down hop reservation belongs to the replay this loop dispatched, and the
|
|
313
|
+
// loop only leaves after that replay's first event has arrived -- so the adapter has
|
|
314
|
+
// already reserved if it was ever going to. Dropping the reference here keeps an
|
|
315
|
+
// adapter that reserves nothing from leaving a free send for an unrelated later leg.
|
|
316
|
+
sendBudgetState.pendingHopPermit = undefined;
|
|
283
317
|
}
|
|
284
318
|
};
|
|
285
319
|
// The empty-completion retry re-runs the turn against a fresh queue: the
|
|
@@ -340,6 +374,7 @@ export async function executeResponsesRunTurn(
|
|
|
340
374
|
stallTimeoutSec: config.stallTimeoutSec,
|
|
341
375
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
342
376
|
declaredToolNames,
|
|
377
|
+
enforceDeclaredToolNames: inboundWire !== "chat" && inboundWire !== "anthropic",
|
|
343
378
|
toolParameterSchemas,
|
|
344
379
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
345
380
|
...(routedCompaction ? { compaction: true } : {}),
|
|
@@ -349,11 +384,7 @@ export async function executeResponsesRunTurn(
|
|
|
349
384
|
onUsage: usage => {
|
|
350
385
|
// Raw adapter usage, pre wire-normalization: the bridged SSE now always carries
|
|
351
386
|
// zero-default detail objects, so provenance must come from here (cache_detail_missing).
|
|
352
|
-
|
|
353
|
-
if (usage) {
|
|
354
|
-
logCtx.usage = usage;
|
|
355
|
-
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
356
|
-
}
|
|
387
|
+
transportState.bindKeyUsageFromBridge(usage);
|
|
357
388
|
},
|
|
358
389
|
onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
|
|
359
390
|
commitReasoningReplayServingRoute();
|
|
@@ -414,17 +445,14 @@ export async function executeResponsesRunTurn(
|
|
|
414
445
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
415
446
|
toolNsMap,
|
|
416
447
|
declaredToolNames,
|
|
448
|
+
enforceDeclaredToolNames: inboundWire !== "chat" && inboundWire !== "anthropic",
|
|
417
449
|
toolParameterSchemas,
|
|
418
450
|
freeformToolNames,
|
|
419
451
|
toolSearchToolNames,
|
|
420
452
|
...(routedCompaction ? { compaction: true } : {}),
|
|
421
453
|
onProviderState: state => { providerState = state; },
|
|
422
454
|
onUsage: usage => {
|
|
423
|
-
|
|
424
|
-
if (usage) {
|
|
425
|
-
logCtx.usage = usage;
|
|
426
|
-
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
427
|
-
}
|
|
455
|
+
transportState.bindKeyUsageFromBridge(usage);
|
|
428
456
|
},
|
|
429
457
|
});
|
|
430
458
|
if (!routedCompaction) {
|
|
@@ -35,7 +35,7 @@ import { bindRouteReasoningReplayScope, adapterNeedsForcedContinuation } from ".
|
|
|
35
35
|
import { namespacedToolName } from "../../types";
|
|
36
36
|
import { providerFetch } from "./fetch-helpers";
|
|
37
37
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
38
|
-
import {
|
|
38
|
+
import { recordAdapterReasoning, recordAdapterTier } from "../request-log";
|
|
39
39
|
import { normalizeLogConversationId } from "../request-log-conversation";
|
|
40
40
|
import { rememberResponseState } from "../../responses/state";
|
|
41
41
|
import { trackStreamLifetime } from "../lifecycle";
|
|
@@ -65,6 +65,8 @@ export async function executeResponsesSidecars(
|
|
|
65
65
|
| "commitResolvedOAuthSelection"
|
|
66
66
|
| "resolveSelectionAdapter"
|
|
67
67
|
| "oauthDispatch"
|
|
68
|
+
| "noteRoutedAttemptSend"
|
|
69
|
+
| "bindKeyUsageFromBridge"
|
|
68
70
|
>,
|
|
69
71
|
sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction" | "openAiSidecar">,
|
|
70
72
|
responseEffects: Pick<
|
|
@@ -322,7 +324,7 @@ export async function executeResponsesSidecars(
|
|
|
322
324
|
...(vidPlan ? { videoPlan: vidPlan } : {}),
|
|
323
325
|
forwardHeaders: requestState.selectedForwardHeaders,
|
|
324
326
|
onAttemptSend: (recovery?: AttemptRecoveryKind) =>
|
|
325
|
-
|
|
327
|
+
transportState.noteRoutedAttemptSend(logCtx.usageLogInputTokens, recovery),
|
|
326
328
|
abortSignal: options.abortSignal,
|
|
327
329
|
maxRounds: imgPlan && vidPlan
|
|
328
330
|
? clampImageMaxRounds(Math.min(config.images?.maxRounds ?? 3, config.images?.videoMaxRounds ?? 2))
|
|
@@ -352,11 +354,7 @@ export async function executeResponsesSidecars(
|
|
|
352
354
|
if (!logCtx.conversationId && parsed._cursorConversationId) {
|
|
353
355
|
logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
|
|
354
356
|
}
|
|
355
|
-
|
|
356
|
-
if (usage) {
|
|
357
|
-
logCtx.usage = usage;
|
|
358
|
-
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
359
|
-
}
|
|
357
|
+
transportState.bindKeyUsageFromBridge(usage);
|
|
360
358
|
},
|
|
361
359
|
on429: rotateSidecarProviderOn429,
|
|
362
360
|
retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
|
|
@@ -432,13 +430,9 @@ export async function executeResponsesSidecars(
|
|
|
432
430
|
recordAdapterTier(logCtx, request);
|
|
433
431
|
},
|
|
434
432
|
onAttemptSend: (recovery?: AttemptRecoveryKind) =>
|
|
435
|
-
|
|
433
|
+
transportState.noteRoutedAttemptSend(logCtx.usageLogInputTokens, recovery),
|
|
436
434
|
onUsage: usage => {
|
|
437
|
-
|
|
438
|
-
if (usage) {
|
|
439
|
-
logCtx.usage = usage;
|
|
440
|
-
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
441
|
-
}
|
|
435
|
+
transportState.bindKeyUsageFromBridge(usage);
|
|
442
436
|
},
|
|
443
437
|
recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
|
|
444
438
|
connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
|
|
@@ -13,6 +13,9 @@ const PLAN_OR_COMPLETION_RE = /(?:\b(?:i(?:'|’)m going to|i will|i(?:'|’)ll|
|
|
|
13
13
|
const WAITING_FOR_USER_RE = /(?:[??]\s*$|需要我|请(?:确认|选择|提供)|是否|要不要|可以吗|\b(?:do you want|should i|which file|please confirm|please provide)\b)/iu;
|
|
14
14
|
const EXPLICIT_CONTINUE_RE = /^(?:继续|接着|往下|go on|continue|proceed|keep going)\s*[.!。!]?$/iu;
|
|
15
15
|
const MAX_ANNOUNCEMENT_CHARS = 280;
|
|
16
|
+
const MAX_RETAINED_EVENTS = 1_024;
|
|
17
|
+
// JavaScript string code units, not UTF-8 bytes or a process-wide memory limit.
|
|
18
|
+
const MAX_RETAINED_CONTENT_CHARS = 64 * 1_024;
|
|
16
19
|
|
|
17
20
|
export const TERMINAL_GUARD_NUDGE =
|
|
18
21
|
"你刚才只描述了计划,没有执行任何工具。不要再次解释计划,现在立即调用必要工具执行用户任务。" +
|
|
@@ -195,7 +198,17 @@ function mergeUsage(first: OcxUsage | undefined, second: OcxUsage | undefined):
|
|
|
195
198
|
};
|
|
196
199
|
}
|
|
197
200
|
|
|
198
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Forward adapter events and re-ask only short, suspicious no-tool completions.
|
|
203
|
+
* Retention is bounded per turn; tools or overflow disable analysis without truncating output.
|
|
204
|
+
* Reported usage from completed legs survives a continuation-factory failure. Unreported
|
|
205
|
+
* usage stays absent, and source-iteration failures propagate to the caller's transport handler.
|
|
206
|
+
*
|
|
207
|
+
* @param options Initial stream, parsed history, and continuation callback. The caller owns
|
|
208
|
+
* provider opt-in; the continuation limit defaults to one and is clamped to at most two.
|
|
209
|
+
* @yields Unchanged content events, internal assistant boundaries, and terminal events with
|
|
210
|
+
* accumulated reported usage when available. Returning the iterator closes its active source.
|
|
211
|
+
*/
|
|
199
212
|
export async function* guardTerminalEventStream(options: GuardedEventStreamOptions): AsyncGenerator<AdapterEvent> {
|
|
200
213
|
const maxContinuations = Math.max(0, Math.min(2, Math.floor(options.maxAutoContinuations ?? 1)));
|
|
201
214
|
let parsed = options.parsed;
|
|
@@ -205,6 +218,10 @@ export async function* guardTerminalEventStream(options: GuardedEventStreamOptio
|
|
|
205
218
|
|
|
206
219
|
while (true) {
|
|
207
220
|
const seen: AdapterEvent[] = [];
|
|
221
|
+
let retainedContentChars = 0;
|
|
222
|
+
let retainedText = "";
|
|
223
|
+
let analysisEnabled = (options.adapterName === "anthropic" || options.adapterName === "openai-chat")
|
|
224
|
+
&& continuations < maxContinuations;
|
|
208
225
|
let terminalSeen = false;
|
|
209
226
|
for await (const event of source) {
|
|
210
227
|
// Liveness markers and tool argument fragments are passed through to the bridge, but
|
|
@@ -216,7 +233,7 @@ export async function* guardTerminalEventStream(options: GuardedEventStreamOptio
|
|
|
216
233
|
}
|
|
217
234
|
if (event.type === "done") {
|
|
218
235
|
terminalSeen = true;
|
|
219
|
-
const analysis =
|
|
236
|
+
const analysis = analysisEnabled
|
|
220
237
|
? analyzeTerminalTurn(parsed, seen)
|
|
221
238
|
: { decision: "pass" as const };
|
|
222
239
|
const normalStop = event.stopReason !== "max_tokens" && event.stopReason !== "content_filter";
|
|
@@ -224,11 +241,17 @@ export async function* guardTerminalEventStream(options: GuardedEventStreamOptio
|
|
|
224
241
|
accumulatedUsage = mergeUsage(accumulatedUsage, event.usage);
|
|
225
242
|
continuations += 1;
|
|
226
243
|
parsed = buildContinuationRequest(parsed, seen);
|
|
244
|
+
seen.length = 0;
|
|
245
|
+
retainedText = "";
|
|
227
246
|
yield { type: "assistant_boundary" };
|
|
228
247
|
try {
|
|
229
248
|
source = await options.continuation(parsed);
|
|
230
249
|
} catch (error) {
|
|
231
|
-
yield {
|
|
250
|
+
yield {
|
|
251
|
+
type: "error",
|
|
252
|
+
message: error instanceof Error ? error.message : String(error),
|
|
253
|
+
...(accumulatedUsage ? { usage: accumulatedUsage } : {}),
|
|
254
|
+
};
|
|
232
255
|
return;
|
|
233
256
|
}
|
|
234
257
|
break;
|
|
@@ -243,7 +266,45 @@ export async function* guardTerminalEventStream(options: GuardedEventStreamOptio
|
|
|
243
266
|
yield usage ? { ...event, usage } : event;
|
|
244
267
|
return;
|
|
245
268
|
}
|
|
246
|
-
|
|
269
|
+
if (analysisEnabled) {
|
|
270
|
+
if (event.type === "tool_call_start") {
|
|
271
|
+
// A real tool call permanently rules out a no-tool continuation for this turn.
|
|
272
|
+
analysisEnabled = false;
|
|
273
|
+
} else if (
|
|
274
|
+
event.type === "text_delta"
|
|
275
|
+
|| event.type === "thinking_delta"
|
|
276
|
+
|| event.type === "thinking_signature"
|
|
277
|
+
|| event.type === "redacted_thinking"
|
|
278
|
+
) {
|
|
279
|
+
const content = event.type === "text_delta"
|
|
280
|
+
? event.text
|
|
281
|
+
: event.type === "thinking_delta"
|
|
282
|
+
? event.thinking
|
|
283
|
+
: event.type === "thinking_signature"
|
|
284
|
+
? event.signature
|
|
285
|
+
: event.data;
|
|
286
|
+
if (
|
|
287
|
+
seen.length >= MAX_RETAINED_EVENTS
|
|
288
|
+
|| content.length > MAX_RETAINED_CONTENT_CHARS - retainedContentChars
|
|
289
|
+
) {
|
|
290
|
+
analysisEnabled = false;
|
|
291
|
+
} else {
|
|
292
|
+
retainedContentChars += content.length;
|
|
293
|
+
if (event.type === "text_delta") retainedText += content;
|
|
294
|
+
// Match analyzeTerminalTurn's trimmed-text semantics, including split padding.
|
|
295
|
+
if (retainedText.trim().length > MAX_ANNOUNCEMENT_CHARS) {
|
|
296
|
+
analysisEnabled = false;
|
|
297
|
+
} else {
|
|
298
|
+
seen.push(event);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!analysisEnabled) {
|
|
303
|
+
// Never rebuild a continuation from truncated thinking or a partial turn.
|
|
304
|
+
seen.length = 0;
|
|
305
|
+
retainedText = "";
|
|
306
|
+
}
|
|
307
|
+
}
|
|
247
308
|
yield event;
|
|
248
309
|
}
|
|
249
310
|
if (!terminalSeen) return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { collectAmbiguousDottedAliases, dottedAliasIsUnambiguous, wireToolInnerName } from "../responses/tool-name-aliases";
|
|
2
2
|
import {
|
|
3
|
-
CODE_MODE_EXEC_TOOL_NAME,
|
|
4
3
|
dottedToolName,
|
|
4
|
+
NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES,
|
|
5
5
|
namespacedToolName,
|
|
6
6
|
normalizeDeclaredToolName,
|
|
7
7
|
} from "../types";
|
|
@@ -104,10 +104,14 @@ function addWireToolName(
|
|
|
104
104
|
if (dottedAliasIsUnambiguous(namespace, name) && !ambiguousDottedAliases?.has(dotted)) {
|
|
105
105
|
names.add(dotted);
|
|
106
106
|
}
|
|
107
|
-
//
|
|
108
|
-
// for a
|
|
109
|
-
// `
|
|
110
|
-
|
|
107
|
+
// The code-mode helper spellings do not get a bare alias for a namespaced tool. Bare `exec`
|
|
108
|
+
// switches nested-helper normalization on for a catalog that never declared the shell; bare
|
|
109
|
+
// `exec_command`/`shell_command` switch it off for one that did; bare `write_stdin`/
|
|
110
|
+
// `apply_patch`/`view_image` are simply accepted as declared under a name the caller only ever
|
|
111
|
+
// authorized inside a namespace. This guard named only `exec` and let the other five through,
|
|
112
|
+
// which is the same drift the bridge-side copy had; both now read one list
|
|
113
|
+
// (src/types/tools.ts). Every other inner name keeps the bare alias.
|
|
114
|
+
if (!NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES.has(name)) names.add(name);
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { chmodSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { win32 } from "node:path";
|
|
3
4
|
import { winswXmlPath } from "../lib/winsw";
|
|
4
5
|
import { hardenSecretPath } from "../lib/windows-secret-acl";
|
|
@@ -9,7 +10,7 @@ import { existsSync, mkdirSync, mkdtempSync, rmdirSync, unlinkSync } from "node:
|
|
|
9
10
|
import { tmpdir } from "node:os";
|
|
10
11
|
import { dirname, join } from "node:path";
|
|
11
12
|
import { getConfigDir } from "../config";
|
|
12
|
-
import { runWindowsElevatedScheduledTaskRegistration, WindowsSchtasksError } from "../lib/windows-elevation";
|
|
13
|
+
import { OCX_ELEVATED_STAGING_UNREADABLE, runWindowsElevatedScheduledTaskRegistration, WindowsSchtasksError, type StagedWindowsTaskXml } from "../lib/windows-elevation";
|
|
13
14
|
import { defaultWinswEntry, installWinswService, statusWinswRaw, uninstallWinswService, WINSW_SERVICE_ID, type WinswStatus } from "../lib/winsw";
|
|
14
15
|
import { forgetEphemeralSecretDir, forgetEphemeralSecretPath, hardenSecretDir } from "../lib/windows-secret-acl";
|
|
15
16
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
@@ -165,6 +166,197 @@ function cleanupWindowsSchedulerStage(
|
|
|
165
166
|
if (cleanupError) throw cleanupError;
|
|
166
167
|
}
|
|
167
168
|
|
|
169
|
+
/** A staged payload set for one elevated registration, plus the way to remove it. */
|
|
170
|
+
export interface StagedElevatedSchedulerRegistration {
|
|
171
|
+
readonly xml: StagedWindowsTaskXml;
|
|
172
|
+
readonly expectedExisting?: StagedWindowsTaskXml;
|
|
173
|
+
/** Remove every staged artifact. Idempotent, so a second call after success is a no-op. */
|
|
174
|
+
cleanup(): void;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface ElevatedSchedulerStagingDeps {
|
|
178
|
+
createStageDir?: () => string;
|
|
179
|
+
hardenDir?: (path: string) => void;
|
|
180
|
+
writePayload?: (path: string, bytes: Buffer) => void;
|
|
181
|
+
hardenPath?: (path: string) => void;
|
|
182
|
+
inspect?: (path: string) => { isSymbolicLink(): boolean; isFile(): boolean; isDirectory(): boolean };
|
|
183
|
+
removeStageDir?: (path: string) => void;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Stage the captured definitions an elevated registration needs, as files rather than
|
|
188
|
+
* as command-line payloads (#4692).
|
|
189
|
+
*
|
|
190
|
+
* A file that an administrator process will read is itself a privilege-escalation
|
|
191
|
+
* surface, so three properties have to hold together and none of them is sufficient
|
|
192
|
+
* alone:
|
|
193
|
+
*
|
|
194
|
+
* - **Access.** The directory is created fresh by `mkdtemp`, then ACL-hardened before
|
|
195
|
+
* anything is written into it, so another local account cannot read or replace the
|
|
196
|
+
* payload while the UAC prompt is open. Hardening the directory first is what makes
|
|
197
|
+
* the file private from the moment it exists.
|
|
198
|
+
* - **No reparse point.** Each artifact is inspected with `lstat` and rejected unless it
|
|
199
|
+
* is what it claims to be. `wx` already refuses to create over an existing name, which
|
|
200
|
+
* is the atomic step here — there is no replace path to race, because every path is
|
|
201
|
+
* inside a directory that did not exist a moment ago. The explicit check is what keeps
|
|
202
|
+
* that guarantee from depending on a reading of `O_EXCL` semantics.
|
|
203
|
+
* - **Tamper evidence.** The digest is taken over the exact bytes written, and the
|
|
204
|
+
* elevated script recomputes it over the bytes it reads. An ACL cannot cover this:
|
|
205
|
+
* a process running as the same user has the same SID and can rewrite the file, so
|
|
206
|
+
* the digest is the only thing that makes such a swap fail closed rather than
|
|
207
|
+
* silently register a different task definition.
|
|
208
|
+
*
|
|
209
|
+
* Payloads are UTF-16LE with no BOM, and the elevated process decodes them straight into
|
|
210
|
+
* `Register-ScheduledTask`. What is hashed is therefore exactly what is registered, with
|
|
211
|
+
* no trimming step in between that the two sides could disagree about.
|
|
212
|
+
*/
|
|
213
|
+
export function stageElevatedSchedulerRegistration(
|
|
214
|
+
xml: string,
|
|
215
|
+
expectedExistingXml?: string,
|
|
216
|
+
deps: ElevatedSchedulerStagingDeps = {},
|
|
217
|
+
): StagedElevatedSchedulerRegistration {
|
|
218
|
+
const createStageDir = deps.createStageDir
|
|
219
|
+
?? (() => mkdtempSync(join(tmpdir(), WINDOWS_SCHEDULER_STAGE_PREFIX)));
|
|
220
|
+
const hardenDir = deps.hardenDir ?? ((path: string) => { hardenSecretDir(path, { required: true }); });
|
|
221
|
+
const writePayload = deps.writePayload ?? ((path: string, bytes: Buffer) => {
|
|
222
|
+
writeFileSync(path, bytes, { flag: "wx", mode: 0o600 });
|
|
223
|
+
});
|
|
224
|
+
const hardenPath = deps.hardenPath ?? ((path: string) => { hardenSecretPath(path, { required: true }); });
|
|
225
|
+
const inspect = deps.inspect ?? ((path: string) => lstatSync(path));
|
|
226
|
+
const removeStageDir = deps.removeStageDir ?? ((path: string) => { rmdirSync(path); });
|
|
227
|
+
|
|
228
|
+
const stageDir = createStageDir();
|
|
229
|
+
const files: string[] = [];
|
|
230
|
+
const cleanup = (): void => {
|
|
231
|
+
let failure: unknown;
|
|
232
|
+
for (const file of files.splice(0)) {
|
|
233
|
+
try {
|
|
234
|
+
unlinkSync(file);
|
|
235
|
+
forgetEphemeralSecretPath(file);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") forgetEphemeralSecretPath(file);
|
|
238
|
+
else if (failure === undefined) failure = error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
removeStageDir(stageDir);
|
|
243
|
+
forgetEphemeralSecretDir(stageDir);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") forgetEphemeralSecretDir(stageDir);
|
|
246
|
+
else if (failure) throw new AggregateError([failure, error], "Elevated Task Scheduler staging cleanup failed.");
|
|
247
|
+
else failure = error;
|
|
248
|
+
}
|
|
249
|
+
if (failure) throw failure;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
try { chmodSync(stageDir, 0o700); } catch { /* required Windows ACL is authoritative */ }
|
|
254
|
+
const dirStats = inspect(stageDir);
|
|
255
|
+
if (dirStats.isSymbolicLink() || !dirStats.isDirectory()) {
|
|
256
|
+
throw new Error(`Refusing to stage an elevated Task Scheduler payload under a redirected path: ${stageDir}`);
|
|
257
|
+
}
|
|
258
|
+
hardenDir(stageDir);
|
|
259
|
+
const stage = (name: string, value: string): StagedWindowsTaskXml => {
|
|
260
|
+
const path = join(stageDir, name);
|
|
261
|
+
const bytes = Buffer.from(value, "utf16le");
|
|
262
|
+
writePayload(path, bytes);
|
|
263
|
+
files.push(path);
|
|
264
|
+
const stats = inspect(path);
|
|
265
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
266
|
+
throw new Error(`Refusing to stage an elevated Task Scheduler payload through a redirected path: ${path}`);
|
|
267
|
+
}
|
|
268
|
+
hardenPath(path);
|
|
269
|
+
return { path, sha256: createHash("sha256").update(bytes).digest("hex") };
|
|
270
|
+
};
|
|
271
|
+
return {
|
|
272
|
+
xml: stage("register.xml", xml),
|
|
273
|
+
...(expectedExistingXml === undefined
|
|
274
|
+
? {}
|
|
275
|
+
: { expectedExisting: stage("expected.xml", expectedExistingXml) }),
|
|
276
|
+
cleanup,
|
|
277
|
+
};
|
|
278
|
+
} catch (error) {
|
|
279
|
+
try {
|
|
280
|
+
cleanup();
|
|
281
|
+
} catch (cleanupError) {
|
|
282
|
+
throw new AggregateError(
|
|
283
|
+
[error, cleanupError],
|
|
284
|
+
"Elevated Task Scheduler staging failed and could not be cleaned up.",
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Turn an elevated registration exit code into something an operator can act on.
|
|
293
|
+
*
|
|
294
|
+
* The elevated process runs hidden, so nothing it writes survives; only the exit code
|
|
295
|
+
* crosses back. That makes an unexplained code the whole user-facing error, which is
|
|
296
|
+
* exactly what made the ENAMETOOLONG in #4692 expensive to diagnose. Staging introduces
|
|
297
|
+
* one new failure of its own — the payload is readable only by the account that created
|
|
298
|
+
* it, so an elevation answered with a different administrator's credentials cannot open
|
|
299
|
+
* it — and that one gets named along with its remedy rather than surfacing as a number.
|
|
300
|
+
*/
|
|
301
|
+
export function describeElevatedRegistrationFailure(
|
|
302
|
+
failureLabel: string,
|
|
303
|
+
exitCode: number,
|
|
304
|
+
stageDir: string,
|
|
305
|
+
): string {
|
|
306
|
+
if (exitCode === OCX_ELEVATED_STAGING_UNREADABLE) {
|
|
307
|
+
return `${failureLabel}: the elevated process could not read the staged task definition in `
|
|
308
|
+
+ `${stageDir}. That directory is readable only by the account that staged it, so this `
|
|
309
|
+
+ "happens when the UAC prompt was answered with a different administrator account. "
|
|
310
|
+
+ "Approve the prompt as the signed-in user, or run the command again from a session "
|
|
311
|
+
+ "already elevated as that user.";
|
|
312
|
+
}
|
|
313
|
+
return `${failureLabel} with exit code ${exitCode}.`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Stage, elevate, and clean up — on every exit, including UAC cancellation and a
|
|
318
|
+
* synchronous spawn failure.
|
|
319
|
+
*
|
|
320
|
+
* A cleanup failure never replaces the registration failure it followed: an operator
|
|
321
|
+
* told only that a temp directory could not be removed would have no idea the task was
|
|
322
|
+
* never registered.
|
|
323
|
+
*/
|
|
324
|
+
async function runStagedElevatedSchedulerRegistration(
|
|
325
|
+
taskName: string,
|
|
326
|
+
xml: string,
|
|
327
|
+
replace: boolean,
|
|
328
|
+
expectedExistingXml: string | undefined,
|
|
329
|
+
failureLabel: string,
|
|
330
|
+
): Promise<void> {
|
|
331
|
+
const staged = stageElevatedSchedulerRegistration(xml, expectedExistingXml);
|
|
332
|
+
let failure: unknown;
|
|
333
|
+
try {
|
|
334
|
+
const exitCode = await runWindowsElevatedScheduledTaskRegistration(
|
|
335
|
+
taskName,
|
|
336
|
+
staged.xml,
|
|
337
|
+
replace,
|
|
338
|
+
staged.expectedExisting,
|
|
339
|
+
);
|
|
340
|
+
if (exitCode !== 0) {
|
|
341
|
+
failure = new Error(describeElevatedRegistrationFailure(failureLabel, exitCode, dirname(staged.xml.path)));
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
failure = error;
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
staged.cleanup();
|
|
348
|
+
} catch (cleanupError) {
|
|
349
|
+
if (failure) {
|
|
350
|
+
throw new AggregateError(
|
|
351
|
+
[failure, cleanupError],
|
|
352
|
+
"Elevated Task Scheduler registration failed and its staging could not be cleaned up.",
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
throw cleanupError;
|
|
356
|
+
}
|
|
357
|
+
if (failure) throw failure;
|
|
358
|
+
}
|
|
359
|
+
|
|
168
360
|
export function stageWindowsSchedulerRegistrationXml(
|
|
169
361
|
attemptNonce: string,
|
|
170
362
|
deps: WindowsSchedulerRegistrationStageDeps = {},
|
|
@@ -294,7 +486,8 @@ export async function registerFreshWindowsSchedulerTask(
|
|
|
294
486
|
throw error;
|
|
295
487
|
}
|
|
296
488
|
// Register from the captured XML string inside the elevated process. Another
|
|
297
|
-
// same-user process can mutate its own temp files,
|
|
489
|
+
// same-user process can mutate its own temp files, so the captured bytes are staged
|
|
490
|
+
// privately and the elevated script verifies their digest before registering them.
|
|
298
491
|
// UAC can remain open for an arbitrary amount of time. Recheck the captured predecessor
|
|
299
492
|
// before launch; the elevated helper repeats the same check after consent and before Force.
|
|
300
493
|
assertReplacementPrecondition();
|
|
@@ -303,15 +496,13 @@ export async function registerFreshWindowsSchedulerTask(
|
|
|
303
496
|
xml: string,
|
|
304
497
|
replaceCurrent: boolean,
|
|
305
498
|
previousXml?: string,
|
|
306
|
-
) =>
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (exitCode !== 0) throw new Error(`Background service install failed with exit code ${exitCode}.`);
|
|
314
|
-
});
|
|
499
|
+
) => runStagedElevatedSchedulerRegistration(
|
|
500
|
+
taskName,
|
|
501
|
+
xml,
|
|
502
|
+
replaceCurrent,
|
|
503
|
+
previousXml,
|
|
504
|
+
"Background service install failed",
|
|
505
|
+
));
|
|
315
506
|
await elevate(TASK, expectedXml, replace, expectedExistingXml);
|
|
316
507
|
}
|
|
317
508
|
|
|
@@ -501,10 +692,13 @@ export async function restoreWindowsSchedulerTaskIfAbsent(registeredXml: string)
|
|
|
501
692
|
) {
|
|
502
693
|
throw error;
|
|
503
694
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
695
|
+
await runStagedElevatedSchedulerRegistration(
|
|
696
|
+
TASK,
|
|
697
|
+
registeredXml,
|
|
698
|
+
false,
|
|
699
|
+
undefined,
|
|
700
|
+
"Task Scheduler rollback failed",
|
|
701
|
+
);
|
|
508
702
|
}
|
|
509
703
|
const recoveredXml = statusWindowsXml();
|
|
510
704
|
if (!windowsSchedulerRegistrationMatchesSnapshot(recoveredXml, registeredXml)) {
|