fraim-hub 2.0.239 → 2.0.240

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.
@@ -341,6 +341,26 @@ function scheduledFireBucketMs(cronExpr, nowMs = Date.now()) {
341
341
  const bucketMs = fieldCount === 6 ? 1000 : 60 * 1000;
342
342
  return Math.floor(nowMs / bucketMs) * bucketMs;
343
343
  }
344
+ function exactSixFieldCronDateMs(cronExpr, nowMs = Date.now()) {
345
+ const fields = cronExpr.trim().split(/\s+/).filter(Boolean);
346
+ if (fields.length !== 6 || fields[5] !== '*')
347
+ return null;
348
+ const [second, minute, hour, day, month] = fields.slice(0, 5).map((field) => Number(field));
349
+ if (![second, minute, hour, day, month].every(Number.isInteger))
350
+ return null;
351
+ const now = new Date(nowMs);
352
+ const candidate = new Date(now.getFullYear(), month - 1, day, hour, minute, second, 0);
353
+ if (candidate.getMonth() !== month - 1 ||
354
+ candidate.getDate() !== day ||
355
+ candidate.getHours() !== hour ||
356
+ candidate.getMinutes() !== minute ||
357
+ candidate.getSeconds() !== second)
358
+ return null;
359
+ if (candidate.getTime() >= nowMs - SCHEDULED_FIRE_LEASE_TTL_MS)
360
+ return candidate.getTime();
361
+ const nextYear = new Date(now.getFullYear() + 1, month - 1, day, hour, minute, second, 0);
362
+ return nextYear.getTime();
363
+ }
344
364
  class DeploymentStore {
345
365
  constructor(filePath) {
346
366
  this.filePath = filePath ?? path_1.default.join(getUserHubDir(), 'hub-deployments.json');
@@ -1635,6 +1655,7 @@ class AiHubServer {
1635
1655
  // never touches a database. Persona and manager-team state resolve from the hosted server
1636
1656
  // through the remote gateway; the hosted server is the sole owner of DB access.
1637
1657
  this.remoteGateway = options.remoteGateway ?? new remote_hub_gateway_1.HttpHubRemoteGateway();
1658
+ this.deploymentStoreProvided = Boolean(options.deploymentStore);
1638
1659
  this.deploymentStore = options.deploymentStore ?? new DeploymentStore();
1639
1660
  this.hostConfigStore = options.hostConfigStore ?? new HostConfigStore();
1640
1661
  this.app.use(express_1.default.json({ limit: '10mb' }));
@@ -1846,8 +1867,12 @@ class AiHubServer {
1846
1867
  void (0, hosts_1.detectEmployeesAsync)({ force: true }).catch((error) => {
1847
1868
  console.warn('[ai-hub] agent availability priming failed:', error?.message || error);
1848
1869
  });
1849
- // Issue #578: rehydrate active scheduled deployments from disk.
1850
- this.rehydrateScheduledDeployments();
1870
+ // Issue #578: rehydrate active scheduled deployments from disk. Test and preview
1871
+ // servers that inject a fake host but not a deployment store should not run the
1872
+ // user's real scheduled deployments from the default store.
1873
+ if (this.deploymentStoreProvided || this.hostRuntime instanceof hosts_1.CliHostRuntime) {
1874
+ this.rehydrateScheduledDeployments();
1875
+ }
1851
1876
  // Start HTTPS server when a cert bundle and port are provided.
1852
1877
  // Word Online requires HTTPS; the HTTPS server shares the same Express app
1853
1878
  // so all routes (including /word-taskpane/*) are available over both protocols.
@@ -5394,20 +5419,48 @@ class AiHubServer {
5394
5419
  console.warn(`[ai-hub] invalid cronExpr for deployment ${deployment.id}: ${deployment.cronExpr}`);
5395
5420
  return;
5396
5421
  }
5397
- const task = cron.schedule(deployment.cronExpr, async () => {
5422
+ const fireScheduledDeployment = async (fireTimeMs = scheduledFireBucketMs(deployment.cronExpr || '')) => {
5398
5423
  try {
5399
- const fireTimeMs = scheduledFireBucketMs(deployment.cronExpr || '');
5400
5424
  if (!this.deploymentStore.claimScheduledFire(deployment.id, fireTimeMs)) {
5401
5425
  console.log(`[ai-hub] scheduled deployment ${deployment.id} skipped - scheduled fire already claimed`);
5402
5426
  return;
5403
5427
  }
5428
+ if (process.env.FRAIM_DEBUG_SCHEDULER === '1') {
5429
+ console.log('[ai-hub] debug claimed scheduled fire', deployment.id, deployment.hostId, deployment.activeRunId || null);
5430
+ }
5404
5431
  await this.fireDeploymentRun(deployment);
5405
5432
  }
5406
5433
  catch (err) {
5407
5434
  console.warn(`[ai-hub] scheduled deployment ${deployment.id} fire failed:`, err);
5408
5435
  }
5436
+ };
5437
+ const task = cron.schedule(deployment.cronExpr, () => { void fireScheduledDeployment(); });
5438
+ const exactFireMs = exactSixFieldCronDateMs(deployment.cronExpr);
5439
+ const exactFireNearDue = exactFireMs !== null && exactFireMs - Date.now() <= 5_000;
5440
+ if (exactFireNearDue) {
5441
+ setTimeout(() => { void fireScheduledDeployment(exactFireMs); }, 0);
5442
+ }
5443
+ const exactTimer = exactFireMs !== null && !exactFireNearDue
5444
+ ? setTimeout(() => { void fireScheduledDeployment(exactFireMs); }, Math.max(0, exactFireMs - Date.now()))
5445
+ : null;
5446
+ const exactInterval = exactFireMs !== null && !exactFireNearDue
5447
+ ? setInterval(() => {
5448
+ if (Date.now() < exactFireMs)
5449
+ return;
5450
+ if (exactInterval)
5451
+ clearInterval(exactInterval);
5452
+ void fireScheduledDeployment(exactFireMs);
5453
+ }, 250)
5454
+ : null;
5455
+ this.cronHandles.set(deployment.id, {
5456
+ stop: () => {
5457
+ task.stop();
5458
+ if (exactTimer)
5459
+ clearTimeout(exactTimer);
5460
+ if (exactInterval)
5461
+ clearInterval(exactInterval);
5462
+ },
5409
5463
  });
5410
- this.cronHandles.set(deployment.id, task);
5411
5464
  }
5412
5465
  catch (err) {
5413
5466
  console.warn('[ai-hub] node-cron not available — scheduled deployments require node-cron:', err);
@@ -5453,6 +5506,9 @@ class AiHubServer {
5453
5506
  // Pre-register before startRun so synchronous onEvent calls (e.g. FakeHostRuntime)
5454
5507
  // can call runRegistry.update without "Run not found" throws.
5455
5508
  this.runRegistry.create(run, {});
5509
+ if (process.env.FRAIM_DEBUG_SCHEDULER === '1') {
5510
+ console.log('[ai-hub] debug starting deployment run', deployment.id, hostId, deployment.projectPath);
5511
+ }
5456
5512
  const child = this.hostRuntime.startRun(hostId, deployment.projectPath, instructions, {
5457
5513
  onEvent: (event, channel) => {
5458
5514
  this.runRegistry.update(run.id, (current) => {
@@ -117,7 +117,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
117
117
  personaKey: 'ashley',
118
118
  bundleId: 'persona-ashley-core',
119
119
  catalogMetadata: buildCatalogMetadata('ashley', ['chief-of-staff-briefing', 'executive-assistant', 'analyze-transcript']),
120
- protectedJobs: ['chief-of-staff-briefing', 'calendar-triage', 'meeting-preparation', 'executive-assistant', 'send-newsletter', 'send-thank-you-notes', 'analyze-transcript'],
120
+ protectedJobs: ['chief-of-staff-briefing', 'calendar-triage', 'meeting-preparation', 'executive-assistant', 'send-newsletter', 'send-thank-you-notes', 'analyze-transcript', 'travel-planning', 'travel-disruption-rebooking'],
121
121
  protectedAliases: ['executive-assistant', 'operations-assistant'],
122
122
  defaultHireMode: 'job',
123
123
  lockCopy: 'Hire AshLey to unlock executive-assistant work for this request.'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-hub",
3
- "version": "2.0.239",
3
+ "version": "2.0.240",
4
4
  "description": "FRAIM Hub local companion package.",
5
5
  "bin": {
6
6
  "fraim-hub": "bin/fraim-hub.js",
@@ -161,7 +161,7 @@
161
161
  "electron": "^41.2.2",
162
162
  "electron-updater": "^6.8.9",
163
163
  "express": "^5.2.1",
164
- "fraim": "2.0.239",
164
+ "fraim": "2.0.240",
165
165
  "mongodb": "^7.0.0",
166
166
  "node-cron": "4.2.1",
167
167
  "node-edge-tts": "^1.2.10",