@moretea-labs/forge 1.7.0 → 1.7.1
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/AGENTS.md +2 -0
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +2 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/agents/openai.yaml +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +2 -0
- package/assets/plugin-registry.v1.json +3 -3
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +17 -0
- package/assets/templates/helpers/inspect-project-state.ts +8 -6
- package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
- package/assets/templates/helpers/workflow-contract.ts +5 -1
- package/docs/architecture/CURRENT.md +12 -4
- package/docs/forge-plugin-management.md +1 -1
- package/docs/operations/controller-performance-and-502.md +19 -0
- package/docs/operations/homebrew.md +2 -2
- package/package.json +5 -2
- package/scripts/architecture-event.ts +2 -820
- package/scripts/architecture-queue.sh +2 -645
- package/scripts/archive-architecture-request.sh +2 -242
- package/scripts/archive-workflow.sh +1 -291
- package/scripts/benchmark-work-efficiency.ts +0 -0
- package/scripts/capability-config.ts +2 -392
- package/scripts/capability-resolver.ts +2 -503
- package/scripts/capture-plan.sh +1 -332
- package/scripts/check-agent-tooling.sh +2 -1531
- package/scripts/check-architecture-sync.sh +2 -302
- package/scripts/check-brain-manifest.sh +2 -253
- package/scripts/check-context-files.sh +2 -47
- package/scripts/check-deploy-sql-order.sh +2 -115
- package/scripts/check-runtime-architecture.mjs +243 -16
- package/scripts/check-skill-version.ts +3 -191
- package/scripts/check-source-duplication.mjs +61 -0
- package/scripts/check-stable-baseline.ts +0 -0
- package/scripts/check-task-sync.sh +2 -63
- package/scripts/check-task-workflow.sh +2 -1000
- package/scripts/codex-handoff-resume.sh +2 -267
- package/scripts/context-contract-sync.sh +2 -514
- package/scripts/contract-run.ts +2 -463
- package/scripts/contract-worktree.sh +1 -702
- package/scripts/ensure-task-workflow.sh +1 -1192
- package/scripts/inspect-project-state.ts +4 -279
- package/scripts/lib/project-init-lib.sh +7 -2
- package/scripts/lib/run-forge-helper.sh +19 -0
- package/scripts/lib/run-forge-helper.ts +26 -0
- package/scripts/migrate-project-template.sh +1 -33
- package/scripts/migrate-workflow-docs.ts +4 -412
- package/scripts/new-plan.sh +1 -180
- package/scripts/new-spec.sh +1 -47
- package/scripts/new-sprint.sh +1 -46
- package/scripts/plan-to-todo.sh +1 -771
- package/scripts/prepare-codex-handoff.sh +2 -201
- package/scripts/prepare-handoff.sh +1 -78
- package/scripts/refresh-current-status.sh +2 -467
- package/scripts/run-governed-gate.ts +3 -0
- package/scripts/select-agent-context-blocks.sh +2 -64
- package/scripts/ship-worktrees.sh +1 -531
- package/scripts/sprint-backlog.sh +1 -834
- package/scripts/stage-runtime-release.ts +2 -1
- package/scripts/summarize-failures.sh +2 -123
- package/scripts/switch-plan.sh +1 -150
- package/scripts/sync-brain-docs.sh +2 -379
- package/scripts/sync-generated-authority.mjs +175 -0
- package/scripts/verify-contract.sh +2 -593
- package/scripts/verify-sprint.sh +1 -500
- package/scripts/workflow-contract.ts +4 -201
- package/scripts/workstream-sync.sh +2 -261
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/commands/plugin.ts +12 -3
- package/src/cli/controller/check-runner.ts +7 -2
- package/src/cli/controller/context-pack.ts +46 -21
- package/src/cli/github/contracts.ts +16 -0
- package/src/cli/github/github.ts +2 -16
- package/src/cli/github/session.ts +2 -1
- package/src/cli/local-bridge/facade-api.ts +37 -47
- package/src/cli/local-bridge/server.ts +2 -2
- package/src/cli/mcp/legacy-tool-service.ts +10 -33
- package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
- package/src/cli/mcp/repository-tools.ts +85 -89
- package/src/cli/mcp/server.ts +57 -13
- package/src/cli/mcp/tool-contract.ts +14 -0
- package/src/cli/mcp/toolset.ts +2 -2
- package/src/cli/mcp/transports/http.ts +23 -0
- package/src/cli/repositories/command-classifier.ts +2 -2
- package/src/cli/repositories/command-executor.ts +24 -6
- package/src/cli/repositories/repository-snapshot.ts +5 -2
- package/src/cli/repositories/structured-git.ts +57 -3
- package/src/cli/repository/inspector.ts +11 -3
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +61 -0
- package/src/runtime/context/semantic-navigation.ts +9 -61
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +18 -25
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
- package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
- package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
- package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
- package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
- package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
- package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
- package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
- package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
- package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
- package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
- package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
- package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
- package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
- package/src/runtime/control-plane/facade/index.ts +2 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
- package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
- package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
- package/src/runtime/control-plane/facade/types.ts +68 -5
- package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
- package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
- package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
- package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
- package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
- package/src/runtime/control-plane/routing/route-policy.ts +18 -6
- package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
- package/src/runtime/evidence/process-check-execution.ts +18 -0
- package/src/runtime/execution/jobs/child-reference.ts +6 -2
- package/src/runtime/execution/process-runtime/check-result.ts +53 -18
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
- package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
- package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
- package/src/runtime/execution/process-runtime/runtime.ts +10 -0
- package/src/runtime/execution/process-runtime/types.ts +2 -12
- package/src/runtime/execution/work-activity.ts +21 -0
- package/src/runtime/execution/workers/executor.ts +18 -13
- package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
- package/src/runtime/gateway/mcp/router.ts +2 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +805 -1009
- package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
- package/src/runtime/plugins/browser-adapter.ts +256 -1083
- package/src/runtime/plugins/browser-handoff-host.ts +2 -25
- package/src/runtime/plugins/browser-handoff.ts +18 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
- package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
- package/src/runtime/plugins/browser-native-session-service.ts +222 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
- package/src/runtime/plugins/browser-runtime.ts +10 -0
- package/src/runtime/plugins/browser-session-store.ts +121 -0
- package/src/runtime/plugins/browser-session-types.ts +98 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
- package/src/runtime/plugins/external-adapter.ts +92 -58
- package/src/runtime/plugins/macos-capability-broker.ts +0 -8
- package/src/runtime/plugins/store.ts +66 -9
- package/src/runtime/plugins/types.ts +3 -1
- package/src/runtime/recovery/maintenance-executor.ts +61 -25
- package/src/runtime/root/package-connector-service.ts +46 -4
- package/src/runtime/root/package-runtime-release.ts +1 -1
- package/src/runtime/root/package-runtime-service.ts +33 -2
- package/src/runtime/root/release-manifest.ts +137 -16
- package/src/runtime/root/release-materialize.ts +109 -35
- package/src/runtime/root/types.ts +10 -0
- package/src/runtime/standalone-recovery/core.ts +425 -194
- package/src/runtime/standalone-recovery/entry.ts +1 -1
- package/src/runtime/standalone-recovery/installer.ts +1 -1
- package/src/runtime/workflow/schedules/engine.ts +113 -87
- package/src/runtime/workflow/schedules/settlement.ts +26 -23
- package/src/runtime/workflow/schedules/store.ts +79 -10
|
@@ -508,7 +508,7 @@ export async function dispatchRecoveryTool(config: RecoveryConfig, name: string,
|
|
|
508
508
|
}
|
|
509
509
|
case 'restart_primary_connector': {
|
|
510
510
|
if (!requestId(args.request_id)) throw new Error('RECOVERY_REQUEST_ID_REQUIRED');
|
|
511
|
-
return restartPrimaryConnector(config);
|
|
511
|
+
return restartPrimaryConnector(config, { requestId: `recovery-gateway:${args.request_id}` });
|
|
512
512
|
}
|
|
513
513
|
case 'recover_primary_runtime': {
|
|
514
514
|
if (!requestId(args.request_id)) throw new Error('RECOVERY_REQUEST_ID_REQUIRED');
|
|
@@ -340,7 +340,7 @@ function recoveryPlist(input: {
|
|
|
340
340
|
}): string {
|
|
341
341
|
const xml = (value: string) => value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
342
342
|
const argumentsList = [
|
|
343
|
-
'/usr/bin/env', '-i', 'PATH=/usr/bin:/bin:/usr/sbin:/sbin', `FORGE_BUILD_VERSION=${FORGE_VERSION}`,
|
|
343
|
+
'/usr/bin/env', '-i', 'PATH=/usr/bin:/bin:/usr/sbin:/sbin', `FORGE_BUILD_VERSION=${FORGE_VERSION}`, `FORGE_CONNECTOR_EXECUTABLE=${process.execPath}`,
|
|
344
344
|
input.executable, input.command, '--controller-home', input.controllerHome,
|
|
345
345
|
];
|
|
346
346
|
return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>${xml(input.label)}</string><key>ProgramArguments</key><array>${argumentsList.map((argument) => `<string>${xml(argument)}</string>`).join('')}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ThrottleInterval</key><integer>5</integer><key>StandardOutPath</key><string>${xml(input.logPath)}</string><key>StandardErrorPath</key><string>${xml(input.logPath)}</string></dict></plist>\n`;
|
|
@@ -3,6 +3,7 @@ import { execFile } from 'child_process';
|
|
|
3
3
|
import { promisify } from 'util';
|
|
4
4
|
import { getRepository } from '../../../cli/repositories/registry';
|
|
5
5
|
import { findExecutionJob, listActiveExecutionJobs, listExecutionJobs } from '../../execution/jobs/store';
|
|
6
|
+
import { executionJobBelongsToWork, workHasActiveExecution } from '../../execution/work-activity';
|
|
6
7
|
import { readRepositoryProjection } from '../../projections/materialized-view';
|
|
7
8
|
import {
|
|
8
9
|
applyRuntimeMaintenance,
|
|
@@ -26,8 +27,8 @@ import {
|
|
|
26
27
|
recordScheduleOccurrenceHandoff,
|
|
27
28
|
listSchedules,
|
|
28
29
|
saveOccurrence,
|
|
29
|
-
saveSchedule,
|
|
30
30
|
saveScheduleDecision,
|
|
31
|
+
updateSchedule,
|
|
31
32
|
} from './store';
|
|
32
33
|
import { applyScheduleFailure, applyScheduleRetryableFailure } from './settlement';
|
|
33
34
|
import { classifyScheduledBrowserObservation, executeScheduledBrowserProbe } from './browser-probe';
|
|
@@ -44,6 +45,8 @@ const LIVE_MAINTENANCE_OPERATION = 'runtime_maintenance_apply';
|
|
|
44
45
|
const EXTERNAL_CONTROLLER_WAKE_OPERATION = 'external_controller_wake';
|
|
45
46
|
const BROWSER_PROBE_OPERATION = 'browser_probe';
|
|
46
47
|
const GITHUB_ISSUE_WATCH_OPERATION = 'github_issue_watch';
|
|
48
|
+
const DEFAULT_EXTERNAL_CONTROLLER_WAKE_TIMEOUT_MS = 60_000;
|
|
49
|
+
const MAX_EXTERNAL_CONTROLLER_WAKE_TIMEOUT_MS = 120_000;
|
|
47
50
|
|
|
48
51
|
function isDeterministicSchedule(schedule: RepositorySchedule): boolean {
|
|
49
52
|
// Kernel executes only typed, bounded dispatch operations.
|
|
@@ -261,13 +264,17 @@ function workBoundScheduleWorkId(schedule: RepositorySchedule): string | undefin
|
|
|
261
264
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
262
265
|
}
|
|
263
266
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
267
|
+
const HUMAN_ONLY_HANDOFF_REASONS = new Set([
|
|
268
|
+
'policy_approval_required',
|
|
269
|
+
'missing_authorization',
|
|
270
|
+
'invalid_objective',
|
|
271
|
+
'destructive_action_requires_confirmation',
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
function handoffRequiresHumanReview(item: ReturnType<typeof listHandoffItems>[number]): boolean {
|
|
275
|
+
if (item.approvalAction) return true;
|
|
276
|
+
if (!item.creationReason) return true;
|
|
277
|
+
return HUMAN_ONLY_HANDOFF_REASONS.has(item.creationReason);
|
|
271
278
|
}
|
|
272
279
|
|
|
273
280
|
async function stopReason(controllerHome: string, schedule: RepositorySchedule): Promise<string | undefined> {
|
|
@@ -277,8 +284,8 @@ async function stopReason(controllerHome: string, schedule: RepositorySchedule):
|
|
|
277
284
|
if (schedule.stopConditions.includes('human_review_required')) {
|
|
278
285
|
if (workId) {
|
|
279
286
|
const activeHandoff = listHandoffItems({ controllerHome, repoId: schedule.repoId, status: 'active', limit: 100 })
|
|
280
|
-
.find((item) => item.workId === workId);
|
|
281
|
-
if (activeHandoff) return `Work ${workId} has active Handoff ${activeHandoff.id} requiring review.`;
|
|
287
|
+
.find((item) => item.workId === workId && handoffRequiresHumanReview(item));
|
|
288
|
+
if (activeHandoff) return `Work ${workId} has active Handoff ${activeHandoff.id} requiring human review.`;
|
|
282
289
|
} else if (projection.currentAttention.length > 0) {
|
|
283
290
|
return 'Repository has jobs requiring human attention.';
|
|
284
291
|
}
|
|
@@ -367,11 +374,31 @@ function decideOccurrence(
|
|
|
367
374
|
|
|
368
375
|
export type ChatgptWakeFailureClass = 'retryable_readiness' | 'semantic_wait' | 'user_action_required' | 'ordinary_failure';
|
|
369
376
|
|
|
377
|
+
export function externalControllerWakeTimeoutMs(value: unknown): number {
|
|
378
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return DEFAULT_EXTERNAL_CONTROLLER_WAKE_TIMEOUT_MS;
|
|
379
|
+
return Math.max(5_000, Math.min(Math.trunc(value), MAX_EXTERNAL_CONTROLLER_WAKE_TIMEOUT_MS));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export async function awaitExternalControllerWake<T>(operation: Promise<T>, timeoutMs: number): Promise<T> {
|
|
383
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
384
|
+
try {
|
|
385
|
+
return await Promise.race([
|
|
386
|
+
operation,
|
|
387
|
+
new Promise<T>((_, reject) => {
|
|
388
|
+
timer = setTimeout(() => reject(new Error(`CONTROLLER_WAKE_TOTAL_TIMEOUT: external Controller dispatch exceeded ${timeoutMs}ms.`)), timeoutMs);
|
|
389
|
+
}),
|
|
390
|
+
]);
|
|
391
|
+
} finally {
|
|
392
|
+
if (timer) clearTimeout(timer);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
370
396
|
export function classifyChatgptWakeFailure(reason: string): ChatgptWakeFailureClass {
|
|
371
397
|
const normalized = reason.toUpperCase();
|
|
372
398
|
if (
|
|
373
399
|
normalized.includes('CHATGPT_AUTOMATION_LOGIN_REQUIRED')
|
|
374
400
|
|| normalized.includes('PLUGIN_BROWSER_JAVASCRIPT_PERMISSION_REQUIRED')
|
|
401
|
+
|| normalized.includes('PLUGIN_MACOS_CAPABILITY_BROKER_CAPABILITY_UNSUPPORTED')
|
|
375
402
|
|| normalized.includes('AUTHENTICATION_REQUIRED')
|
|
376
403
|
|| normalized.includes('CONSENT_REQUIRED')
|
|
377
404
|
) return 'user_action_required';
|
|
@@ -384,12 +411,15 @@ export function classifyChatgptWakeFailure(reason: string): ChatgptWakeFailureCl
|
|
|
384
411
|
'PLUGIN_BROWSER_ACTIVE_TAB_UNAVAILABLE',
|
|
385
412
|
'PLUGIN_BROWSER_SESSION_STATE_LOST',
|
|
386
413
|
'PLUGIN_SESSION_NOT_FOUND',
|
|
414
|
+
'PLUGIN_BROWSER_NATIVE_TAB_IDENTITY_UNPROVEN',
|
|
415
|
+
'PLUGIN_MACOS_CAPABILITY_BROKER_UNAVAILABLE',
|
|
387
416
|
'PLUGIN_BROWSER_NATIVE_FOREGROUND_ACTIVATOR_UNAVAILABLE',
|
|
388
417
|
'CHATGPT_CONTROLLER_BROWSER_ROOT_UNAVAILABLE',
|
|
389
418
|
'CHATGPT_CONTROLLER_BROWSER_FAILED',
|
|
390
419
|
'CHATGPT_AUTOMATION_COMPOSER_UNAVAILABLE',
|
|
391
420
|
'CHATGPT_AUTOMATION_INTELLIGENCE_CONTROL_UNAVAILABLE',
|
|
392
421
|
'CHATGPT_AUTOMATION_REPLACEMENT_SESSION_NOT_CONFIRMED',
|
|
422
|
+
'CONTROLLER_WAKE_TOTAL_TIMEOUT',
|
|
393
423
|
'HTTP 502',
|
|
394
424
|
'ECONNREFUSED',
|
|
395
425
|
'ECONNRESET',
|
|
@@ -434,24 +464,27 @@ async function executeExternalControllerWake(
|
|
|
434
464
|
const workStore = { controllerHome, repoId: schedule.repoId };
|
|
435
465
|
const work = getWorkContract(workStore, workId);
|
|
436
466
|
if (!work) {
|
|
437
|
-
|
|
467
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ enabled: false, pausedReason: `Work ${workId} no longer exists.`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId }));
|
|
438
468
|
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', `EXTERNAL_CONTROLLER_WAKE_WORK_NOT_FOUND:${workId}`);
|
|
439
469
|
}
|
|
440
470
|
if (isTerminalWorkContractStatus(work.status)) {
|
|
441
|
-
|
|
471
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ enabled: false, pausedReason: `Work ${workId} is terminal (${work.status}).`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId }));
|
|
442
472
|
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} is terminal (${work.status}); automatic continuation stopped.`);
|
|
443
473
|
}
|
|
444
474
|
const existingOwner = getControllerSession(workStore, workId);
|
|
445
475
|
const occurrenceNowMs = Date.parse(timestamp);
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
476
|
+
const nowMs = Number.isFinite(occurrenceNowMs) ? occurrenceNowMs : Date.now();
|
|
477
|
+
if (workHasActiveExecution(controllerHome, schedule.repoId, workId)) {
|
|
478
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId }));
|
|
479
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} has active execution; duplicate Controller wake suppressed.`);
|
|
480
|
+
}
|
|
481
|
+
if (existingOwner && controllerSessionBlocksRecovery(workStore, workId, { nowMs })) {
|
|
482
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId }));
|
|
483
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} has a recently active Controller ${existingOwner.controllerId}.`);
|
|
451
484
|
}
|
|
452
485
|
const launchReservation = getExternalControllerLaunchReservation(workStore, workId);
|
|
453
486
|
if (launchReservation) {
|
|
454
|
-
|
|
487
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId }));
|
|
455
488
|
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} already has a pending external Controller launch ${launchReservation.reservationId}.`);
|
|
456
489
|
}
|
|
457
490
|
const wakeDecision = decideOccurrence(
|
|
@@ -481,6 +514,7 @@ async function executeExternalControllerWake(
|
|
|
481
514
|
if (controllerType === 'chatgpt') {
|
|
482
515
|
const reasoning = args.reasoning === 'medium' || args.reasoning === 'xhigh' ? args.reasoning : 'high';
|
|
483
516
|
const tabPolicy = args.tab_policy === 'reuse' || args.tab_policy === 'new' ? args.tab_policy : 'auto';
|
|
517
|
+
const timeoutMs = externalControllerWakeTimeoutMs(args.timeout_ms);
|
|
484
518
|
let relay;
|
|
485
519
|
try {
|
|
486
520
|
relay = beginInitialControllerRoundDispatch(workStore, {
|
|
@@ -496,8 +530,8 @@ async function executeExternalControllerWake(
|
|
|
496
530
|
conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
|
|
497
531
|
});
|
|
498
532
|
if (relay.status === 'blocked') throw new Error(`CONTROLLER_RELAY_LAUNCH_BLOCKED: ${relay.blockedReason ?? relay.relayScopeId}`);
|
|
499
|
-
const relayPrompt = `${buildControllerRoundRelayPrompt(workStore, relay)}\n\nScheduled continuation hint: ${continuationPrompt}`;
|
|
500
|
-
const dispatched = await runWorkChatgptContinuation({
|
|
533
|
+
const relayPrompt = `${buildControllerRoundRelayPrompt(workStore, relay, { exactOriginWork: true })}\n\nScheduled continuation hint: ${continuationPrompt}`;
|
|
534
|
+
const dispatched = await awaitExternalControllerWake(runWorkChatgptContinuation({
|
|
501
535
|
controllerHome,
|
|
502
536
|
repoId: schedule.repoId,
|
|
503
537
|
repoRoot: repository.canonicalRoot,
|
|
@@ -509,8 +543,8 @@ async function executeExternalControllerWake(
|
|
|
509
543
|
model: typeof args.model === 'string' ? args.model : 'gpt-5.6',
|
|
510
544
|
reasoning,
|
|
511
545
|
tabPolicy,
|
|
512
|
-
timeoutMs
|
|
513
|
-
});
|
|
546
|
+
timeoutMs,
|
|
547
|
+
}), timeoutMs);
|
|
514
548
|
if (dispatched.status === 'failed') throw new Error(dispatched.error?.message ?? 'CHATGPT_WORK_CONTINUATION_FAILED');
|
|
515
549
|
const completedRelay = finishControllerRoundRelayDispatch(workStore, {
|
|
516
550
|
workId,
|
|
@@ -518,15 +552,13 @@ async function executeExternalControllerWake(
|
|
|
518
552
|
browserSessionId: dispatched.browserSessionId,
|
|
519
553
|
conversationUrl: dispatched.conversationUrl,
|
|
520
554
|
});
|
|
521
|
-
|
|
522
|
-
saveSchedule(controllerHome, {
|
|
523
|
-
...latest,
|
|
555
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => ({
|
|
524
556
|
lastTriggeredAt: timestamp,
|
|
525
557
|
lastOccurrenceId: occurrence.occurrenceId,
|
|
526
558
|
consecutiveFailures: 0,
|
|
527
559
|
nextEligibleAt: undefined,
|
|
528
|
-
pausedReason: undefined,
|
|
529
|
-
});
|
|
560
|
+
...(current.enabled ? { pausedReason: undefined } : {}),
|
|
561
|
+
}));
|
|
530
562
|
const succeededOccurrence = saveOccurrence(controllerHome, {
|
|
531
563
|
...wakeDecision,
|
|
532
564
|
status: 'succeeded',
|
|
@@ -548,8 +580,7 @@ async function executeExternalControllerWake(
|
|
|
548
580
|
});
|
|
549
581
|
}
|
|
550
582
|
if (error instanceof Error && error.message.startsWith('CONTROLLER_RELAY_ROUND_ALREADY_OPEN:')) {
|
|
551
|
-
|
|
552
|
-
saveSchedule(controllerHome, { ...latest, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId });
|
|
583
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId }));
|
|
553
584
|
return decideOccurrence(controllerHome, schedule, wakeDecision, 'nothing_to_do', 'skipped', `Work ${workId} already has an open ChatGPT controller round awaiting claim or disposition.`);
|
|
554
585
|
}
|
|
555
586
|
throw error;
|
|
@@ -570,15 +601,13 @@ async function executeExternalControllerWake(
|
|
|
570
601
|
continuationPrompt,
|
|
571
602
|
cwd: repository.canonicalRoot,
|
|
572
603
|
});
|
|
573
|
-
|
|
574
|
-
saveSchedule(controllerHome, {
|
|
575
|
-
...latest,
|
|
604
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => ({
|
|
576
605
|
lastTriggeredAt: timestamp,
|
|
577
606
|
lastOccurrenceId: occurrence.occurrenceId,
|
|
578
607
|
consecutiveFailures: 0,
|
|
579
608
|
nextEligibleAt: undefined,
|
|
580
|
-
pausedReason: undefined,
|
|
581
|
-
});
|
|
609
|
+
...(current.enabled ? { pausedReason: undefined } : {}),
|
|
610
|
+
}));
|
|
582
611
|
return saveOccurrence(controllerHome, {
|
|
583
612
|
...wakeDecision,
|
|
584
613
|
status: 'succeeded',
|
|
@@ -597,12 +626,10 @@ async function executeExternalControllerWake(
|
|
|
597
626
|
: `Controller wake deferred by transient readiness; schedule remains active with bounded backoff: ${reason}`,
|
|
598
627
|
countFailure: !semanticWait,
|
|
599
628
|
});
|
|
600
|
-
|
|
601
|
-
saveSchedule(controllerHome, {
|
|
602
|
-
...latest,
|
|
629
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({
|
|
603
630
|
lastTriggeredAt: timestamp,
|
|
604
631
|
lastOccurrenceId: occurrence.occurrenceId,
|
|
605
|
-
});
|
|
632
|
+
}));
|
|
606
633
|
return deferred.occurrence ?? saveOccurrence(controllerHome, {
|
|
607
634
|
...wakeDecision,
|
|
608
635
|
status: semanticWait ? 'skipped' : 'failed',
|
|
@@ -628,8 +655,7 @@ async function executeExternalControllerWake(
|
|
|
628
655
|
attemptedActions: [`schedule:${schedule.scheduleId}`, `work:${workId}`, `controller:${controllerType}`],
|
|
629
656
|
},
|
|
630
657
|
});
|
|
631
|
-
|
|
632
|
-
saveSchedule(controllerHome, { ...latest, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId });
|
|
658
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId }));
|
|
633
659
|
return failed.occurrence ?? saveOccurrence(controllerHome, { ...wakeDecision, status: 'failed', reason });
|
|
634
660
|
}
|
|
635
661
|
}
|
|
@@ -640,6 +666,10 @@ export async function evaluateSchedule(
|
|
|
640
666
|
force = false,
|
|
641
667
|
triggerContext?: ScheduleTriggerContext,
|
|
642
668
|
): Promise<ScheduleOccurrence | undefined> {
|
|
669
|
+
// The persisted Schedule record is the authority. Callers may legitimately
|
|
670
|
+
// retain the object returned by create/list/get across multiple trigger
|
|
671
|
+
// attempts, so never let an older in-memory revision become execution state.
|
|
672
|
+
schedule = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
|
|
643
673
|
// Preserve retired schedule records as evidence, but do not create a new
|
|
644
674
|
// occurrence or resume a controller/external action from their old payload.
|
|
645
675
|
if (retiredScheduleReason(schedule)) return undefined;
|
|
@@ -694,7 +724,7 @@ export async function evaluateSchedule(
|
|
|
694
724
|
attemptedActions: [`operation:${schedule.action.operation}`],
|
|
695
725
|
},
|
|
696
726
|
);
|
|
697
|
-
|
|
727
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId }));
|
|
698
728
|
return externalControllerHandoff;
|
|
699
729
|
}
|
|
700
730
|
|
|
@@ -731,7 +761,10 @@ export async function evaluateSchedule(
|
|
|
731
761
|
return decideOccurrence(controllerHome, schedule, occurrence, 'active_occurrence', 'skipped', 'Maximum active occurrences reached.');
|
|
732
762
|
}
|
|
733
763
|
if (schedule.consecutiveFailures >= schedule.policy.maxFailures) {
|
|
734
|
-
|
|
764
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => ({
|
|
765
|
+
enabled: false,
|
|
766
|
+
pausedReason: current.enabled ? 'Maximum consecutive failures reached.' : current.pausedReason,
|
|
767
|
+
}));
|
|
735
768
|
return decideOccurrence(controllerHome, schedule, occurrence, 'stopped', 'skipped', 'Schedule paused after repeated failures.');
|
|
736
769
|
}
|
|
737
770
|
if (schedule.nextEligibleAt && Date.parse(schedule.nextEligibleAt) > Date.now() && !force) {
|
|
@@ -785,14 +818,14 @@ export async function evaluateSchedule(
|
|
|
785
818
|
readyForExecution: preview.status.readyForExecution,
|
|
786
819
|
}));
|
|
787
820
|
}
|
|
788
|
-
|
|
821
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId }));
|
|
789
822
|
return decideOccurrence(controllerHome, schedule, occurrence, 'would_execute', 'shadowed', 'Shadow mode records the live maintenance decision without applying it.', occurrenceDecisionEvidence({
|
|
790
823
|
actionId: preview.actionId,
|
|
791
824
|
safeCandidates: preview.selectedCandidateIds.length,
|
|
792
825
|
typedSafeCandidates: preview.selectedTypedCandidateIds.length,
|
|
793
826
|
}));
|
|
794
827
|
}
|
|
795
|
-
|
|
828
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId }));
|
|
796
829
|
return decideOccurrence(controllerHome, schedule, occurrence, 'would_execute', 'shadowed', 'Shadow mode records the decision without modifying the repository.', due.evidence);
|
|
797
830
|
}
|
|
798
831
|
|
|
@@ -848,28 +881,29 @@ export async function evaluateSchedule(
|
|
|
848
881
|
try {
|
|
849
882
|
const repository = getRepository(schedule.repoId, controllerHome, { includeRemoved: true });
|
|
850
883
|
const observation = await executeScheduledGithubIssueWatch({ repoRoot: repository.canonicalRoot, args, observedAt: timestamp });
|
|
851
|
-
const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
|
|
852
884
|
const evidence = occurrenceDecisionEvidence({
|
|
853
885
|
githubRepository: typeof args.github_repository === 'string' ? args.github_repository : undefined,
|
|
854
886
|
observationStatus: observation.status,
|
|
855
887
|
observedIssueCount: observation.issues.length,
|
|
856
888
|
changedIssueNumbers: observation.changedOpenIssues.map((issue) => issue.number),
|
|
857
889
|
});
|
|
858
|
-
const persistObservation = () =>
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
890
|
+
const persistObservation = () => updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => {
|
|
891
|
+
if (current.action.operation !== GITHUB_ISSUE_WATCH_OPERATION) throw new Error('SCHEDULE_ACTION_CHANGED_DURING_GITHUB_OBSERVATION');
|
|
892
|
+
return {
|
|
893
|
+
action: {
|
|
894
|
+
...current.action,
|
|
895
|
+
arguments: { ...(current.action.arguments ?? {}), issue_watch_state: observation.nextState, issue_watch_since: undefined },
|
|
896
|
+
},
|
|
897
|
+
lastTriggeredAt: timestamp,
|
|
898
|
+
lastOccurrenceId: occurrenceId,
|
|
899
|
+
lastObservationAt: timestamp,
|
|
900
|
+
lastObservationStatus: observation.status,
|
|
901
|
+
lastObservationChangedAt: observation.shouldWake ? timestamp : current.lastObservationChangedAt,
|
|
902
|
+
consecutiveFailures: 0,
|
|
903
|
+
consecutiveNoops: observation.shouldWake ? 0 : (current.consecutiveNoops ?? 0) + 1,
|
|
904
|
+
nextEligibleAt: undefined,
|
|
905
|
+
...(current.enabled ? { pausedReason: undefined } : {}),
|
|
906
|
+
};
|
|
873
907
|
});
|
|
874
908
|
if (!observation.shouldWake) {
|
|
875
909
|
const observedSchedule = persistObservation();
|
|
@@ -923,11 +957,11 @@ export async function evaluateSchedule(
|
|
|
923
957
|
const workStore = { controllerHome, repoId: schedule.repoId };
|
|
924
958
|
const work = getWorkContract(workStore, workId);
|
|
925
959
|
if (!work) {
|
|
926
|
-
|
|
960
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ enabled: false, pausedReason: `Work ${workId} no longer exists.`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId }));
|
|
927
961
|
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', `SCHEDULE_BROWSER_PROBE_WORK_NOT_FOUND:${workId}`);
|
|
928
962
|
}
|
|
929
963
|
if (isTerminalWorkContractStatus(work.status)) {
|
|
930
|
-
|
|
964
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ enabled: false, pausedReason: `Work ${workId} is terminal (${work.status}).`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId }));
|
|
931
965
|
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} is terminal (${work.status}); browser watcher stopped before probing.`);
|
|
932
966
|
}
|
|
933
967
|
}
|
|
@@ -945,16 +979,15 @@ export async function evaluateSchedule(
|
|
|
945
979
|
});
|
|
946
980
|
|
|
947
981
|
if (probe.status === 'auth_required') {
|
|
948
|
-
const observedSchedule =
|
|
949
|
-
...latest,
|
|
982
|
+
const observedSchedule = updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => ({
|
|
950
983
|
lastTriggeredAt: timestamp,
|
|
951
984
|
lastOccurrenceId: occurrenceId,
|
|
952
985
|
lastObservationAt: timestamp,
|
|
953
986
|
lastObservationStatus: 'auth_required',
|
|
954
987
|
consecutiveFailures: 0,
|
|
955
988
|
nextEligibleAt: undefined,
|
|
956
|
-
pausedReason: undefined,
|
|
957
|
-
});
|
|
989
|
+
...(current.enabled ? { pausedReason: undefined } : {}),
|
|
990
|
+
}));
|
|
958
991
|
if (args.wake_on_auth_required !== false) {
|
|
959
992
|
const authPrompt = typeof args.auth_required_prompt === 'string'
|
|
960
993
|
? args.auth_required_prompt
|
|
@@ -997,8 +1030,7 @@ export async function evaluateSchedule(
|
|
|
997
1030
|
}
|
|
998
1031
|
|
|
999
1032
|
if (probe.status === 'keepalive') {
|
|
1000
|
-
const observedSchedule =
|
|
1001
|
-
...latest,
|
|
1033
|
+
const observedSchedule = updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => ({
|
|
1002
1034
|
lastTriggeredAt: timestamp,
|
|
1003
1035
|
lastOccurrenceId: occurrenceId,
|
|
1004
1036
|
lastObservationAt: timestamp,
|
|
@@ -1006,8 +1038,8 @@ export async function evaluateSchedule(
|
|
|
1006
1038
|
consecutiveFailures: 0,
|
|
1007
1039
|
consecutiveNoops: 0,
|
|
1008
1040
|
nextEligibleAt: undefined,
|
|
1009
|
-
pausedReason: undefined,
|
|
1010
|
-
});
|
|
1041
|
+
...(current.enabled ? { pausedReason: undefined } : {}),
|
|
1042
|
+
}));
|
|
1011
1043
|
return decideOccurrence(controllerHome, observedSchedule, occurrence, 'execute', 'succeeded', 'Browser session keepalive refreshed successfully.', observationEvidence);
|
|
1012
1044
|
}
|
|
1013
1045
|
|
|
@@ -1019,19 +1051,18 @@ export async function evaluateSchedule(
|
|
|
1019
1051
|
const changed = observation.status === 'changed';
|
|
1020
1052
|
const shouldWake = args.wake_on_change !== false && observation.shouldWake;
|
|
1021
1053
|
const observationStatus: RepositorySchedule['lastObservationStatus'] = observation.status;
|
|
1022
|
-
const observedSchedule =
|
|
1023
|
-
...latest,
|
|
1054
|
+
const observedSchedule = updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => ({
|
|
1024
1055
|
lastTriggeredAt: timestamp,
|
|
1025
1056
|
lastOccurrenceId: occurrenceId,
|
|
1026
1057
|
lastObservationAt: timestamp,
|
|
1027
1058
|
lastObservationFingerprint: probe.fingerprint,
|
|
1028
|
-
lastObservationChangedAt: changed ? timestamp :
|
|
1059
|
+
lastObservationChangedAt: changed ? timestamp : current.lastObservationChangedAt,
|
|
1029
1060
|
lastObservationStatus: observationStatus,
|
|
1030
1061
|
consecutiveFailures: 0,
|
|
1031
|
-
consecutiveNoops: shouldWake ? 0 : (
|
|
1062
|
+
consecutiveNoops: shouldWake ? 0 : (current.consecutiveNoops ?? 0) + 1,
|
|
1032
1063
|
nextEligibleAt: undefined,
|
|
1033
|
-
pausedReason: undefined,
|
|
1034
|
-
});
|
|
1064
|
+
...(current.enabled ? { pausedReason: undefined } : {}),
|
|
1065
|
+
}));
|
|
1035
1066
|
|
|
1036
1067
|
if (!shouldWake) {
|
|
1037
1068
|
const reason = observation.status === 'baseline'
|
|
@@ -1066,8 +1097,7 @@ export async function evaluateSchedule(
|
|
|
1066
1097
|
attemptedActions: [`schedule:${schedule.scheduleId}`, `work:${workId}`, 'operation:browser_probe'],
|
|
1067
1098
|
},
|
|
1068
1099
|
});
|
|
1069
|
-
|
|
1070
|
-
saveSchedule(controllerHome, { ...latest, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
|
|
1100
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId }));
|
|
1071
1101
|
return failed.occurrence ?? saveOccurrence(controllerHome, { ...occurrence, status: 'failed', decision: 'execute', reason });
|
|
1072
1102
|
}
|
|
1073
1103
|
}
|
|
@@ -1112,15 +1142,13 @@ export async function evaluateSchedule(
|
|
|
1112
1142
|
cancelPendingApprovals: false,
|
|
1113
1143
|
});
|
|
1114
1144
|
const appliedCount = result.applied.filter((candidate) => candidate.applied).length;
|
|
1115
|
-
|
|
1116
|
-
saveSchedule(controllerHome, {
|
|
1117
|
-
...latest,
|
|
1145
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => ({
|
|
1118
1146
|
lastTriggeredAt: timestamp,
|
|
1119
1147
|
lastOccurrenceId: occurrenceId,
|
|
1120
1148
|
consecutiveFailures: 0,
|
|
1121
1149
|
nextEligibleAt: undefined,
|
|
1122
|
-
pausedReason: undefined,
|
|
1123
|
-
});
|
|
1150
|
+
...(current.enabled ? { pausedReason: undefined } : {}),
|
|
1151
|
+
}));
|
|
1124
1152
|
return saveOccurrence(controllerHome, {
|
|
1125
1153
|
...decision,
|
|
1126
1154
|
status: 'succeeded',
|
|
@@ -1149,12 +1177,10 @@ export async function evaluateSchedule(
|
|
|
1149
1177
|
],
|
|
1150
1178
|
},
|
|
1151
1179
|
});
|
|
1152
|
-
|
|
1153
|
-
saveSchedule(controllerHome, {
|
|
1154
|
-
...latest,
|
|
1180
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({
|
|
1155
1181
|
lastTriggeredAt: timestamp,
|
|
1156
1182
|
lastOccurrenceId: occurrenceId,
|
|
1157
|
-
});
|
|
1183
|
+
}));
|
|
1158
1184
|
return failed.occurrence ?? saveOccurrence(controllerHome, {
|
|
1159
1185
|
...decision,
|
|
1160
1186
|
status: 'failed',
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
getSchedule,
|
|
6
6
|
recordScheduleOccurrenceHandoff,
|
|
7
7
|
saveOccurrence,
|
|
8
|
-
|
|
8
|
+
updateSchedule,
|
|
9
9
|
type ScheduleOccurrenceHandoffInput,
|
|
10
10
|
} from './store';
|
|
11
11
|
|
|
@@ -27,7 +27,7 @@ function computeScheduleFailureState(schedule: ReturnType<typeof getSchedule>, n
|
|
|
27
27
|
consecutiveFailures: nextFailures,
|
|
28
28
|
nextEligibleAt: computeScheduleBackoff(schedule, nextFailures),
|
|
29
29
|
enabled: shouldPause ? false : schedule.enabled,
|
|
30
|
-
pausedReason: shouldPause ? 'Maximum consecutive failures reached.' : undefined,
|
|
30
|
+
pausedReason: shouldPause ? 'Maximum consecutive failures reached.' : schedule.enabled ? undefined : schedule.pausedReason,
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -62,16 +62,15 @@ export function applyScheduleFailure(
|
|
|
62
62
|
|
|
63
63
|
const countFailure = options.countFailure !== false;
|
|
64
64
|
const nextSchedule = options.pauseReason
|
|
65
|
-
?
|
|
66
|
-
...schedule,
|
|
65
|
+
? updateSchedule(controllerHome, repoId, scheduleId, (current) => ({
|
|
67
66
|
enabled: false,
|
|
68
67
|
pausedReason: options.pauseReason,
|
|
69
68
|
nextEligibleAt: undefined,
|
|
70
|
-
consecutiveFailures: countFailure ?
|
|
71
|
-
})
|
|
69
|
+
consecutiveFailures: countFailure ? current.consecutiveFailures + 1 : current.consecutiveFailures,
|
|
70
|
+
}))
|
|
72
71
|
: countFailure
|
|
73
|
-
?
|
|
74
|
-
:
|
|
72
|
+
? updateSchedule(controllerHome, repoId, scheduleId, (current) => computeScheduleFailureState(current, current.consecutiveFailures + 1))
|
|
73
|
+
: getSchedule(controllerHome, repoId, scheduleId);
|
|
75
74
|
return { schedule: nextSchedule, occurrence: nextOccurrence };
|
|
76
75
|
}
|
|
77
76
|
|
|
@@ -87,7 +86,6 @@ export function applyScheduleRetryableFailure(
|
|
|
87
86
|
countFailure?: boolean;
|
|
88
87
|
},
|
|
89
88
|
): { schedule: ReturnType<typeof getSchedule>; occurrence?: ScheduleOccurrence } {
|
|
90
|
-
const schedule = getSchedule(controllerHome, repoId, scheduleId);
|
|
91
89
|
const occurrence = getOccurrence(controllerHome, repoId, occurrenceId);
|
|
92
90
|
let nextOccurrence = occurrence;
|
|
93
91
|
if (occurrence && !TERMINAL_OCCURRENCE_STATUSES.has(occurrence.status)) {
|
|
@@ -99,15 +97,22 @@ export function applyScheduleRetryableFailure(
|
|
|
99
97
|
});
|
|
100
98
|
}
|
|
101
99
|
const countFailure = options.countFailure !== false;
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
100
|
+
const nextSchedule = updateSchedule(controllerHome, repoId, scheduleId, (current) => {
|
|
101
|
+
const nextFailures = countFailure ? current.consecutiveFailures + 1 : current.consecutiveFailures;
|
|
102
|
+
const failureState = countFailure && current.enabled
|
|
103
|
+
? computeScheduleFailureState(current, nextFailures)
|
|
104
|
+
: {
|
|
105
|
+
consecutiveFailures: nextFailures,
|
|
106
|
+
nextEligibleAt: countFailure
|
|
107
|
+
? current.nextEligibleAt
|
|
108
|
+
: computeScheduleBackoff(current, Math.max(1, nextFailures)),
|
|
109
|
+
enabled: current.enabled,
|
|
110
|
+
pausedReason: current.enabled ? undefined : current.pausedReason,
|
|
111
|
+
};
|
|
112
|
+
// One transient fault backs off and re-arms. Repeated faults have crossed
|
|
113
|
+
// the already-declared failure budget and must stop before they consume
|
|
114
|
+
// further controller sessions or browser interactions.
|
|
115
|
+
return failureState;
|
|
111
116
|
});
|
|
112
117
|
return { schedule: nextSchedule, occurrence: nextOccurrence };
|
|
113
118
|
}
|
|
@@ -142,7 +147,6 @@ export function settleScheduledExecution(
|
|
|
142
147
|
const occurrenceId = typeof job.payload.occurrenceId === 'string' ? job.payload.occurrenceId : undefined;
|
|
143
148
|
if (!scheduleId || !occurrenceId) return;
|
|
144
149
|
try {
|
|
145
|
-
const schedule = getSchedule(controllerHome, job.repoId, scheduleId);
|
|
146
150
|
const occurrence = getOccurrence(controllerHome, job.repoId, occurrenceId);
|
|
147
151
|
if (occurrence && !['succeeded', 'failed', 'shadowed', 'skipped'].includes(occurrence.status)) {
|
|
148
152
|
saveOccurrence(controllerHome, {
|
|
@@ -175,12 +179,11 @@ export function settleScheduledExecution(
|
|
|
175
179
|
});
|
|
176
180
|
return;
|
|
177
181
|
}
|
|
178
|
-
|
|
179
|
-
...schedule,
|
|
182
|
+
updateSchedule(controllerHome, job.repoId, scheduleId, (current) => ({
|
|
180
183
|
consecutiveFailures: 0,
|
|
181
184
|
nextEligibleAt: undefined,
|
|
182
|
-
pausedReason: undefined,
|
|
183
|
-
});
|
|
185
|
+
...(current.enabled ? { pausedReason: undefined } : {}),
|
|
186
|
+
}));
|
|
184
187
|
} catch {
|
|
185
188
|
// Job terminal state remains authoritative even if an old schedule record
|
|
186
189
|
// has already been removed. Reconciliation must not be blocked by it.
|