libre-webui 0.39.0 → 0.40.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/backend/dist/persistence/extensionTypes.d.ts +2 -0
- package/backend/dist/persistence/extensionTypes.d.ts.map +1 -1
- package/backend/dist/persistence/extensionTypes.js.map +1 -1
- package/backend/dist/persistence/postgresExtensionRepositories.d.ts.map +1 -1
- package/backend/dist/persistence/postgresExtensionRepositories.js +6 -0
- package/backend/dist/persistence/postgresExtensionRepositories.js.map +1 -1
- package/backend/dist/persistence/sqliteExtensionRepositories.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteExtensionRepositories.js +7 -0
- package/backend/dist/persistence/sqliteExtensionRepositories.js.map +1 -1
- package/backend/dist/routes/email.d.ts.map +1 -1
- package/backend/dist/routes/email.js +51 -1
- package/backend/dist/routes/email.js.map +1 -1
- package/backend/dist/routes/tts.d.ts.map +1 -1
- package/backend/dist/routes/tts.js +6 -1
- package/backend/dist/routes/tts.js.map +1 -1
- package/backend/dist/services/agentCliService.d.ts.map +1 -1
- package/backend/dist/services/agentCliService.js +1 -0
- package/backend/dist/services/agentCliService.js.map +1 -1
- package/backend/dist/services/emailService.d.ts +17 -3
- package/backend/dist/services/emailService.d.ts.map +1 -1
- package/backend/dist/services/emailService.js +73 -11
- package/backend/dist/services/emailService.js.map +1 -1
- package/backend/dist/services/pluginCredentialsService.d.ts +3 -0
- package/backend/dist/services/pluginCredentialsService.d.ts.map +1 -1
- package/backend/dist/services/pluginCredentialsService.js +18 -5
- package/backend/dist/services/pluginCredentialsService.js.map +1 -1
- package/backend/dist/services/pluginService.d.ts +9 -0
- package/backend/dist/services/pluginService.d.ts.map +1 -1
- package/backend/dist/services/pluginService.js +31 -14
- package/backend/dist/services/pluginService.js.map +1 -1
- package/backend/dist/services/workRuntimeService.d.ts +5 -0
- package/backend/dist/services/workRuntimeService.d.ts.map +1 -1
- package/backend/dist/services/workRuntimeService.js +243 -84
- package/backend/dist/services/workRuntimeService.js.map +1 -1
- package/backend/dist/utils/emailMarkdown.js +3 -3
- package/backend/dist/utils/emailMarkdown.js.map +1 -1
- package/backend/dist/utils/pluginDefinitionTrust.d.ts +7 -0
- package/backend/dist/utils/pluginDefinitionTrust.d.ts.map +1 -1
- package/backend/dist/utils/pluginDefinitionTrust.js +49 -1
- package/backend/dist/utils/pluginDefinitionTrust.js.map +1 -1
- package/backend/package.json +1 -1
- package/frontend/dist/artifact-runtime/.build-fingerprint +1 -1
- package/frontend/dist/artifact-runtime/mermaid.js +366 -366
- package/frontend/dist/assets/index-DLfU_qNT.js +75 -0
- package/frontend/dist/assets/index-DtS3qhdq.css +2 -0
- package/frontend/dist/assets/xterm-B1njb2Cr.css +1 -0
- package/frontend/dist/index.html +27 -26
- package/frontend/dist/js/ArtifactContainer-BjzltfdU.js +1 -0
- package/frontend/dist/js/{ArtifactDemoPage-Ddpy2O_v.js → ArtifactDemoPage-DWua6xj9.js} +1 -1
- package/frontend/dist/js/{ArtifactSandboxFrame-CdsAqv8H.js → ArtifactSandboxFrame-CR9ghP0A.js} +1 -1
- package/frontend/dist/js/ArtifactSlideOutPanel-DX2nL3AS.js +1 -0
- package/frontend/dist/js/AutomationModal-DZe-C2jN.js +1 -0
- package/frontend/dist/js/AutomationsPage-CrCcnTyk.js +1 -0
- package/frontend/dist/js/AvatarUpload-nPS0QHod.js +1 -0
- package/frontend/dist/js/Button-Dh2UJA-B.js +1 -0
- package/frontend/dist/js/CalendarPage-D2WiWglM.js +1 -0
- package/frontend/dist/js/ChannelsPage-COp2T8FQ.js +1 -0
- package/frontend/dist/js/ChatPage-CUfAlpdt.js +14 -0
- package/frontend/dist/js/ConnectModels-emc3dv4A.js +1 -0
- package/frontend/dist/js/EvaluationsPage-CbyeDkz_.js +2 -0
- package/frontend/dist/js/FirstTimeSetup-C4ffQRMN.js +1 -0
- package/frontend/dist/js/GalleryPage-D4Ci_3dv.js +1 -0
- package/frontend/dist/js/HomePage-C3IhEERN.js +1 -0
- package/frontend/dist/js/LanguageSwitcher-BQqDJPwm.js +1 -0
- package/frontend/dist/js/ModelManagerModals-DhY8Rme6.js +1 -0
- package/frontend/dist/js/NotesPage-j812I1BN.js +3 -0
- package/frontend/dist/js/PasswordStrengthMeter-BSEtQBJp.js +1 -0
- package/frontend/dist/js/PersonasPage--YWoThqF.js +13 -0
- package/frontend/dist/js/PluginUsagePage-CZiMdmjI.js +1 -0
- package/frontend/dist/js/{RichMessageContent-BfYbVBFz.js → RichMessageContent-BLI0hhzR.js} +2 -2
- package/frontend/dist/js/RichMessageContent-BXv5baJ_.js +1 -0
- package/frontend/dist/js/SettingsModal-C6aVzSI-.js +19 -0
- package/frontend/dist/js/SettingsToggle-CkLLtFbz.js +1 -0
- package/frontend/dist/js/ShareDialog-DQskc17d.js +1 -0
- package/frontend/dist/js/StrandsPage-DENhVKcz.js +1 -0
- package/frontend/dist/js/SystemPage-C86ypXG4.js +1 -0
- package/frontend/dist/js/UserManagementPanel-D1CS0KPn.js +1 -0
- package/frontend/dist/js/WhatsNewModal-B31A_UQY.js +1 -0
- package/frontend/dist/js/WorkPage-DEXBL5J7.js +7 -0
- package/frontend/dist/js/api-DRAKhdz7.js +1 -0
- package/frontend/dist/js/{api-BrHW-VGd.js → api-Dcz1G4Ib.js} +4 -2
- package/frontend/dist/js/{appNavigation-B6xYIgLi.js → appNavigation-C5A9BF_G.js} +1 -1
- package/frontend/dist/js/appStore-0_qlwS8f.js +2 -0
- package/frontend/dist/js/appVersion-D5lk_2zb.js +1 -0
- package/frontend/dist/js/ar-BZntyucf.js +2 -0
- package/frontend/dist/js/authStore-C4dzH1NY.js +1 -0
- package/frontend/dist/js/{authStore-DjAsRpNL.js → authStore-COgKeTRv.js} +3 -3
- package/frontend/dist/js/bn-BDCaFQu-.js +2 -0
- package/frontend/dist/js/chatStore-CVTbkZyb.js +3 -0
- package/frontend/dist/js/{client-DppLngAs.js → client-jaxZzn-I.js} +2 -2
- package/frontend/dist/js/{composerStyles-C83jssgZ.js → composerStyles-BEx0XAp6.js} +1 -1
- package/frontend/dist/js/{config-4_s8hdqO.js → config-Da1yh9sC.js} +1 -1
- package/frontend/dist/js/confirmStore-CKkUtP9c.js +1 -0
- package/frontend/dist/js/cs-BCmN--jQ.js +2 -0
- package/frontend/dist/js/da-B895dAyV.js +2 -0
- package/frontend/dist/js/de-B0ZhC0dm.js +2 -0
- package/frontend/dist/js/es-D7eaw4_W.js +2 -0
- package/frontend/dist/js/{evaluationsApi-DyuW7IJ_.js → evaluationsApi-BwaxBVpl.js} +1 -1
- package/frontend/dist/js/fr-DsB0gLW4.js +2 -0
- package/frontend/dist/js/hi-CLeU3ung.js +2 -0
- package/frontend/dist/js/id-Cdq6tOTi.js +2 -0
- package/frontend/dist/js/is-DJ1tHaAT.js +2 -0
- package/frontend/dist/js/it-9K1R4CeT.js +2 -0
- package/frontend/dist/js/ja-BYP_3M5M.js +2 -0
- package/frontend/dist/js/ko-CNtjlaDm.js +2 -0
- package/frontend/dist/js/liveAnnouncerStore-B6mS-JWC.js +1 -0
- package/frontend/dist/js/messageCodeStyles-D3cWF9XO.js +2 -0
- package/frontend/dist/js/ms-A_IVO_qt.js +2 -0
- package/frontend/dist/js/nl-CrKWO3ea.js +2 -0
- package/frontend/dist/js/pl-BrpSuyDP.js +2 -0
- package/frontend/dist/js/pt-BHbpB5BM.js +2 -0
- package/frontend/dist/js/ru-DzLsS9zY.js +2 -0
- package/frontend/dist/js/{strandsApi-BFbeEpAY.js → strandsApi-COxSPh_5.js} +1 -1
- package/frontend/dist/js/sv-VU2CjzNj.js +2 -0
- package/frontend/dist/js/{teamEventStream-DsAS6P4m.js → teamEventStream-RNJYm84u.js} +1 -1
- package/frontend/dist/js/th-DV2hNXXc.js +2 -0
- package/frontend/dist/js/tr-DvVZ9jxF.js +2 -0
- package/frontend/dist/js/ui-CI0OdG9q.js +2 -0
- package/frontend/dist/js/ui-vendor-6S05F84J.js +1 -0
- package/frontend/dist/js/uk-MFFDRo2m.js +2 -0
- package/frontend/dist/js/useChat-BSkqxHnH.js +13 -0
- package/frontend/dist/js/useDictation-D9whH12W.js +1 -0
- package/frontend/dist/js/vi-CfZ-U8Cw.js +2 -0
- package/frontend/dist/js/{work-CdbmBok5.js → work-C7MYlv5S.js} +1 -1
- package/frontend/dist/js/{workModels-BM2Zyiba.js → workAnnouncements-CAsI8P7c.js} +1 -1
- package/frontend/dist/js/workApi-BJfbaV6k.js +1 -0
- package/frontend/dist/js/{workApi-UPxumrEF.js → workApi-Ck8nTlJn.js} +1 -1
- package/frontend/dist/js/zh-CTl_raYa.js +2 -0
- package/package.json +3 -2
- package/plugins/codex-oauth.json +1 -0
- package/frontend/dist/assets/index-D99DS5c6.js +0 -57
- package/frontend/dist/assets/index-Dz1JufgZ.css +0 -1
- package/frontend/dist/assets/xterm-B8QFf3JU.css +0 -1
- package/frontend/dist/js/ArtifactContainer-Cn7X9HGm.js +0 -1
- package/frontend/dist/js/ArtifactSlideOutPanel-cVd9X80o.js +0 -1
- package/frontend/dist/js/AutomationModal-B4UaWjkk.js +0 -1
- package/frontend/dist/js/AutomationsPage-huCrvLUp.js +0 -1
- package/frontend/dist/js/AvatarUpload-CG-w6Mpt.js +0 -1
- package/frontend/dist/js/Button-j68Q8KVh.js +0 -1
- package/frontend/dist/js/CalendarPage-CZXT1w99.js +0 -1
- package/frontend/dist/js/ChannelsPage-DbtdHnSL.js +0 -1
- package/frontend/dist/js/ChatPage-BZE65kiq.js +0 -14
- package/frontend/dist/js/ConnectModels-DD4bLgdq.js +0 -1
- package/frontend/dist/js/EvaluationsPage-Dq53LaXd.js +0 -2
- package/frontend/dist/js/FirstTimeSetup-BoJIfuQT.js +0 -1
- package/frontend/dist/js/GalleryPage-DKwqeB2A.js +0 -1
- package/frontend/dist/js/HomePage-C8bCJfls.js +0 -1
- package/frontend/dist/js/LanguageSwitcher-B_YyU5JG.js +0 -1
- package/frontend/dist/js/ModelManagerModals-BzjZ-QwL.js +0 -1
- package/frontend/dist/js/NotesPage-CnaAp_w_.js +0 -3
- package/frontend/dist/js/PasswordStrengthMeter-DI4RDv67.js +0 -1
- package/frontend/dist/js/PersonasPage-CH3GUKph.js +0 -13
- package/frontend/dist/js/PluginUsagePage-d54K2gAj.js +0 -1
- package/frontend/dist/js/RichMessageContent-CDxxs3Tt.js +0 -1
- package/frontend/dist/js/SettingsModal-BvtJcvow.js +0 -19
- package/frontend/dist/js/SettingsToggle-DrJ0Bapi.js +0 -1
- package/frontend/dist/js/ShareDialog-DaSxkTKS.js +0 -1
- package/frontend/dist/js/StrandsPage-BQwz0mWq.js +0 -1
- package/frontend/dist/js/SystemPage-BqNZoFxR.js +0 -1
- package/frontend/dist/js/UserManagementPanel-C3T1ReJJ.js +0 -1
- package/frontend/dist/js/WhatsNewModal-CLcU2Ugk.js +0 -1
- package/frontend/dist/js/WorkPage-BpjGn--1.js +0 -8
- package/frontend/dist/js/api-K_hhqPdx.js +0 -1
- package/frontend/dist/js/appStore-DkRdQQeY.js +0 -2
- package/frontend/dist/js/appVersion-BVtSqLVj.js +0 -1
- package/frontend/dist/js/ar-XTZGEtBr.js +0 -2
- package/frontend/dist/js/authStore-DBS6Kbvh.js +0 -1
- package/frontend/dist/js/bn-JiK7ZWmy.js +0 -2
- package/frontend/dist/js/chatStore-DpVxt9VX.js +0 -3
- package/frontend/dist/js/cs-D8ju18CO.js +0 -2
- package/frontend/dist/js/da-BJWcAoUc.js +0 -2
- package/frontend/dist/js/de-CLJLmy69.js +0 -2
- package/frontend/dist/js/es-BvqHEL-3.js +0 -2
- package/frontend/dist/js/fr-B8ZoZomI.js +0 -2
- package/frontend/dist/js/goober.modern-BaHbaW3_.js +0 -1
- package/frontend/dist/js/hi-AzJLALo-.js +0 -2
- package/frontend/dist/js/id-DZYGQhvC.js +0 -2
- package/frontend/dist/js/is-Da_kvtzW.js +0 -2
- package/frontend/dist/js/it-B90zSlqn.js +0 -2
- package/frontend/dist/js/ja-DU_K1GOr.js +0 -2
- package/frontend/dist/js/ko-DicfoO-e.js +0 -2
- package/frontend/dist/js/messageCodeStyles-CuQPkfEK.js +0 -2
- package/frontend/dist/js/ms-CfH7wjUY.js +0 -2
- package/frontend/dist/js/nl-ELlOz-B3.js +0 -2
- package/frontend/dist/js/pl-33dayyUk.js +0 -2
- package/frontend/dist/js/pt-aIXSQPCT.js +0 -2
- package/frontend/dist/js/ru-BSvGtBtW.js +0 -2
- package/frontend/dist/js/sv-B7KOz81W.js +0 -2
- package/frontend/dist/js/th-DgpjGxvA.js +0 -2
- package/frontend/dist/js/tr-BQTbl2h-.js +0 -2
- package/frontend/dist/js/ui-Aq8MP7Ph.js +0 -2
- package/frontend/dist/js/ui-vendor-DVTj_sKN.js +0 -1
- package/frontend/dist/js/uk-JLymloot.js +0 -2
- package/frontend/dist/js/useChat-D3ZAGz70.js +0 -10
- package/frontend/dist/js/useDictation-B6l8QzeM.js +0 -1
- package/frontend/dist/js/vi-BXdLOJ8q.js +0 -2
- package/frontend/dist/js/workApi-D1xtiG3Q.js +0 -1
- package/frontend/dist/js/zh-gdUf9sXa.js +0 -2
|
@@ -346,6 +346,9 @@ export const acquireSharedRuntimeLeaseWithWait = async (attempt, options) => {
|
|
|
346
346
|
};
|
|
347
347
|
/** How long a preview may sit at `starting` before it counts as abandoned. */
|
|
348
348
|
const PREVIEW_START_GRACE_MS = 5 * 60000;
|
|
349
|
+
// Only this explicit protocol exempts a runtime hold from idle cleanup. Older
|
|
350
|
+
// generic members and unrecognized members remain protected during upgrades.
|
|
351
|
+
const isPreviewOnlyPresence = (member) => /^(?:standalone|app-external|app-embedded|external-worker)-\d+-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}:preview-only$/.test(member);
|
|
349
352
|
export class WorkRuntimeService {
|
|
350
353
|
constructor(driver = new DockerWorkRuntimeDriver()) {
|
|
351
354
|
this.image = config.image;
|
|
@@ -365,6 +368,7 @@ export class WorkRuntimeService {
|
|
|
365
368
|
this.activeCommands = new Set();
|
|
366
369
|
this.lastRuntimeUnavailableReason = null;
|
|
367
370
|
this.runtimeLeases = new Map();
|
|
371
|
+
this.runtimePresenceTails = new Map();
|
|
368
372
|
this.previewLeaseReleases = new Map();
|
|
369
373
|
this.previewUsageReleases = new Map();
|
|
370
374
|
this.terminalHolds = new Map();
|
|
@@ -621,6 +625,15 @@ export class WorkRuntimeService {
|
|
|
621
625
|
throw new WorkRuntimeError('This Work runtime lease belongs to a different administrator.', 409, 'WORK_RUNTIME_LEASE_CONFLICT');
|
|
622
626
|
}
|
|
623
627
|
existing.holders += 1;
|
|
628
|
+
try {
|
|
629
|
+
// Publish protection before a helper or run can enter the lifecycle
|
|
630
|
+
// lock, even when its only previous holder was an idle preview.
|
|
631
|
+
await this.refreshRuntimePresence(task, existing);
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
this.releaseRuntimeLease(task.id, task.userId);
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
624
637
|
}
|
|
625
638
|
else {
|
|
626
639
|
const perUser = [...this.runtimeLeases.values()].filter(lease => lease.userId === task.userId).length;
|
|
@@ -666,8 +679,10 @@ export class WorkRuntimeService {
|
|
|
666
679
|
throw new Error('lease ownership was lost');
|
|
667
680
|
})
|
|
668
681
|
.catch(error => {
|
|
669
|
-
if (runtimeLease.sharedLeaseLost
|
|
682
|
+
if (runtimeLease.sharedLeaseLost ||
|
|
683
|
+
this.runtimeLeases.get(task.id) !== runtimeLease) {
|
|
670
684
|
return;
|
|
685
|
+
}
|
|
671
686
|
runtimeLease.sharedLeaseLost = true;
|
|
672
687
|
logger.error(`Shared Work runtime lease was lost for task ${task.id}; stopping its sandbox:`, error);
|
|
673
688
|
void this.driver
|
|
@@ -676,7 +691,7 @@ export class WorkRuntimeService {
|
|
|
676
691
|
});
|
|
677
692
|
}, 20000);
|
|
678
693
|
runtimeLease.sharedLeaseTimer.unref?.();
|
|
679
|
-
const refreshPresence = () =>
|
|
694
|
+
const refreshPresence = (reconcilePreview = false) => this.refreshRuntimePresence(task, runtimeLease, reconcilePreview);
|
|
680
695
|
try {
|
|
681
696
|
await waitForAbortSignal(refreshPresence(), signal);
|
|
682
697
|
}
|
|
@@ -688,7 +703,7 @@ export class WorkRuntimeService {
|
|
|
688
703
|
throw error;
|
|
689
704
|
}
|
|
690
705
|
runtimeLease.presenceTimer = setInterval(() => {
|
|
691
|
-
void refreshPresence().catch(error => logger.warn(`Could not refresh Work activity for ${task.id}:`, error));
|
|
706
|
+
void refreshPresence(true).catch(error => logger.warn(`Could not refresh Work activity for ${task.id}:`, error));
|
|
692
707
|
}, 10000);
|
|
693
708
|
runtimeLease.presenceTimer.unref?.();
|
|
694
709
|
}
|
|
@@ -710,6 +725,7 @@ export class WorkRuntimeService {
|
|
|
710
725
|
lease.holders -= 1;
|
|
711
726
|
if (!force && lease.holders > 0)
|
|
712
727
|
return;
|
|
728
|
+
lease.released = true;
|
|
713
729
|
if (lease.presenceTimer)
|
|
714
730
|
clearInterval(lease.presenceTimer);
|
|
715
731
|
if (lease.sharedLeaseTimer)
|
|
@@ -717,10 +733,78 @@ export class WorkRuntimeService {
|
|
|
717
733
|
this.runtimeLeases.delete(taskId);
|
|
718
734
|
void lease.sharedLease?.release().catch(() => false);
|
|
719
735
|
if (getPlatformRuntimeConfig().mode === 'team') {
|
|
720
|
-
|
|
721
|
-
.clearPresence(`work-task-active:${taskId}`, this.activityMemberId)
|
|
722
|
-
|
|
736
|
+
const clear = async () => {
|
|
737
|
+
await withCoordinationTimeout(getCoordinator().clearPresence(`work-task-active:${taskId}`, this.activityMemberId), SHARED_COORDINATION_OPERATION_TIMEOUT_MS);
|
|
738
|
+
await withCoordinationTimeout(getCoordinator().clearPresence(`work-task-active:${taskId}`, `${this.activityMemberId}:preview-only`), SHARED_COORDINATION_OPERATION_TIMEOUT_MS);
|
|
739
|
+
};
|
|
740
|
+
lease.presenceTail = this.queueRuntimePresence(taskId, clear);
|
|
741
|
+
void lease.presenceTail.catch(error => logger.warn(`Could not clear Work activity for ${taskId}:`, error));
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
refreshRuntimePresence(task, lease, reconcilePreview = false) {
|
|
745
|
+
if (getPlatformRuntimeConfig().mode !== 'team')
|
|
746
|
+
return Promise.resolve();
|
|
747
|
+
const refresh = async () => {
|
|
748
|
+
if (lease.released)
|
|
749
|
+
return;
|
|
750
|
+
if (reconcilePreview && this.previewLeaseReleases.has(task.id)) {
|
|
751
|
+
const generation = lease.previewGeneration;
|
|
752
|
+
const release = this.previewLeaseReleases.get(task.id);
|
|
753
|
+
const row = await getWorkPersistence().findTask(task.id, task.userId);
|
|
754
|
+
// A stopped-state snapshot must not release a preview reopened while
|
|
755
|
+
// this database read was in flight. Startup advances the generation
|
|
756
|
+
// under the lifecycle lock before changing its persisted state.
|
|
757
|
+
if ((!row || row.preview_status === 'stopped') &&
|
|
758
|
+
lease.previewGeneration === generation &&
|
|
759
|
+
this.previewLeaseReleases.get(task.id) === release) {
|
|
760
|
+
this.releasePreviewLease(task.id);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (lease.released)
|
|
764
|
+
return;
|
|
765
|
+
const activeMember = this.activityMemberId;
|
|
766
|
+
const previewMember = `${activeMember}:preview-only`;
|
|
767
|
+
const previewOnly = lease.holders === 1 && this.previewLeaseReleases.has(task.id);
|
|
768
|
+
// Set the new member before clearing the old one, and serialize all
|
|
769
|
+
// transitions so a racing heartbeat cannot erase a newly active hold.
|
|
770
|
+
await withCoordinationTimeout(getCoordinator().setPresence(`work-task-active:${task.id}`, previewOnly ? previewMember : activeMember, 30000), SHARED_COORDINATION_OPERATION_TIMEOUT_MS);
|
|
771
|
+
await withCoordinationTimeout(getCoordinator().clearPresence(`work-task-active:${task.id}`, previewOnly ? activeMember : previewMember), SHARED_COORDINATION_OPERATION_TIMEOUT_MS);
|
|
772
|
+
};
|
|
773
|
+
const pending = this.queueRuntimePresence(task.id, refresh);
|
|
774
|
+
lease.presenceTail = pending;
|
|
775
|
+
return pending;
|
|
776
|
+
}
|
|
777
|
+
queueRuntimePresence(taskId, operation) {
|
|
778
|
+
// Include releases and reacquisitions in one queue: an old lease's delayed
|
|
779
|
+
// clear must finish before a new lease publishes the same process member.
|
|
780
|
+
const pending = (this.runtimePresenceTails.get(taskId) ?? Promise.resolve())
|
|
781
|
+
.catch(() => undefined)
|
|
782
|
+
.then(operation);
|
|
783
|
+
this.runtimePresenceTails.set(taskId, pending);
|
|
784
|
+
void pending
|
|
785
|
+
.finally(() => {
|
|
786
|
+
if (this.runtimePresenceTails.get(taskId) === pending) {
|
|
787
|
+
this.runtimePresenceTails.delete(taskId);
|
|
788
|
+
}
|
|
789
|
+
})
|
|
790
|
+
.catch(() => undefined);
|
|
791
|
+
return pending;
|
|
792
|
+
}
|
|
793
|
+
hasLocalRuntimeActivity(taskId) {
|
|
794
|
+
return (this.activeCommands.has(taskId) ||
|
|
795
|
+
(this.terminalHolds.get(taskId) ?? 0) > 0 ||
|
|
796
|
+
(this.screenHolds.get(taskId) ?? 0) > 0 ||
|
|
797
|
+
(this.runtimeLeases.get(taskId)?.holders ?? 0) >
|
|
798
|
+
(this.previewLeaseReleases.has(taskId) ? 1 : 0));
|
|
799
|
+
}
|
|
800
|
+
async hasRuntimeActivity(taskId) {
|
|
801
|
+
if (this.hasLocalRuntimeActivity(taskId))
|
|
802
|
+
return true;
|
|
803
|
+
if (getPlatformRuntimeConfig().mode === 'team' &&
|
|
804
|
+
(await withCoordinationTimeout(getCoordinator().listPresence(`work-task-active:${taskId}`), SHARED_COORDINATION_OPERATION_TIMEOUT_MS)).some(member => !isPreviewOnlyPresence(member))) {
|
|
805
|
+
return true;
|
|
723
806
|
}
|
|
807
|
+
return (await workUsageService.list(taskId)).some(entry => entry.kind !== 'preview');
|
|
724
808
|
}
|
|
725
809
|
assertRuntimeLease(task) {
|
|
726
810
|
const lease = this.runtimeLeases.get(task.id);
|
|
@@ -866,7 +950,9 @@ export class WorkRuntimeService {
|
|
|
866
950
|
releasePresence();
|
|
867
951
|
try {
|
|
868
952
|
if (remaining <= 0 && !this.shuttingDown) {
|
|
869
|
-
await this.withLifecycleLock(task.id, (_assertHeld, signal) => this.stopContainerIfIdleWithLock(task, signal
|
|
953
|
+
await this.withLifecycleLock(task.id, (_assertHeld, signal) => this.stopContainerIfIdleWithLock(task, signal, {
|
|
954
|
+
ownsRuntimeLease: false,
|
|
955
|
+
}));
|
|
870
956
|
}
|
|
871
957
|
}
|
|
872
958
|
catch (error) {
|
|
@@ -1164,19 +1250,7 @@ export class WorkRuntimeService {
|
|
|
1164
1250
|
// Containers without a task row are startup reconciliation's business.
|
|
1165
1251
|
if (!task)
|
|
1166
1252
|
continue;
|
|
1167
|
-
|
|
1168
|
-
(await getCoordinator().listPresence(`work-task-active:${task.id}`))
|
|
1169
|
-
.length > 0) {
|
|
1170
|
-
continue;
|
|
1171
|
-
}
|
|
1172
|
-
const busy = this.activeCommands.has(task.id) ||
|
|
1173
|
-
(this.terminalHolds.get(task.id) ?? 0) > 0 ||
|
|
1174
|
-
(this.screenHolds.get(task.id) ?? 0) > 0 ||
|
|
1175
|
-
(this.runtimeLeases.has(task.id) &&
|
|
1176
|
-
!this.previewLeaseReleases.has(task.id)) ||
|
|
1177
|
-
// Holds from other processes: terminals, screens and commands
|
|
1178
|
-
// record themselves in the usage registry.
|
|
1179
|
-
(await workUsageService.list(task.id)).some(entry => entry.kind !== 'preview');
|
|
1253
|
+
const busy = await this.hasRuntimeActivity(task.id);
|
|
1180
1254
|
const sharedActivity = getPlatformRuntimeConfig().mode === 'team'
|
|
1181
1255
|
? await getCoordinator().getCache(`work-task-activity:${task.id}`)
|
|
1182
1256
|
: null;
|
|
@@ -1190,14 +1264,29 @@ export class WorkRuntimeService {
|
|
|
1190
1264
|
if (idleAfterMs <= 0 || now - lastActivity < idleAfterMs)
|
|
1191
1265
|
continue;
|
|
1192
1266
|
try {
|
|
1193
|
-
|
|
1194
|
-
await
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1267
|
+
const didStop = await this.withLifecycleLock(task.id, async (assertHeld, signal) => {
|
|
1268
|
+
await assertHeld();
|
|
1269
|
+
// A run, viewer, or preview request may have arrived while the
|
|
1270
|
+
// sweep awaited policy resolution or the lifecycle lock.
|
|
1271
|
+
const currentActivity = getPlatformRuntimeConfig().mode === 'team'
|
|
1272
|
+
? await getCoordinator().getCache(`work-task-activity:${task.id}`)
|
|
1273
|
+
: this.taskActivity.get(task.id);
|
|
1274
|
+
if ((await this.hasRuntimeActivity(task.id)) ||
|
|
1275
|
+
this.hasLocalRuntimeActivity(task.id) ||
|
|
1276
|
+
(currentActivity !== null &&
|
|
1277
|
+
currentActivity !== undefined &&
|
|
1278
|
+
now - currentActivity < idleAfterMs)) {
|
|
1279
|
+
return false;
|
|
1280
|
+
}
|
|
1281
|
+
await this.stopPreviewPrepared(task, signal);
|
|
1282
|
+
await assertHeld();
|
|
1283
|
+
await this.markPreviewStopped(task.id);
|
|
1284
|
+
this.releasePreviewLease(task.id);
|
|
1285
|
+
this.completeRecoveryTask(task.id);
|
|
1286
|
+
return true;
|
|
1287
|
+
});
|
|
1288
|
+
if (!didStop)
|
|
1289
|
+
continue;
|
|
1201
1290
|
this.taskActivity.delete(task.id);
|
|
1202
1291
|
stopped += 1;
|
|
1203
1292
|
logger.info(`Stopped idle Work sandbox ${task.containerName} after ${idleAfterMs}ms of inactivity.`);
|
|
@@ -1226,53 +1315,54 @@ export class WorkRuntimeService {
|
|
|
1226
1315
|
const candidates = (await workTaskService.listAllTaskRecords()).filter(task => task.previewStatus === 'starting' || task.previewStatus === 'running');
|
|
1227
1316
|
if (candidates.length === 0)
|
|
1228
1317
|
return { stopped: 0 };
|
|
1229
|
-
let discovered;
|
|
1230
1318
|
try {
|
|
1231
|
-
|
|
1319
|
+
await this.driver.listManaged();
|
|
1232
1320
|
}
|
|
1233
1321
|
catch {
|
|
1234
1322
|
// The runtime is unreachable; the rows cannot be judged either way.
|
|
1235
1323
|
return { stopped: 0 };
|
|
1236
1324
|
}
|
|
1237
|
-
const running = new Set(discovered.filter(entry => entry.running).map(entry => entry.taskId));
|
|
1238
1325
|
let stopped = 0;
|
|
1239
1326
|
for (const task of candidates) {
|
|
1240
|
-
|
|
1241
|
-
// open, is not stale: leave the owner's own flows to settle it.
|
|
1242
|
-
if (this.activeCommands.has(task.id) ||
|
|
1243
|
-
(this.terminalHolds.get(task.id) ?? 0) > 0 ||
|
|
1244
|
-
(this.screenHolds.get(task.id) ?? 0) > 0 ||
|
|
1245
|
-
(this.runtimeLeases.has(task.id) &&
|
|
1246
|
-
!this.previewLeaseReleases.has(task.id))) {
|
|
1247
|
-
continue;
|
|
1248
|
-
}
|
|
1249
|
-
if (task.previewStatus === 'starting' &&
|
|
1250
|
-
now - task.updatedAt < PREVIEW_START_GRACE_MS) {
|
|
1251
|
-
continue;
|
|
1252
|
-
}
|
|
1253
|
-
if ((await workUsageService.list(task.id)).some(entry => entry.kind !== 'preview')) {
|
|
1327
|
+
if (await this.hasRuntimeActivity(task.id))
|
|
1254
1328
|
continue;
|
|
1255
|
-
}
|
|
1256
|
-
if (getPlatformRuntimeConfig().mode === 'team' &&
|
|
1257
|
-
(await getCoordinator().listPresence(`work-task-active:${task.id}`))
|
|
1258
|
-
.length > 0) {
|
|
1259
|
-
continue;
|
|
1260
|
-
}
|
|
1261
1329
|
try {
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1330
|
+
const didReconcile = await this.withLifecycleLock(task.id, async (assertHeld, signal) => {
|
|
1331
|
+
await assertHeld();
|
|
1332
|
+
const row = await getWorkPersistence().findTask(task.id, task.userId);
|
|
1333
|
+
if (!row ||
|
|
1334
|
+
(row.preview_status !== 'starting' &&
|
|
1335
|
+
row.preview_status !== 'running') ||
|
|
1336
|
+
(row.preview_status === 'starting' &&
|
|
1337
|
+
now - row.updated_at < PREVIEW_START_GRACE_MS) ||
|
|
1338
|
+
(await this.hasRuntimeActivity(task.id)) ||
|
|
1339
|
+
this.hasLocalRuntimeActivity(task.id)) {
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1342
|
+
const sharedPreview = getPlatformRuntimeConfig().mode === 'team' &&
|
|
1343
|
+
(await getCoordinator().listPresence(`work-task-active:${task.id}`)).some(isPreviewOnlyPresence);
|
|
1344
|
+
const state = await this.previewProcessCheckWithLock(task, signal);
|
|
1345
|
+
if (state === 'ready' &&
|
|
1346
|
+
(this.previewLeaseReleases.has(task.id) || sharedPreview))
|
|
1347
|
+
return false;
|
|
1348
|
+
if ((await this.hasRuntimeActivity(task.id)) ||
|
|
1349
|
+
this.hasLocalRuntimeActivity(task.id))
|
|
1350
|
+
return false;
|
|
1351
|
+
if (state !== 'absent')
|
|
1352
|
+
await this.stopPreviewPrepared(task, signal);
|
|
1353
|
+
await assertHeld();
|
|
1354
|
+
await this.markPreviewStopped(task.id);
|
|
1355
|
+
this.releasePreviewLease(task.id);
|
|
1356
|
+
return true;
|
|
1357
|
+
});
|
|
1358
|
+
if (!didReconcile)
|
|
1265
1359
|
continue;
|
|
1266
|
-
|
|
1360
|
+
stopped += 1;
|
|
1361
|
+
logger.info(`Marked the stale ${task.previewStatus} preview of Work task ${task.id} as stopped.`);
|
|
1267
1362
|
}
|
|
1268
1363
|
catch (error) {
|
|
1269
1364
|
logger.warn(`Could not check the preview of Work task ${task.id}:`, error);
|
|
1270
|
-
continue;
|
|
1271
1365
|
}
|
|
1272
|
-
await this.markPreviewStopped(task.id);
|
|
1273
|
-
this.releasePreviewLease(task.id);
|
|
1274
|
-
stopped += 1;
|
|
1275
|
-
logger.info(`Marked the stale ${task.previewStatus} preview of Work task ${task.id} as stopped.`);
|
|
1276
1366
|
}
|
|
1277
1367
|
return { stopped };
|
|
1278
1368
|
}
|
|
@@ -1535,9 +1625,23 @@ export class WorkRuntimeService {
|
|
|
1535
1625
|
// transitions (start, stop, recreate, preview) stay lease-guarded.
|
|
1536
1626
|
if (error instanceof WorkRuntimeError &&
|
|
1537
1627
|
error.code === 'WORK_RUNTIME_LEASE_CONFLICT') {
|
|
1538
|
-
await this.
|
|
1539
|
-
|
|
1540
|
-
return
|
|
1628
|
+
const releasePresence = await this.acquireViewerPresence(task.id);
|
|
1629
|
+
try {
|
|
1630
|
+
return await this.withLifecycleLock(task.id, async () => {
|
|
1631
|
+
await this.assertTaskIsActive(task);
|
|
1632
|
+
if ((await this.driver.runtimeState(task)) !== 'running') {
|
|
1633
|
+
throw error;
|
|
1634
|
+
}
|
|
1635
|
+
try {
|
|
1636
|
+
return await operation();
|
|
1637
|
+
}
|
|
1638
|
+
finally {
|
|
1639
|
+
this.noteTaskActivity(task.id);
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
finally {
|
|
1644
|
+
releasePresence();
|
|
1541
1645
|
}
|
|
1542
1646
|
}
|
|
1543
1647
|
throw error;
|
|
@@ -1566,6 +1670,16 @@ export class WorkRuntimeService {
|
|
|
1566
1670
|
if (!options.ignoreActiveCommands && this.activeCommands.has(task.id)) {
|
|
1567
1671
|
return false;
|
|
1568
1672
|
}
|
|
1673
|
+
// Commands and helpers still own their lease during teardown, but screen
|
|
1674
|
+
// viewers do not. Preserve any other holder, including the run executor
|
|
1675
|
+
// retaining the sandbox between tool calls, until explicit run cleanup.
|
|
1676
|
+
const ownLeaseHolders = options.ownsRuntimeLease === false ? 0 : 1;
|
|
1677
|
+
// A registered preview lease is checked for readiness below. Counting it
|
|
1678
|
+
// as another hold would retain dead previews and their capacity forever.
|
|
1679
|
+
const hasOtherRuntimeHolders = () => (this.runtimeLeases.get(task.id)?.holders ?? 0) >
|
|
1680
|
+
ownLeaseHolders + (this.previewLeaseReleases.has(task.id) ? 1 : 0);
|
|
1681
|
+
if (hasOtherRuntimeHolders())
|
|
1682
|
+
return false;
|
|
1569
1683
|
// An attached terminal session owns the running container exactly like a
|
|
1570
1684
|
// ready preview does.
|
|
1571
1685
|
if ((this.terminalHolds.get(task.id) ?? 0) > 0)
|
|
@@ -1602,10 +1716,11 @@ export class WorkRuntimeService {
|
|
|
1602
1716
|
catch (error) {
|
|
1603
1717
|
logger.warn(`Could not verify preview state before idling Work container ${task.containerName}; stopping it:`, error);
|
|
1604
1718
|
}
|
|
1605
|
-
// The preview probe above awaits a container exec; a
|
|
1606
|
-
// hold can land during that window. Re-check synchronously
|
|
1607
|
-
//
|
|
1719
|
+
// The preview probe above awaits a container exec; a runtime lease or
|
|
1720
|
+
// terminal/screen hold can land during that window. Re-check synchronously
|
|
1721
|
+
// so newly acquired holds do not lose the sandbox to a stale decision.
|
|
1608
1722
|
if ((!options.ignoreActiveCommands && this.activeCommands.has(task.id)) ||
|
|
1723
|
+
hasOtherRuntimeHolders() ||
|
|
1609
1724
|
(this.terminalHolds.get(task.id) ?? 0) > 0 ||
|
|
1610
1725
|
(this.screenHolds.get(task.id) ?? 0) > 0) {
|
|
1611
1726
|
return false;
|
|
@@ -2155,12 +2270,21 @@ export class WorkRuntimeService {
|
|
|
2155
2270
|
// idempotent, and lifecycle transitions stay lease-guarded.
|
|
2156
2271
|
if (error instanceof WorkRuntimeError &&
|
|
2157
2272
|
error.code === 'WORK_RUNTIME_LEASE_CONFLICT') {
|
|
2158
|
-
await this.
|
|
2159
|
-
|
|
2160
|
-
await this.
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2273
|
+
const releasePresence = await this.acquireViewerPresence(task.id);
|
|
2274
|
+
try {
|
|
2275
|
+
return await this.withLifecycleLock(task.id, async (_assertHeld, signal) => {
|
|
2276
|
+
await this.assertTaskIsActive(task);
|
|
2277
|
+
if ((await this.driver.runtimeState(task)) !== 'running') {
|
|
2278
|
+
throw error;
|
|
2279
|
+
}
|
|
2280
|
+
await this.startComputerInContainer(task, signal);
|
|
2281
|
+
const value = await fn(signal);
|
|
2282
|
+
this.noteTaskActivity(task.id);
|
|
2283
|
+
return value;
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
finally {
|
|
2287
|
+
releasePresence();
|
|
2164
2288
|
}
|
|
2165
2289
|
}
|
|
2166
2290
|
throw error;
|
|
@@ -2213,7 +2337,13 @@ export class WorkRuntimeService {
|
|
|
2213
2337
|
throw new WorkRuntimeError('Wait for the active command to finish before starting a preview.', 409, 'WORK_PREVIEW_COMMAND_ACTIVE');
|
|
2214
2338
|
}
|
|
2215
2339
|
await assertHeld();
|
|
2340
|
+
const startingLease = this.runtimeLeases.get(task.id);
|
|
2341
|
+
if (startingLease) {
|
|
2342
|
+
startingLease.previewGeneration =
|
|
2343
|
+
(startingLease.previewGeneration ?? 0) + 1;
|
|
2344
|
+
}
|
|
2216
2345
|
await hooks.onStarting?.();
|
|
2346
|
+
let registeredPreview = false;
|
|
2217
2347
|
try {
|
|
2218
2348
|
await this.prepareWithLock(task, signal);
|
|
2219
2349
|
await this.assertTaskIsActive(task);
|
|
@@ -2228,28 +2358,53 @@ export class WorkRuntimeService {
|
|
|
2228
2358
|
await this.assertTaskIsActive(task);
|
|
2229
2359
|
await assertHeld();
|
|
2230
2360
|
await hooks.onRunning?.(preview.url, preview.endpoint);
|
|
2361
|
+
// Invalidate stopped snapshots begun during startup as well as
|
|
2362
|
+
// those begun before it, including a reused preview callback.
|
|
2363
|
+
if (startingLease) {
|
|
2364
|
+
startingLease.previewGeneration =
|
|
2365
|
+
(startingLease.previewGeneration ?? 0) + 1;
|
|
2366
|
+
}
|
|
2367
|
+
if (this.previewLeaseReleases.has(task.id)) {
|
|
2368
|
+
releaseLease();
|
|
2369
|
+
}
|
|
2370
|
+
else {
|
|
2371
|
+
this.previewLeaseReleases.set(task.id, releaseLease);
|
|
2372
|
+
registeredPreview = true;
|
|
2373
|
+
this.previewUsageReleases.set(task.id, await workUsageService.begin(task.id, 'preview', task.userId));
|
|
2374
|
+
}
|
|
2375
|
+
const lease = this.runtimeLeases.get(task.id);
|
|
2376
|
+
if (lease)
|
|
2377
|
+
await this.refreshRuntimePresence(task, lease);
|
|
2378
|
+
leaseRetained = true;
|
|
2231
2379
|
return preview.url;
|
|
2232
2380
|
}
|
|
2233
2381
|
catch (error) {
|
|
2234
|
-
|
|
2235
|
-
|
|
2382
|
+
if (registeredPreview && !this.hasLocalRuntimeActivity(task.id)) {
|
|
2383
|
+
// Retain ownership and the startup lifecycle lock until rollback
|
|
2384
|
+
// has stopped the sandbox. Never stop a newly active run holder.
|
|
2385
|
+
try {
|
|
2386
|
+
await this.stopPreviewPrepared(task, signal);
|
|
2387
|
+
await assertHeld();
|
|
2388
|
+
await this.markPreviewStopped(task.id);
|
|
2389
|
+
}
|
|
2390
|
+
catch (cleanupError) {
|
|
2391
|
+
logger.warn(`Could not clean up failed Work preview ${task.id}:`, cleanupError);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
try {
|
|
2395
|
+
await hooks.onFailed?.();
|
|
2396
|
+
}
|
|
2397
|
+
finally {
|
|
2398
|
+
this.releasePreviewLease(task.id);
|
|
2399
|
+
}
|
|
2236
2400
|
throw error;
|
|
2237
2401
|
}
|
|
2238
2402
|
});
|
|
2239
|
-
if (this.previewLeaseReleases.has(task.id)) {
|
|
2240
|
-
releaseLease();
|
|
2241
|
-
}
|
|
2242
|
-
else {
|
|
2243
|
-
this.previewLeaseReleases.set(task.id, releaseLease);
|
|
2244
|
-
this.previewUsageReleases.set(task.id, await workUsageService.begin(task.id, 'preview', task.userId));
|
|
2245
|
-
}
|
|
2246
|
-
leaseRetained = true;
|
|
2247
2403
|
return url;
|
|
2248
2404
|
}
|
|
2249
2405
|
finally {
|
|
2250
|
-
if (!leaseRetained)
|
|
2406
|
+
if (!leaseRetained)
|
|
2251
2407
|
releaseLease();
|
|
2252
|
-
}
|
|
2253
2408
|
}
|
|
2254
2409
|
}
|
|
2255
2410
|
async detectPreviewLaunch(task, signal) {
|
|
@@ -2424,6 +2579,10 @@ export class WorkRuntimeService {
|
|
|
2424
2579
|
if (this.activeCommands.has(task.id))
|
|
2425
2580
|
return false;
|
|
2426
2581
|
const state = await this.previewProcessCheckWithLock(task, signal);
|
|
2582
|
+
const sharedPreview = getPlatformRuntimeConfig().mode === 'team' &&
|
|
2583
|
+
(await withCoordinationTimeout(getCoordinator().listPresence(`work-task-active:${task.id}`), SHARED_COORDINATION_OPERATION_TIMEOUT_MS)).some(member => member !== this.activityMemberId);
|
|
2584
|
+
if (sharedPreview)
|
|
2585
|
+
return state === 'ready';
|
|
2427
2586
|
if (state === 'ready' && this.previewLeaseReleases.has(task.id)) {
|
|
2428
2587
|
return true;
|
|
2429
2588
|
}
|