forkit-connect 0.1.0 → 0.1.3

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/dist/cli.js CHANGED
@@ -38,6 +38,7 @@ const CLI_FALLBACK_PLAN_LIMITS = {
38
38
  draftPassports: 0,
39
39
  maxWorkspaces: 0,
40
40
  maxProjects: 0,
41
+ maxGovernedPassports: 0,
41
42
  runtimeSignalsPerMonth: null,
42
43
  },
43
44
  signal: {
@@ -45,6 +46,7 @@ const CLI_FALLBACK_PLAN_LIMITS = {
45
46
  draftPassports: 25,
46
47
  maxWorkspaces: 3,
47
48
  maxProjects: 3,
49
+ maxGovernedPassports: 3,
48
50
  runtimeSignalsPerMonth: 10000,
49
51
  },
50
52
  protocol: {
@@ -52,6 +54,7 @@ const CLI_FALLBACK_PLAN_LIMITS = {
52
54
  draftPassports: 20,
53
55
  maxWorkspaces: null,
54
56
  maxProjects: null,
57
+ maxGovernedPassports: null,
55
58
  runtimeSignalsPerMonth: 250000,
56
59
  },
57
60
  sovereign: {
@@ -59,6 +62,7 @@ const CLI_FALLBACK_PLAN_LIMITS = {
59
62
  draftPassports: null,
60
63
  maxWorkspaces: null,
61
64
  maxProjects: null,
65
+ maxGovernedPassports: null,
62
66
  runtimeSignalsPerMonth: null,
63
67
  },
64
68
  };
@@ -74,6 +78,7 @@ const PUBLIC_COMMANDS = [
74
78
  ['inbox', 'Review the Smart Registration Inbox'],
75
79
  ['sync', 'Flush queued drafts and lifecycle metadata'],
76
80
  ['workspace', 'List, select, or inspect optional governed workspace/project scope'],
81
+ ['runtime', 'Review local runtime discovery and runtime health'],
77
82
  ['register', 'Register ready local models into the current scope'],
78
83
  ['ignore', 'Ignore one detected local model'],
79
84
  ['doctor', 'Run local environment diagnostics'],
@@ -101,11 +106,12 @@ const ADVANCED_COMMAND_GROUPS = [
101
106
  ['notify', 'Notification preview and delivery controls'],
102
107
  ];
103
108
  function usage() {
104
- console.log('Usage: forkit-connect <init|login|logout|status|changes|start|stop|scan|inbox|sync|workspace|register|ignore|doctor> [options]');
109
+ console.log('Usage: forkit-connect <init|login|logout|status|changes|start|stop|scan|inbox|sync|workspace|runtime|register|ignore|doctor> [options]');
105
110
  console.log(' forkit-connect workspace <list|select|create|status> [options]');
111
+ console.log(' forkit-connect runtime <review|status> [options]');
106
112
  console.log('Public commands:');
107
113
  for (const [command, description] of PUBLIC_COMMANDS) {
108
- console.log(` ${command.padEnd(7)} ${description}`);
114
+ console.log(` ${command.padEnd(10)} ${description}`);
109
115
  }
110
116
  console.log('Options:');
111
117
  console.log(' --description <text> Optional workspace description used by workspace create');
@@ -151,6 +157,22 @@ function advancedUsage() {
151
157
  console.log(' --heartbeat-gaid <gaid> Queue heartbeat runtime signal event for GAID');
152
158
  console.log(' --heartbeat-key <key> API key used for heartbeat runtime signal event');
153
159
  console.log(' Also used by: c2 set-key (stores key + backfills events)');
160
+ console.log(' --gaid <gaid> Passport GAID used by c2 run-log emit');
161
+ console.log(' --api-key <key> Runtime signal API key used by c2 run-log emit');
162
+ console.log(' --provider <name> Provider label used by c2 run-log emit');
163
+ console.log(' --service-name <name> Service/agent/workflow label used by c2 run-log emit');
164
+ console.log(' --prompt-tokens <n> Prompt tokens used by c2 run-log emit');
165
+ console.log(' --completion-tokens <n> Completion tokens used by c2 run-log emit');
166
+ console.log(' --client-name <name> Optional repo/runtime client label used by c2 run-log emit');
167
+ console.log(' --actor-labels <csv> Actor labels used by c2 run-log emit (for example: Codex,Claude)');
168
+ console.log(' --task-labels <csv> Task or chat labels used by c2 run-log emit');
169
+ console.log(' --folder-labels <csv> Relative folder labels used by c2 run-log emit');
170
+ console.log(' --file-labels <csv> Relative file labels used by c2 run-log emit');
171
+ console.log(' --model-labels <csv> Optional model labels used by c2 run-log emit');
172
+ console.log(' --cpu-percent <n> Scoped CPU percentage used by c2 run-log emit');
173
+ console.log(' --memory-mb <n> Scoped memory usage used by c2 run-log emit');
174
+ console.log(' --vram-mb <n> Device VRAM usage used by c2 run-log emit');
175
+ console.log(' --draft-only Allow draft creation even when governed publish capacity is full');
154
176
  }
155
177
  function showUsage() {
156
178
  if (hasFlag('--advanced-help')) {
@@ -169,6 +191,13 @@ function getArg(flag) {
169
191
  function hasFlag(flag) {
170
192
  return process.argv.slice(2).includes(flag);
171
193
  }
194
+ function getNumericArg(flag) {
195
+ const value = getArg(flag);
196
+ if (value === null)
197
+ return undefined;
198
+ const parsed = Number(value);
199
+ return Number.isFinite(parsed) ? parsed : undefined;
200
+ }
172
201
  function isHelpCommand(command) {
173
202
  return command === 'help' || command === '--help' || command === '-h' || command === '--advanced-help';
174
203
  }
@@ -182,6 +211,20 @@ function sleep(ms) {
182
211
  delay(resolve, ms);
183
212
  });
184
213
  }
214
+ async function withTimeout(promise, timeoutMs, fallbackValue) {
215
+ return new Promise((resolve) => {
216
+ const timeout = setTimeout(() => resolve(fallbackValue), Math.max(0, timeoutMs));
217
+ promise
218
+ .then((value) => {
219
+ clearTimeout(timeout);
220
+ resolve(value);
221
+ })
222
+ .catch(() => {
223
+ clearTimeout(timeout);
224
+ resolve(fallbackValue);
225
+ });
226
+ });
227
+ }
185
228
  function isDeviceConnectStartResponse(body) {
186
229
  if (!body || typeof body !== 'object')
187
230
  return false;
@@ -220,12 +263,11 @@ function printDeviceLoginInstructions(start) {
220
263
  console.log('[forkit-connect] Keep this terminal open while Forkit Connect waits for approval.');
221
264
  }
222
265
  function printSessionExportFallback(token) {
266
+ void token;
223
267
  console.log('[forkit-connect] Approval succeeded, but secure credential storage is unavailable in this Linux session.');
224
268
  console.log('[forkit-connect] Forkit Connect can keep using this approved session in the current interactive run.');
225
- console.log('[forkit-connect] To keep working in this terminal right now, export the session reference manually:');
226
- console.log(`export FORKIT_CONNECT_SESSION_REF='${token}'`);
227
- console.log('[forkit-connect] Then rerun: forkit-connect status');
228
- console.log('[forkit-connect] For a persistent and safer setup, install libsecret-tools and run forkit-connect login again.');
269
+ console.log('[forkit-connect] For persistent login, install libsecret-tools and run forkit-connect login again.');
270
+ console.log('[forkit-connect] Headless automation may pass FORKIT_CONNECT_SESSION_REF explicitly, but Connect will not print session tokens.');
229
271
  }
230
272
  function hasLinuxGuiSession() {
231
273
  if (process.platform !== 'linux') {
@@ -300,6 +342,9 @@ function formatRemainingLimit(limit, used, singular, plural = `${singular}s`) {
300
342
  const remaining = Math.max(safeLimit - safeUsed, 0);
301
343
  return `${remaining} left (${safeUsed}/${safeLimit} used)`;
302
344
  }
345
+ function isCapacityExhausted(limit, remaining) {
346
+ return Number.isFinite(limit) && Number(remaining) <= 0;
347
+ }
303
348
  function formatWorkspaceAccessLine(workspace) {
304
349
  const workspaceId = String(workspace.id || workspace.gaid || workspace.passportGaid || 'unknown');
305
350
  return `- ${summarizeWorkspaceLabel(workspace)} | id=${workspaceId}`;
@@ -577,6 +622,11 @@ function printJson(value) {
577
622
  console.log(JSON.stringify(value, null, 2));
578
623
  }
579
624
  const INTERACTIVE_LABEL_WIDTH = 24;
625
+ const INTERACTIVE_DISCOVERY_TIMEOUT_MS = 800;
626
+ const INTERACTIVE_BINDING_TIMEOUT_MS = 800;
627
+ const INTERACTIVE_ACCOUNT_LIMITS_TIMEOUT_MS = 700;
628
+ const SESSION_STATE_CHECK_TIMEOUT_MS = 3000;
629
+ const STATUS_BINDING_TIMEOUT_MS = 800;
580
630
  function canRenderInteractiveShell() {
581
631
  return Boolean(node_process_1.stdin.isTTY && node_process_1.stdout.isTTY);
582
632
  }
@@ -628,11 +678,8 @@ function shellListLine(value) {
628
678
  return `• ${value}`;
629
679
  }
630
680
  function buildInteractiveOverviewSections(service, sessionState, accountLimits) {
631
- const overview = service.getConnectStatusOverview();
632
- const inbox = service.buildSmartRegistrationInbox();
633
- const leftoverReady = inbox.groups.ready_to_connect.slice(0, 4).map((item) => shellListLine(item.display_name));
634
- const leftoverNeedsConfirmation = inbox.groups.needs_confirmation.slice(0, 4).map((item) => shellListLine(item.display_name));
635
- const accountTrusted = sessionState === 'authorized' || sessionState === 'unavailable';
681
+ const accountTrusted = sessionState === 'authorized';
682
+ const overview = service.getConnectStatusOverview({ includeInbox: false });
636
683
  const preparedWorkspace = accountTrusted ? String(overview.workspace_id || '').trim() : '';
637
684
  const preparedProject = accountTrusted ? String(overview.project_id || '').trim() : '';
638
685
  // Base section: always visible
@@ -673,6 +720,9 @@ function buildInteractiveOverviewSections(service, sessionState, accountLimits)
673
720
  lines: [
674
721
  shellLine('Plan', accountLimits.planName),
675
722
  shellLine('Private passports', formatRemainingLimit(accountLimits.privatePassportsLimit, accountLimits.privatePassportsUsed, 'private passport')),
723
+ ...(accountLimits.governedPassportsLimit !== null || accountLimits.governedPassportsUsed !== null
724
+ ? [shellLine('Governed passports', formatRemainingLimit(accountLimits.governedPassportsLimit, accountLimits.governedPassportsUsed, 'governed passport'))]
725
+ : []),
676
726
  shellLine('Drafts', formatRemainingLimit(accountLimits.draftLimit, accountLimits.draftsUsed, 'draft')),
677
727
  shellLine('Workspaces', formatRemainingLimit(accountLimits.workspaceLimit, accountLimits.workspacesUsed, 'workspace')),
678
728
  shellLine('Projects', formatRemainingLimit(accountLimits.projectLimit, accountLimits.projectsUsed, 'project')),
@@ -687,10 +737,8 @@ function buildInteractiveOverviewSections(service, sessionState, accountLimits)
687
737
  lines: [
688
738
  shellLine('Workspace', formatScopeReferenceLabel(preparedWorkspace || null, 'workspace')),
689
739
  shellLine('Project', formatScopeReferenceLabel(preparedProject || null, 'project')),
690
- shellLine('Ready to connect', inbox.groups.ready_to_connect.length),
691
- shellLine('Needs confirmation', inbox.groups.needs_confirmation.length),
692
- ...leftoverReady,
693
- ...leftoverNeedsConfirmation,
740
+ shellLine('Ready to connect', overview.ready_to_connect_count),
741
+ shellLine('Needs confirmation', overview.needs_confirmation_count),
694
742
  ],
695
743
  });
696
744
  }
@@ -1081,22 +1129,24 @@ function printAgentLedger(summary) {
1081
1129
  console.log(JSON.stringify(summary, null, 2));
1082
1130
  }
1083
1131
  function printRuntimeReview(summary) {
1084
- console.log(`[forkit-connect] Runtime review items=${summary.total_runtimes}`);
1132
+ console.log('[forkit-connect] Runtime review');
1133
+ console.log(`- runtimes=${summary.total_runtimes}`);
1085
1134
  for (const runtime of summary.runtimes) {
1086
- console.log(`- runtime=${runtime.runtime_name} | type=${runtime.runtime_type} | status=${runtime.status} | gaid=${shortId(runtime.runtime_gaid)} | linked_models=${runtime.linked_models_count} | linked_agents=${runtime.linked_agents_count} | health=${runtime.health_status} | next=${runtime.recommended_action}`);
1135
+ console.log(`- ${runtime.runtime_name} | ${runtime.status} | linked models=${runtime.linked_models_count} | linked agents=${runtime.linked_agents_count} | health=${runtime.health_status} | next=${runtime.recommended_action}`);
1087
1136
  }
1088
1137
  }
1089
1138
  function printRuntimeStatus(status) {
1090
- console.log(JSON.stringify({
1091
- total_runtimes: status.total_runtimes,
1092
- online_runtimes: status.online_runtimes,
1093
- offline_runtimes: status.offline_runtimes,
1094
- linked_runtimes: status.linked_runtimes,
1095
- unlinked_runtimes: status.unlinked_runtimes,
1096
- unhealthy_runtimes: status.unhealthy_runtimes,
1097
- latest_runtime_lifecycle_event: status.latest_runtime_lifecycle_event,
1098
- c2_pending_count: status.c2_pending_count,
1099
- }, null, 2));
1139
+ console.log('[forkit-connect] Runtime status');
1140
+ console.log(`- total=${status.total_runtimes}`);
1141
+ console.log(`- online=${status.online_runtimes}`);
1142
+ console.log(`- offline=${status.offline_runtimes}`);
1143
+ console.log(`- linked=${status.linked_runtimes}`);
1144
+ console.log(`- unlinked=${status.unlinked_runtimes}`);
1145
+ console.log(`- needs attention=${status.unhealthy_runtimes}`);
1146
+ console.log(`- pending runtime sync=${status.c2_pending_count}`);
1147
+ if (status.latest_runtime_lifecycle_event) {
1148
+ console.log(`- latest event=${status.latest_runtime_lifecycle_event}`);
1149
+ }
1100
1150
  }
1101
1151
  function formatSmartInboxActionValue(action, itemType, connectableModelName) {
1102
1152
  switch (action) {
@@ -1172,18 +1222,14 @@ function printConnectStatusOverview(status) {
1172
1222
  }
1173
1223
  function printPublicStatusOverview(status) {
1174
1224
  console.log('[forkit-connect] Status');
1175
- console.log(`- device_paired=${String(status.device_paired)}`);
1176
- console.log(`- workspace=${status.workspace_id || 'not selected'}`);
1177
- console.log(`- project=${status.project_id || 'not selected'}`);
1225
+ console.log(`- device=${status.device_paired ? 'paired' : 'approval pending'}`);
1226
+ console.log(`- scope=${status.workspace_id && status.project_id ? `${status.workspace_id} / ${status.project_id}` : 'not selected'}`);
1178
1227
  console.log(`- daemon=${status.daemon_status}`);
1179
- console.log(`- models_discovered=${status.models_discovered}`);
1180
- console.log(`- agents_discovered=${status.agents_discovered}`);
1181
- console.log(`- runtimes_discovered=${status.runtimes_discovered}`);
1182
- console.log(`- ready_to_connect=${status.ready_to_connect_count}`);
1183
- console.log(`- needs_confirmation=${status.needs_confirmation_count}`);
1184
- console.log(`- connected=${status.connected_count}`);
1185
- console.log(`- c2_sync_pending=${status.c2_sync_pending}`);
1186
- console.log(`- privacy_mode=${status.privacy_mode}`);
1228
+ console.log(`- local inventory=models ${status.models_discovered} · agents ${status.agents_discovered} · runtimes ${status.runtimes_discovered}`);
1229
+ console.log(`- review queue=ready ${status.ready_to_connect_count} · needs review ${status.needs_confirmation_count}`);
1230
+ console.log(`- connected records=${status.connected_count}`);
1231
+ console.log(`- pending runtime sync=${status.c2_sync_pending}`);
1232
+ console.log(`- privacy=${status.privacy_mode}`);
1187
1233
  if (status.lifecycle_note) {
1188
1234
  console.log(`- note=${status.lifecycle_note}`);
1189
1235
  }
@@ -1192,6 +1238,14 @@ function printPublicStatusOverview(status) {
1192
1238
  }
1193
1239
  }
1194
1240
  function printPublicStatusGuidance(status, sessionState) {
1241
+ const accountLabel = sessionState === 'authorized'
1242
+ ? 'connected'
1243
+ : sessionState === 'expired'
1244
+ ? 'expired'
1245
+ : sessionState === 'unavailable'
1246
+ ? 'unverified'
1247
+ : 'login required';
1248
+ console.log(`- account=${accountLabel}`);
1195
1249
  if (sessionState === 'missing') {
1196
1250
  console.log('- note=Local discovery is working. Sign in next to pair this device with your Forkit.dev account.');
1197
1251
  console.log('- next=run forkit-connect login');
@@ -1227,7 +1281,9 @@ async function checkBackendSessionState(service) {
1227
1281
  baseUrl: DEFAULT_BASE_URL,
1228
1282
  sessionRef: sessionRefValue,
1229
1283
  });
1230
- const result = await api.getProfileAccess();
1284
+ const result = await withTimeout(api.getProfileAccess(), SESSION_STATE_CHECK_TIMEOUT_MS, null);
1285
+ if (!result)
1286
+ return 'unavailable';
1231
1287
  if (result.ok)
1232
1288
  return 'authorized';
1233
1289
  if (result.status === 401 || result.status === 403)
@@ -1434,18 +1490,27 @@ async function run() {
1434
1490
  const runPublicConnectInit = () => {
1435
1491
  printConnectInit(service.initializeConnectIdentity());
1436
1492
  };
1493
+ const withSmartInboxSnapshotCounts = (overview) => {
1494
+ const inbox = service.getSmartRegistrationInbox({
1495
+ preferSnapshot: true,
1496
+ refreshInBackground: false,
1497
+ });
1498
+ return {
1499
+ ...overview,
1500
+ ready_to_connect_count: inbox.summary.ready_to_connect_count,
1501
+ needs_confirmation_count: inbox.summary.needs_confirmation_count,
1502
+ connected_count: inbox.summary.connected_count,
1503
+ next_recommended_action: inbox.summary.next_recommended_action,
1504
+ };
1505
+ };
1437
1506
  const runPublicConnectStatus = async () => {
1438
1507
  const sessionState = await checkBackendSessionState(service);
1508
+ const secureStorage = service.getCredentialStoreStatus();
1439
1509
  if (service.readSessionRef()) {
1440
- try {
1441
- await service.refreshEffectiveBinding();
1442
- }
1443
- catch {
1444
- // Status remains useful with the last local binding snapshot.
1445
- }
1510
+ await withTimeout(service.refreshEffectiveBinding(), STATUS_BINDING_TIMEOUT_MS, undefined);
1446
1511
  }
1447
- const overview = service.getConnectStatusOverview();
1448
- const accountTrusted = sessionState === 'authorized' || sessionState === 'unavailable';
1512
+ const accountTrusted = sessionState === 'authorized';
1513
+ const overview = withSmartInboxSnapshotCounts(service.getConnectStatusOverview({ includeInbox: false }));
1449
1514
  const localScopeCached = Boolean(String(overview.workspace_id || '').trim() || String(overview.project_id || '').trim());
1450
1515
  const displayOverview = accountTrusted
1451
1516
  ? overview
@@ -1466,23 +1531,45 @@ async function run() {
1466
1531
  session_truth: accountTrusted ? 'account_verified_or_offline' : 'local_scope_cached_login_required',
1467
1532
  local_scope_cached: !accountTrusted && localScopeCached,
1468
1533
  binding_truth: accountTrusted ? 'account_binding_active' : 'account_login_required',
1534
+ secure_storage: {
1535
+ backend: secureStorage.backend,
1536
+ available: secureStorage.available,
1537
+ plaintext_fallback_active: secureStorage.plaintextFallbackActive,
1538
+ legacy_plaintext_file_present: secureStorage.legacyPlaintextFilePresent,
1539
+ detail: secureStorage.detail,
1540
+ },
1469
1541
  });
1470
1542
  return;
1471
1543
  }
1472
1544
  printPublicStatusOverview(displayOverview);
1473
- console.log(`- session=${sessionState}`);
1474
- console.log(`- binding_truth=${accountTrusted ? 'account_binding_active' : 'account_login_required'}`);
1475
- if (!accountTrusted && localScopeCached) {
1476
- console.log('- local_scope_cached=true');
1545
+ console.log(`- secure storage=${secureStorage.backend}`);
1546
+ if (!secureStorage.available || secureStorage.plaintextFallbackActive) {
1547
+ console.log(`- secure storage detail=${secureStorage.detail}`);
1477
1548
  }
1478
1549
  printPublicStatusGuidance(displayOverview, sessionState);
1479
1550
  };
1480
- const runPublicConnectInbox = () => {
1481
- const inbox = service.buildSmartRegistrationInbox();
1551
+ const runPublicConnectInbox = async () => {
1552
+ const forceRefresh = hasFlag('--refresh');
1553
+ const inbox = service.getSmartRegistrationInbox({
1554
+ forceRefresh,
1555
+ preferSnapshot: !forceRefresh,
1556
+ refreshInBackground: !forceRefresh,
1557
+ });
1482
1558
  if (hasFlag('--json')) {
1483
1559
  printJson(inbox);
1484
1560
  return;
1485
1561
  }
1562
+ const freshness = inbox.summary.freshness_state ?? 'fresh';
1563
+ const ageSeconds = Number.isFinite(inbox.summary.snapshot_age_seconds)
1564
+ ? Number(inbox.summary.snapshot_age_seconds)
1565
+ : 0;
1566
+ console.log(`[forkit-connect] Inbox snapshot freshness=${freshness} age_seconds=${ageSeconds}`);
1567
+ if (freshness === 'stale') {
1568
+ console.log('[forkit-connect] Snapshot is stale. Run `forkit-connect inbox --refresh` for immediate reconcile.');
1569
+ }
1570
+ else if (freshness === 'syncing') {
1571
+ console.log('[forkit-connect] Background reconcile is running; results will self-refresh after completion.');
1572
+ }
1486
1573
  printSmartInbox(inbox);
1487
1574
  };
1488
1575
  const runPublicCollectedChanges = () => {
@@ -1571,10 +1658,11 @@ async function run() {
1571
1658
  const runPublicLogout = () => {
1572
1659
  const currentSessionRef = String(service.readSessionRef() || '').trim();
1573
1660
  const hadEnvironmentSession = Boolean(String(process.env.FORKIT_CONNECT_SESSION_REF || '').trim());
1661
+ const logoutAt = new Date().toISOString();
1574
1662
  delete process.env.FORKIT_CONNECT_SESSION_REF;
1575
1663
  try {
1576
1664
  service.setSessionRef(null);
1577
- console.log('[forkit-connect] Logged out. Local discovery remains available on this device.');
1665
+ console.log(`[forkit-connect] Logged out at ${logoutAt}. Local discovery remains available on this device.`);
1578
1666
  if (hadEnvironmentSession) {
1579
1667
  console.log('[forkit-connect] The in-process fallback session was cleared for this run.');
1580
1668
  }
@@ -1583,12 +1671,12 @@ async function run() {
1583
1671
  catch (error) {
1584
1672
  if (error instanceof credential_store_1.ConnectCredentialStoreError) {
1585
1673
  if (currentSessionRef) {
1586
- console.log('[forkit-connect] Logged out from the current interactive run.');
1674
+ console.log(`[forkit-connect] Logged out from the current interactive run at ${logoutAt}.`);
1587
1675
  console.log('[forkit-connect] If you previously exported a session in your shell, remove it there with:');
1588
1676
  console.log('unset FORKIT_CONNECT_SESSION_REF');
1589
1677
  return;
1590
1678
  }
1591
- console.log('[forkit-connect] No stored session found.');
1679
+ console.log(`[forkit-connect] No stored session found at ${logoutAt}.`);
1592
1680
  console.log('[forkit-connect] If you previously exported a session in this shell, remove it with:');
1593
1681
  console.log('unset FORKIT_CONNECT_SESSION_REF');
1594
1682
  return;
@@ -1644,6 +1732,7 @@ async function run() {
1644
1732
  try {
1645
1733
  service.setSessionRef(polled.body.connect_access_token);
1646
1734
  await service.refreshEffectiveBinding();
1735
+ await withTimeout(service.prewarmSmartRegistrationInbox(), 1800, undefined);
1647
1736
  const displayName = getSessionDisplayName(polled.body.connect_access_token);
1648
1737
  console.log(displayName
1649
1738
  ? `[forkit-connect] Login approved. Welcome, ${displayName}. Session credentials stored securely.`
@@ -1653,6 +1742,7 @@ async function run() {
1653
1742
  catch (error) {
1654
1743
  if (error instanceof credential_store_1.ConnectCredentialStoreError) {
1655
1744
  await activateEnvironmentSessionFallback(polled.body.connect_access_token);
1745
+ await withTimeout(service.prewarmSmartRegistrationInbox(), 1800, undefined);
1656
1746
  printSessionExportFallback(polled.body.connect_access_token);
1657
1747
  const displayName = getSessionDisplayName(polled.body.connect_access_token);
1658
1748
  console.log(displayName
@@ -1709,16 +1799,11 @@ async function run() {
1709
1799
  const buildWorkspaceStatusPayload = async () => {
1710
1800
  const sessionState = await checkBackendSessionState(service);
1711
1801
  if (service.readSessionRef()) {
1712
- try {
1713
- await service.refreshEffectiveBinding();
1714
- }
1715
- catch {
1716
- // Keep local scope view available.
1717
- }
1802
+ await withTimeout(service.refreshEffectiveBinding(), STATUS_BINDING_TIMEOUT_MS, undefined);
1718
1803
  }
1719
- const overview = service.getConnectStatusOverview();
1720
1804
  const operatingMode = resolveOperatingMode(service);
1721
- const accountTrusted = sessionState === 'authorized' || sessionState === 'unavailable';
1805
+ const accountTrusted = sessionState === 'authorized';
1806
+ const overview = withSmartInboxSnapshotCounts(service.getConnectStatusOverview({ includeInbox: false }));
1722
1807
  const localScopeCached = Boolean(String(overview.workspace_id || '').trim() || String(overview.project_id || '').trim());
1723
1808
  return {
1724
1809
  session_state: sessionState,
@@ -1739,7 +1824,9 @@ async function run() {
1739
1824
  };
1740
1825
  };
1741
1826
  const renderInteractiveStatusScreen = async (sessionState) => {
1742
- const accountLimits = await loadCliAccountLimits().catch(() => null);
1827
+ const accountLimits = node_process_1.stdin.isTTY && node_process_1.stdout.isTTY
1828
+ ? await withTimeout(loadCliAccountLimits().catch(() => null), INTERACTIVE_ACCOUNT_LIMITS_TIMEOUT_MS, null)
1829
+ : null;
1743
1830
  const displayName = accountLimits?.displayName ?? getSessionDisplayName(service.readSessionRef());
1744
1831
  renderInteractiveScreen('Forkit Connect', {
1745
1832
  subtitle: displayName ? `Welcome back, ${displayName}` : 'Interactive overview',
@@ -1788,18 +1875,8 @@ async function run() {
1788
1875
  // Only run discovery and binding refresh when the user has a session.
1789
1876
  // Pre-login, these calls add significant startup latency with no user benefit.
1790
1877
  if (service.readSessionRef()) {
1791
- try {
1792
- await service.runDiscoveryCycle();
1793
- }
1794
- catch {
1795
- // Keep the interactive shell usable even if local discovery has transient issues.
1796
- }
1797
- try {
1798
- await service.refreshEffectiveBinding();
1799
- }
1800
- catch {
1801
- // Use the latest local binding snapshot when backend refresh is unavailable.
1802
- }
1878
+ await withTimeout(service.runDiscoveryCycle(), INTERACTIVE_DISCOVERY_TIMEOUT_MS, undefined);
1879
+ await withTimeout(service.refreshEffectiveBinding(), INTERACTIVE_BINDING_TIMEOUT_MS, undefined);
1803
1880
  }
1804
1881
  };
1805
1882
  const runInteractiveWorkspaceMenu = async () => {
@@ -2394,7 +2471,10 @@ async function run() {
2394
2471
  }
2395
2472
  };
2396
2473
  const buildInteractiveRegisterCandidates = () => {
2397
- const inbox = service.buildSmartRegistrationInbox();
2474
+ const inbox = service.getSmartRegistrationInbox({
2475
+ preferSnapshot: true,
2476
+ refreshInBackground: true,
2477
+ });
2398
2478
  const candidates = new Map();
2399
2479
  const groups = ['needs_confirmation', 'ready_to_connect'];
2400
2480
  const state = service.getStateStore().readState();
@@ -2654,7 +2734,10 @@ async function run() {
2654
2734
  await runInteractiveAutoRefresh();
2655
2735
  const groupOrder = ['needs_confirmation', 'ready_to_connect', 'connected', 'ignored'];
2656
2736
  while (true) {
2657
- const inbox = service.buildSmartRegistrationInbox();
2737
+ const inbox = service.getSmartRegistrationInbox({
2738
+ preferSnapshot: true,
2739
+ refreshInBackground: true,
2740
+ });
2658
2741
  const entries = groupOrder.flatMap((group) => inbox.groups[group].map((item) => ({ group, item })));
2659
2742
  if (entries.length === 0) {
2660
2743
  renderInteractiveScreen('Smart Registration Inbox', {
@@ -2669,7 +2752,7 @@ async function run() {
2669
2752
  return;
2670
2753
  }
2671
2754
  renderInteractiveScreen('Smart Registration Inbox', {
2672
- subtitle: `Generated at ${inbox.summary.generated_at}`,
2755
+ subtitle: `Generated at ${inbox.summary.generated_at} · freshness=${inbox.summary.freshness_state ?? 'fresh'}`,
2673
2756
  sections: buildInteractiveInboxSections(inbox),
2674
2757
  footerLines: ['Choose an inbox item below.'],
2675
2758
  });
@@ -2704,11 +2787,15 @@ async function run() {
2704
2787
  }
2705
2788
  };
2706
2789
  const runInteractiveStart = async () => {
2790
+ if (!node_process_1.stdin.isTTY || !node_process_1.stdout.isTTY) {
2791
+ const sessionState = service.readSessionRef() ? 'unavailable' : 'missing';
2792
+ await renderInteractiveStatusScreen(sessionState);
2793
+ return;
2794
+ }
2707
2795
  while (true) {
2708
2796
  process.exitCode = 0;
2709
- await runInteractiveAutoRefresh();
2710
2797
  const sessionState = await checkBackendSessionState(service);
2711
- const authenticated = sessionState === 'authorized' || (sessionState === 'unavailable' && Boolean(service.readSessionRef()));
2798
+ const authenticated = sessionState === 'authorized';
2712
2799
  if (!authenticated) {
2713
2800
  await renderInteractiveStatusScreen(sessionState);
2714
2801
  const selected = await promptSelection('Choose an action', [
@@ -2765,6 +2852,8 @@ async function run() {
2765
2852
  { value: 'exit', label: 'Exit' },
2766
2853
  ]);
2767
2854
  if (!selected || selected === 'exit') {
2855
+ const exitAt = new Date().toISOString();
2856
+ console.log(`[forkit-connect] Quit at ${exitAt}. Interactive work stopped for this device; use logout if you want to clear the session reference.`);
2768
2857
  return;
2769
2858
  }
2770
2859
  if (selected === 'status') {
@@ -2964,6 +3053,10 @@ async function run() {
2964
3053
  runtimeSignalsUsed: null,
2965
3054
  runtimeSignalsLimit: fallbackPlan.runtimeSignalsPerMonth,
2966
3055
  runtimeSignalsRemaining: null,
3056
+ governedPassportsUsed: null,
3057
+ governedPassportsLimit: fallbackPlan.maxGovernedPassports,
3058
+ governedPassportsRemaining: null,
3059
+ publishedModelPassports: [],
2967
3060
  };
2968
3061
  cachedCliAccountLimits = fallback;
2969
3062
  cachedCliAccountLimitsAt = now;
@@ -3027,6 +3120,21 @@ async function run() {
3027
3120
  const runtimeSignalsUsed = typeof summaryPayload?.usage?.runtimeSignals === 'number'
3028
3121
  ? summaryPayload.usage.runtimeSignals
3029
3122
  : null;
3123
+ const governedPassportsUsed = typeof summaryPayload?.usage?.passports === 'number'
3124
+ ? summaryPayload.usage.passports
3125
+ : passports.length;
3126
+ const governedPassportsLimit = summaryPayload?.planCapabilities?.governance?.maxGovernedPassports
3127
+ ?? summaryPayload?.entitlements?.maxGovernedPassports
3128
+ ?? fallback.maxGovernedPassports;
3129
+ const publishedModelPassports = passports
3130
+ .filter((passport) => String(passport.passportType || passport.passport_type || passport.type || '').toLowerCase() === 'model')
3131
+ .map((passport) => ({
3132
+ gaid: String(passport.gaid || '').trim(),
3133
+ name: String(passport.name || 'Unnamed model passport').trim(),
3134
+ workspaceId: String(passport.workspaceId || passport.workspace_id || '').trim() || null,
3135
+ projectId: String(passport.projectId || passport.project_id || '').trim() || null,
3136
+ }))
3137
+ .filter((passport) => passport.gaid);
3030
3138
  const resolved = {
3031
3139
  displayName,
3032
3140
  planKey,
@@ -3047,6 +3155,10 @@ async function run() {
3047
3155
  runtimeSignalsUsed,
3048
3156
  runtimeSignalsLimit,
3049
3157
  runtimeSignalsRemaining: remainingFromLimit(runtimeSignalsLimit, runtimeSignalsUsed),
3158
+ governedPassportsUsed,
3159
+ governedPassportsLimit,
3160
+ governedPassportsRemaining: remainingFromLimit(governedPassportsLimit, governedPassportsUsed),
3161
+ publishedModelPassports,
3050
3162
  };
3051
3163
  cachedCliAccountLimits = resolved;
3052
3164
  cachedCliAccountLimitsAt = now;
@@ -3298,41 +3410,91 @@ async function run() {
3298
3410
  return 'Draft creation is not active for this binding yet. Complete Connect approval or update consent on Forkit.dev first.';
3299
3411
  return raw;
3300
3412
  };
3301
- const runRegisterOne = async (targetModelName) => {
3413
+ const normalizeRegisterSuccessMessage = (action) => {
3414
+ if (action === 'already_bound')
3415
+ return 'Model is already connected to an existing passport.';
3416
+ if (action === 'already_pending')
3417
+ return 'Model already has a pending draft. No duplicate draft created.';
3418
+ if (action === 'passport_registered')
3419
+ return 'Passport published successfully.';
3420
+ if (action === 'draft_created')
3421
+ return 'Draft created successfully.';
3422
+ if (action === 'draft_queued')
3423
+ return 'Draft queued locally and will sync when backend access is available.';
3424
+ return action;
3425
+ };
3426
+ const accountLimits = await loadCliAccountLimits().catch(() => null);
3427
+ const governedPassportCapacityFull = accountLimits
3428
+ ? isCapacityExhausted(accountLimits.governedPassportsLimit, accountLimits.governedPassportsRemaining)
3429
+ : false;
3430
+ const buildCapacityPayload = (requestedModel) => ({
3431
+ ok: false,
3432
+ code: 'GOVERNED_PASSPORT_CAPACITY_REACHED',
3433
+ requested_model: requestedModel ?? null,
3434
+ plan: accountLimits?.planName ?? operatingMode.tier ?? null,
3435
+ governed_passports_used: accountLimits?.governedPassportsUsed ?? null,
3436
+ governed_passports_limit: accountLimits?.governedPassportsLimit ?? null,
3437
+ message: 'This account has reached governed passport capacity. Forkit Connect will not create more governed drafts by default because publishing would be blocked.',
3438
+ next_actions: [
3439
+ 'Use an existing published model passport for Runtime Signals C2 or run-log testing.',
3440
+ 'Free capacity or upgrade before publishing another governed model.',
3441
+ 'Use --draft-only if you intentionally want to save another draft for later review.',
3442
+ ],
3443
+ existing_model_passports: accountLimits?.publishedModelPassports.slice(0, 8) ?? [],
3444
+ });
3445
+ const runRegisterOne = async (targetModelSelector, displayNameHint) => {
3302
3446
  try {
3303
- const result = await service.connectDetectedModel(targetModelName);
3447
+ const result = await service.connectDetectedModel(targetModelSelector);
3304
3448
  return {
3305
3449
  ok: true,
3306
3450
  model: result.model.model,
3451
+ selector: result.model.discoveryHash,
3307
3452
  draftId: result.draftId ?? null,
3308
3453
  gaid: result.gaid ?? null,
3309
- message: result.action,
3454
+ message: normalizeRegisterSuccessMessage(result.action),
3455
+ action: result.action,
3310
3456
  };
3311
3457
  }
3312
3458
  catch (error) {
3313
3459
  const rawMessage = error instanceof Error ? error.message : 'register_failed';
3314
3460
  return {
3315
3461
  ok: false,
3316
- model: targetModelName,
3462
+ model: displayNameHint || targetModelSelector,
3463
+ selector: targetModelSelector,
3317
3464
  message: normalizeRegisterErrorMessage(rawMessage),
3318
3465
  };
3319
3466
  }
3320
3467
  };
3321
3468
  if (hasFlag('--all-ready')) {
3469
+ if (governedPassportCapacityFull && !hasFlag('--draft-only')) {
3470
+ printJson({
3471
+ ...buildCapacityPayload(null),
3472
+ attempted: 0,
3473
+ skipped: 'all-ready',
3474
+ });
3475
+ process.exitCode = 2;
3476
+ return;
3477
+ }
3322
3478
  const inbox = service.buildSmartRegistrationInbox();
3323
- const readyModels = inbox.groups.ready_to_connect
3479
+ const readyModelCandidates = inbox.groups.ready_to_connect
3324
3480
  .filter((item) => item.item_type === 'model' && item.recommended_action === 'create_passport_draft')
3325
- .map((item) => item.display_name);
3326
- if (readyModels.length === 0) {
3481
+ .map((item) => ({
3482
+ selector: extractInboxItemSelector(item),
3483
+ model: item.display_name,
3484
+ }))
3485
+ .filter((item) => String(item.selector || '').trim())
3486
+ .sort((left, right) => left.model.localeCompare(right.model) || left.selector.localeCompare(right.selector));
3487
+ if (readyModelCandidates.length === 0) {
3327
3488
  console.log('No ready local models need registration.');
3328
3489
  return;
3329
3490
  }
3330
3491
  const results = [];
3331
- for (const item of readyModels) {
3332
- results.push(await runRegisterOne(item));
3492
+ for (const item of readyModelCandidates) {
3493
+ results.push(await runRegisterOne(item.selector, item.model));
3333
3494
  }
3334
3495
  printJson({
3335
- attempted: readyModels.length,
3496
+ attempted: readyModelCandidates.length,
3497
+ selectors: readyModelCandidates.map((item) => item.selector),
3336
3498
  results,
3337
3499
  });
3338
3500
  if (results.some((item) => !item.ok)) {
@@ -3345,18 +3507,41 @@ async function run() {
3345
3507
  const readyModels = inbox.groups.ready_to_connect
3346
3508
  .filter((item) => item.item_type === 'model')
3347
3509
  .map((item) => item.display_name);
3510
+ const readyModelSelectors = inbox.groups.ready_to_connect
3511
+ .filter((item) => item.item_type === 'model')
3512
+ .map((item) => ({
3513
+ model: item.display_name,
3514
+ selector: extractInboxItemSelector(item),
3515
+ }))
3516
+ .filter((item) => String(item.selector || '').trim())
3517
+ .sort((left, right) => left.model.localeCompare(right.model) || left.selector.localeCompare(right.selector));
3348
3518
  printJson({
3349
3519
  operating_mode: operatingMode.mode,
3350
3520
  tier: operatingMode.tier,
3351
3521
  workspace_id: boundWorkspaceId,
3352
3522
  project_id: boundProjectId,
3523
+ capacity: accountLimits ? {
3524
+ governed_passports_used: accountLimits.governedPassportsUsed,
3525
+ governed_passports_limit: accountLimits.governedPassportsLimit,
3526
+ governed_passports_remaining: accountLimits.governedPassportsRemaining,
3527
+ capacity_full: governedPassportCapacityFull,
3528
+ } : null,
3353
3529
  ready_models: readyModels,
3530
+ ready_model_selectors: readyModelSelectors,
3531
+ existing_model_passports: accountLimits?.publishedModelPassports.slice(0, 8) ?? [],
3354
3532
  next: readyModels.length
3355
- ? 'Run forkit-connect register --model "<name>" or forkit-connect register --all-ready'
3533
+ ? governedPassportCapacityFull
3534
+ ? 'Capacity is full. Use an existing passport for C2/run-log testing, free capacity, upgrade, or pass --draft-only if you intentionally want another draft.'
3535
+ : 'Run forkit-connect register --model "<name>" or forkit-connect register --all-ready'
3356
3536
  : 'Run forkit-connect scan first or review forkit-connect inbox',
3357
3537
  });
3358
3538
  return;
3359
3539
  }
3540
+ if (governedPassportCapacityFull && !hasFlag('--draft-only')) {
3541
+ printJson(buildCapacityPayload(modelName));
3542
+ process.exitCode = 2;
3543
+ return;
3544
+ }
3360
3545
  const result = await runRegisterOne(modelName);
3361
3546
  printJson(result);
3362
3547
  if (!result.ok) {
@@ -3408,7 +3593,7 @@ async function run() {
3408
3593
  return;
3409
3594
  }
3410
3595
  if (command === 'inbox') {
3411
- runPublicConnectInbox();
3596
+ await runPublicConnectInbox();
3412
3597
  return;
3413
3598
  }
3414
3599
  if (command === 'start') {
@@ -3423,6 +3608,30 @@ async function run() {
3423
3608
  await runPublicSync();
3424
3609
  return;
3425
3610
  }
3611
+ if (command === 'runtime') {
3612
+ const subcommand = args[1] || 'review';
3613
+ if (subcommand === 'review') {
3614
+ const summary = service.getRuntimePassportReview();
3615
+ if (hasFlag('--json')) {
3616
+ printJson(summary);
3617
+ return;
3618
+ }
3619
+ printRuntimeReview(summary);
3620
+ return;
3621
+ }
3622
+ if (subcommand === 'status') {
3623
+ const status = service.getRuntimePassportStatus();
3624
+ if (hasFlag('--json')) {
3625
+ printJson(status);
3626
+ return;
3627
+ }
3628
+ printRuntimeStatus(status);
3629
+ return;
3630
+ }
3631
+ console.error('Usage: forkit-connect runtime <review|status>');
3632
+ process.exitCode = 2;
3633
+ return;
3634
+ }
3426
3635
  if (command === 'workspace') {
3427
3636
  const subcommand = args[1] || 'status';
3428
3637
  try {
@@ -3505,7 +3714,7 @@ async function run() {
3505
3714
  return;
3506
3715
  }
3507
3716
  if (subcommand === 'inbox') {
3508
- runPublicConnectInbox();
3717
+ await runPublicConnectInbox();
3509
3718
  return;
3510
3719
  }
3511
3720
  if (subcommand === 'services') {
@@ -3530,12 +3739,23 @@ async function run() {
3530
3739
  return;
3531
3740
  }
3532
3741
  if (subcommand === 'runtime') {
3533
- if (args[2] === 'review') {
3534
- printRuntimeReview(service.getRuntimePassportReview());
3742
+ const runtimeSubcommand = args[2] || 'review';
3743
+ if (runtimeSubcommand === 'review') {
3744
+ const summary = service.getRuntimePassportReview();
3745
+ if (hasFlag('--json')) {
3746
+ printJson(summary);
3747
+ return;
3748
+ }
3749
+ printRuntimeReview(summary);
3535
3750
  return;
3536
3751
  }
3537
- if (args[2] === 'status') {
3538
- printRuntimeStatus(service.getRuntimePassportStatus());
3752
+ if (runtimeSubcommand === 'status') {
3753
+ const status = service.getRuntimePassportStatus();
3754
+ if (hasFlag('--json')) {
3755
+ printJson(status);
3756
+ return;
3757
+ }
3758
+ printRuntimeStatus(status);
3539
3759
  return;
3540
3760
  }
3541
3761
  console.error('Usage: forkit-connect connect runtime <review|status>');
@@ -3650,6 +3870,54 @@ async function run() {
3650
3870
  }, null, 2));
3651
3871
  return;
3652
3872
  }
3873
+ if (subcommand === 'run-log' && args[2] === 'emit') {
3874
+ const gaid = getArg('--gaid') ?? getArg('--heartbeat-gaid');
3875
+ const apiKey = getArg('--api-key') ?? getArg('--heartbeat-key') ?? process.env.FORKIT_RUNTIME_SIGNAL_API_KEY ?? null;
3876
+ const provider = getArg('--provider');
3877
+ const runModel = getArg('--model');
3878
+ const serviceName = getArg('--service-name') ?? getArg('--name');
3879
+ if (!gaid || !provider || !runModel || !serviceName) {
3880
+ console.error('[forkit-connect] c2 run-log emit requires --gaid, --provider, --model, and --service-name.');
3881
+ console.error('[forkit-connect] Provide an API key with --api-key, --heartbeat-key, FORKIT_RUNTIME_SIGNAL_API_KEY, or a stored c2 set-key entry.');
3882
+ process.exitCode = 1;
3883
+ return;
3884
+ }
3885
+ const result = await service.emitRuntimeRunLog({
3886
+ gaid,
3887
+ apiKey,
3888
+ provider,
3889
+ model: runModel,
3890
+ serviceName,
3891
+ serviceKind: getArg('--service-kind') ?? 'custom',
3892
+ runId: getArg('--run-id'),
3893
+ externalRunId: getArg('--external-run-id'),
3894
+ status: getArg('--status') ?? 'completed',
3895
+ startedAt: getArg('--started-at'),
3896
+ endedAt: getArg('--ended-at'),
3897
+ promptTokens: getNumericArg('--prompt-tokens'),
3898
+ completionTokens: getNumericArg('--completion-tokens'),
3899
+ cachedPromptTokens: getNumericArg('--cached-prompt-tokens'),
3900
+ reasoningTokens: getNumericArg('--reasoning-tokens'),
3901
+ latencyMs: getNumericArg('--latency-ms'),
3902
+ estimatedCostCents: getNumericArg('--estimated-cost-cents'),
3903
+ currency: getArg('--currency') ?? 'USD',
3904
+ summary: getArg('--summary'),
3905
+ });
3906
+ if (!result.ok) {
3907
+ console.error(`[forkit-connect] Runtime run log emit failed (${result.status || 'local'}).`);
3908
+ if (result.body) {
3909
+ console.error(typeof result.body === 'string' ? result.body : JSON.stringify(result.body));
3910
+ }
3911
+ process.exitCode = 2;
3912
+ return;
3913
+ }
3914
+ console.log(JSON.stringify({
3915
+ accepted: true,
3916
+ status: result.status,
3917
+ response: result.body,
3918
+ }, null, 2));
3919
+ return;
3920
+ }
3653
3921
  if (subcommand === 'sync') {
3654
3922
  const syncResult = await service.flushC2LifecycleEvents({
3655
3923
  runtimeSignalApiKey: getArg('--heartbeat-key'),
@@ -4441,6 +4709,29 @@ async function run() {
4441
4709
  process.exitCode = 2;
4442
4710
  return;
4443
4711
  }
4712
+ const accountLimits = await loadCliAccountLimits().catch(() => null);
4713
+ const governedPassportCapacityFull = accountLimits
4714
+ ? isCapacityExhausted(accountLimits.governedPassportsLimit, accountLimits.governedPassportsRemaining)
4715
+ : false;
4716
+ if (governedPassportCapacityFull && !hasFlag('--draft-only')) {
4717
+ printJson({
4718
+ ok: false,
4719
+ code: 'GOVERNED_PASSPORT_CAPACITY_REACHED',
4720
+ requested_model: modelName,
4721
+ plan: accountLimits?.planName ?? null,
4722
+ governed_passports_used: accountLimits?.governedPassportsUsed ?? null,
4723
+ governed_passports_limit: accountLimits?.governedPassportsLimit ?? null,
4724
+ message: 'This account has reached governed passport capacity. Forkit Connect will not create another governed draft by default because publishing would be blocked.',
4725
+ next_actions: [
4726
+ 'Use an existing published model passport for Runtime Signals C2 or run-log testing.',
4727
+ 'Free capacity or upgrade before publishing another governed model.',
4728
+ 'Use --draft-only if you intentionally want to save another draft for later review.',
4729
+ ],
4730
+ existing_model_passports: accountLimits?.publishedModelPassports.slice(0, 8) ?? [],
4731
+ });
4732
+ process.exitCode = 2;
4733
+ return;
4734
+ }
4444
4735
  const modelKey = `${model.model}#${model.digest}`;
4445
4736
  const existingBinding = (state.model_bindings || []).find((binding) => binding.modelKey === modelKey && binding.status !== 'ignored');
4446
4737
  if (existingBinding?.draftId) {