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.
@@ -50,6 +50,7 @@ const SMART_INBOX_CONFIDENCE_PRIORITY = {
50
50
  medium: 1,
51
51
  low: 2,
52
52
  };
53
+ const SMART_INBOX_FRESH_MAX_AGE_MS = 45 * 1000;
53
54
  function isRecord(value) {
54
55
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
55
56
  }
@@ -113,37 +114,44 @@ function notificationTargetUrl(candidate) {
113
114
  || 'http://127.0.0.1:4317/').trim();
114
115
  const base = configuredBase.replace(/\/+$/, '');
115
116
  const params = new URLSearchParams();
117
+ const routeToCommandReview = () => {
118
+ params.set('screen', 'command');
119
+ params.set('focus', 'review');
120
+ if (!candidate)
121
+ return;
122
+ const grouped = normalizeDisplayText(candidate.metadata.notification_group);
123
+ if (!grouped) {
124
+ params.set('item', candidate.id);
125
+ }
126
+ };
116
127
  if (!candidate) {
117
- params.set('screen', 'discovery');
118
- params.set('focus', 'reviews');
128
+ routeToCommandReview();
119
129
  return `${base}/?${params.toString()}`;
120
130
  }
121
131
  if (candidate.suggested_action === 'sync_c2' || candidate.type === 'c2_sync_pending') {
122
132
  params.set('screen', 'runtime');
123
133
  params.set('focus', 'c2');
124
134
  }
125
- else if (candidate.suggested_action === 'connect_model'
135
+ else if (candidate.suggested_action === 'open_inbox'
136
+ || candidate.type === 'runtime_unavailable'
137
+ || candidate.suggested_action === 'connect_model'
126
138
  || candidate.suggested_action === 'review_model'
127
139
  || candidate.type === 'model_detected'
128
140
  || candidate.type === 'agent_detected'
129
141
  || candidate.suggested_action === 'review_agent'
130
142
  || candidate.suggested_action === 'link_agent_model') {
131
- params.set('screen', 'discovery');
132
- params.set('focus', 'item');
143
+ routeToCommandReview();
133
144
  }
134
- else if (candidate.suggested_action === 'view_pulse_history' || candidate.type === 'runtime_unavailable') {
135
- params.set('screen', 'runtime');
136
- params.set('focus', 'item');
145
+ else if (candidate.suggested_action === 'view_pulse_history') {
146
+ routeToCommandReview();
137
147
  }
138
148
  else if (candidate.type === 'passport_draft_created') {
139
149
  params.set('screen', 'passports');
140
150
  params.set('focus', 'history');
141
151
  }
142
152
  else {
143
- params.set('screen', 'discovery');
144
- params.set('focus', 'reviews');
153
+ routeToCommandReview();
145
154
  }
146
- params.set('item', candidate.id);
147
155
  return `${base}/?${params.toString()}`;
148
156
  }
149
157
  function hasLinuxGuiSession() {
@@ -568,6 +576,7 @@ function isGovernorRevokedStatus(status, code) {
568
576
  class ConnectV1Service {
569
577
  stateStore;
570
578
  credentialStore;
579
+ smartInboxBackgroundRefreshInFlight = false;
571
580
  constructor(stateDir, options) {
572
581
  this.stateStore = new state_1.LocalStateStore(stateDir);
573
582
  this.stateStore.ensureInitialized();
@@ -580,6 +589,68 @@ class ConnectV1Service {
580
589
  getStateStore() {
581
590
  return this.stateStore;
582
591
  }
592
+ cloneInboxSnapshot(inbox) {
593
+ return JSON.parse(JSON.stringify(inbox));
594
+ }
595
+ withInboxFreshness(inbox, freshness, ageMs) {
596
+ const next = this.cloneInboxSnapshot(inbox);
597
+ next.summary = {
598
+ ...next.summary,
599
+ freshness_state: freshness,
600
+ snapshot_age_seconds: Math.max(0, Math.floor(ageMs / 1000)),
601
+ };
602
+ return next;
603
+ }
604
+ persistSmartInboxSnapshot(inbox) {
605
+ const state = this.stateStore.readState();
606
+ state.smart_inbox_snapshot = this.cloneInboxSnapshot(inbox);
607
+ state.smart_inbox_snapshot_updated_at = nowIso();
608
+ this.stateStore.writeState(state);
609
+ }
610
+ getSmartRegistrationInbox(options) {
611
+ const preferSnapshot = options?.preferSnapshot !== false;
612
+ const forceRefresh = options?.forceRefresh === true;
613
+ const maxSnapshotAgeMs = Math.max(1000, options?.maxSnapshotAgeMs ?? SMART_INBOX_FRESH_MAX_AGE_MS);
614
+ const refreshInBackground = options?.refreshInBackground !== false;
615
+ const state = this.stateStore.readState();
616
+ const snapshot = state.smart_inbox_snapshot;
617
+ const updatedAtMs = state.smart_inbox_snapshot_updated_at ? Date.parse(state.smart_inbox_snapshot_updated_at) : Number.NaN;
618
+ const ageMs = Number.isFinite(updatedAtMs) ? Math.max(0, Date.now() - updatedAtMs) : Number.MAX_SAFE_INTEGER;
619
+ if (!forceRefresh && preferSnapshot && snapshot) {
620
+ if (this.smartInboxBackgroundRefreshInFlight) {
621
+ return this.withInboxFreshness(snapshot, 'syncing', ageMs);
622
+ }
623
+ if (ageMs <= maxSnapshotAgeMs) {
624
+ return this.withInboxFreshness(snapshot, 'fresh', ageMs);
625
+ }
626
+ if (refreshInBackground) {
627
+ this.refreshSmartRegistrationInboxInBackground();
628
+ }
629
+ return this.withInboxFreshness(snapshot, refreshInBackground ? 'syncing' : 'stale', ageMs);
630
+ }
631
+ const liveInbox = this.buildSmartRegistrationInbox();
632
+ this.persistSmartInboxSnapshot(liveInbox);
633
+ return this.withInboxFreshness(liveInbox, 'fresh', 0);
634
+ }
635
+ refreshSmartRegistrationInboxInBackground() {
636
+ if (this.smartInboxBackgroundRefreshInFlight) {
637
+ return;
638
+ }
639
+ this.smartInboxBackgroundRefreshInFlight = true;
640
+ void Promise.resolve().then(() => {
641
+ try {
642
+ const liveInbox = this.buildSmartRegistrationInbox();
643
+ this.persistSmartInboxSnapshot(liveInbox);
644
+ }
645
+ finally {
646
+ this.smartInboxBackgroundRefreshInFlight = false;
647
+ }
648
+ });
649
+ }
650
+ async prewarmSmartRegistrationInbox() {
651
+ const liveInbox = this.buildSmartRegistrationInbox();
652
+ this.persistSmartInboxSnapshot(liveInbox);
653
+ }
583
654
  getStoredSessionRef() {
584
655
  return this.credentialStore.getSessionRef();
585
656
  }
@@ -1547,6 +1618,9 @@ class ConnectV1Service {
1547
1618
  backendBaseUrl: DEFAULT_BASE_URL,
1548
1619
  };
1549
1620
  }
1621
+ getCredentialStoreStatus() {
1622
+ return this.credentialStore.getStatus();
1623
+ }
1550
1624
  getConfig() {
1551
1625
  return this.stateStore.readState().connect_config;
1552
1626
  }
@@ -1691,6 +1765,16 @@ class ConnectV1Service {
1691
1765
  // Preserve the current item failure and let the next pass reconcile binding state again.
1692
1766
  }
1693
1767
  }
1768
+ if (input.source === 'runtimeSignal' && input.passportGaid && isCredentialInvalidStatus(input.status, code)) {
1769
+ this.clearRuntimeSignalApiKeyForGaid(input.passportGaid, 'recovery');
1770
+ try {
1771
+ await this.autoProvisionRuntimeSignalKeys({ suppressErrors: true });
1772
+ }
1773
+ catch {
1774
+ // Keep the failure visible in local state. A later cycle or manual key
1775
+ // configuration can recover if provisioning is still blocked.
1776
+ }
1777
+ }
1694
1778
  const observedSession = this.observeBackendCommunicationState(input);
1695
1779
  return {
1696
1780
  observedSession,
@@ -1946,10 +2030,18 @@ class ConnectV1Service {
1946
2030
  if (!sessionRef) {
1947
2031
  return this.getC2SessionSummary();
1948
2032
  }
2033
+ const workspaceId = String(state.workspace_binding.workspaceId || '').trim();
2034
+ const projectId = String(state.project_binding.projectId || '').trim();
2035
+ const scope = workspaceId || projectId
2036
+ ? {
2037
+ ...(workspaceId ? { workspaceId } : {}),
2038
+ ...(projectId ? { projectId } : {}),
2039
+ }
2040
+ : undefined;
1949
2041
  const boundGaids = [...new Set(state.model_bindings.filter((binding) => binding.status !== 'ignored' && typeof binding.gaid === 'string' && binding.gaid?.trim()).map((binding) => binding.gaid))];
1950
2042
  for (const passportGaid of boundGaids) {
1951
2043
  try {
1952
- const result = await this.getApiClient(state).getDeployments(passportGaid);
2044
+ const result = await this.getApiClient(state).getDeployments(passportGaid, scope);
1953
2045
  if (!result.ok) {
1954
2046
  this.observeBackendCommunicationState({
1955
2047
  passportGaid,
@@ -2104,6 +2196,41 @@ class ConnectV1Service {
2104
2196
  }
2105
2197
  return null;
2106
2198
  }
2199
+ hasExactBoundPassportForModel(state, model) {
2200
+ const modelName = String(model.model || '').trim().toLowerCase();
2201
+ const digest = String(model.digest || '').trim();
2202
+ if (!modelName || !digest) {
2203
+ return false;
2204
+ }
2205
+ return state.model_bindings.some((binding) => {
2206
+ if (binding.status !== 'bound') {
2207
+ return false;
2208
+ }
2209
+ const boundGaid = String(binding.gaid || '').trim();
2210
+ if (!boundGaid) {
2211
+ return false;
2212
+ }
2213
+ const bindingModelName = modelNameFromKey(binding.modelKey).trim().toLowerCase();
2214
+ const bindingDigest = String(modelDigestFromKey(binding.modelKey) || '').trim();
2215
+ return bindingModelName === modelName && bindingDigest === digest;
2216
+ });
2217
+ }
2218
+ runtimeHasExactBoundModelDuplicate(state, runtimePassport) {
2219
+ for (const model of state.detected_models) {
2220
+ if (this.getModelReviewDisposition(model) !== 'active') {
2221
+ continue;
2222
+ }
2223
+ const detectedRuntime = this.findDetectedRuntimeForModel(state, model);
2224
+ const candidateRuntimePassport = detectedRuntime ? this.findRuntimePassportForRuntime(state, detectedRuntime) : null;
2225
+ if (!candidateRuntimePassport || candidateRuntimePassport.runtime_gaid !== runtimePassport.runtime_gaid) {
2226
+ continue;
2227
+ }
2228
+ if (this.hasExactBoundPassportForModel(state, model)) {
2229
+ return true;
2230
+ }
2231
+ }
2232
+ return false;
2233
+ }
2107
2234
  clearModelReviewDeferral(model) {
2108
2235
  if (model.review_state !== 'deferred' && !model.review_deferred_until) {
2109
2236
  return;
@@ -2544,6 +2671,87 @@ class ConnectV1Service {
2544
2671
  return undefined;
2545
2672
  return state.model_bindings.find((binding) => binding.gaid === gaid);
2546
2673
  }
2674
+ findTrainingAnchorBinding(state, input) {
2675
+ const normalizedModelName = input.modelName.trim().toLowerCase();
2676
+ if (!normalizedModelName)
2677
+ return undefined;
2678
+ const normalizedWorkspaceId = String(input.workspaceId || '').trim() || null;
2679
+ const normalizedProjectId = String(input.projectId || '').trim() || null;
2680
+ const candidates = state.model_bindings.filter((binding) => {
2681
+ if (!binding.gaid || binding.status === 'ignored')
2682
+ return false;
2683
+ return modelNameFromKey(binding.modelKey).trim().toLowerCase() === normalizedModelName;
2684
+ });
2685
+ if (candidates.length === 0)
2686
+ return undefined;
2687
+ return [...candidates].sort((left, right) => {
2688
+ const leftProjectMatch = normalizedProjectId && String(left.projectId || '').trim() === normalizedProjectId;
2689
+ const rightProjectMatch = normalizedProjectId && String(right.projectId || '').trim() === normalizedProjectId;
2690
+ if (leftProjectMatch !== rightProjectMatch)
2691
+ return leftProjectMatch ? -1 : 1;
2692
+ const leftWorkspaceMatch = normalizedWorkspaceId && String(left.workspaceId || '').trim() === normalizedWorkspaceId;
2693
+ const rightWorkspaceMatch = normalizedWorkspaceId && String(right.workspaceId || '').trim() === normalizedWorkspaceId;
2694
+ if (leftWorkspaceMatch !== rightWorkspaceMatch)
2695
+ return leftWorkspaceMatch ? -1 : 1;
2696
+ const leftBound = left.status === 'bound';
2697
+ const rightBound = right.status === 'bound';
2698
+ if (leftBound !== rightBound)
2699
+ return leftBound ? -1 : 1;
2700
+ return String(right.updatedAt || '').localeCompare(String(left.updatedAt || ''));
2701
+ })[0];
2702
+ }
2703
+ anchorBuildSessionToBinding(state, session) {
2704
+ if (session.passport_gaid)
2705
+ return session;
2706
+ const binding = this.findTrainingAnchorBinding(state, {
2707
+ modelName: session.model_name,
2708
+ workspaceId: session.workspaceId,
2709
+ projectId: session.projectId,
2710
+ });
2711
+ if (!binding?.gaid)
2712
+ return session;
2713
+ return {
2714
+ ...session,
2715
+ passport_gaid: binding.gaid,
2716
+ workspaceId: session.workspaceId || binding.workspaceId || null,
2717
+ projectId: session.projectId || binding.projectId || null,
2718
+ };
2719
+ }
2720
+ resolveRuntimeRunScope(state, passportGaid) {
2721
+ const trimScope = (value) => String(value || '').trim() || null;
2722
+ const binding = this.findBindingByGaid(state, passportGaid);
2723
+ if (binding) {
2724
+ const primaryBound = this.getPrimaryBoundBinding(state);
2725
+ return {
2726
+ workspaceId: trimScope(binding.workspaceId) ?? (primaryBound?.gaid === binding.gaid ? trimScope(state.workspace_binding.workspaceId) : null),
2727
+ projectId: trimScope(binding.projectId) ?? (primaryBound?.gaid === binding.gaid ? trimScope(state.project_binding.projectId) : null),
2728
+ };
2729
+ }
2730
+ const latestSession = [...state.c2_sessions]
2731
+ .filter((session) => session.passport_gaid === passportGaid)
2732
+ .sort((left, right) => String(right.last_seen_at || right.last_control_seen_at || '').localeCompare(String(left.last_seen_at || left.last_control_seen_at || '')))
2733
+ .at(0);
2734
+ if (latestSession) {
2735
+ return {
2736
+ workspaceId: trimScope(latestSession.workspaceId),
2737
+ projectId: trimScope(latestSession.projectId),
2738
+ };
2739
+ }
2740
+ const runtimeGaid = state.agent_links.find((link) => link.passport_gaid === passportGaid)?.runtime_gaid ?? null;
2741
+ if (runtimeGaid) {
2742
+ const runtimePassport = state.runtime_passports.find((runtime) => runtime.runtime_gaid === runtimeGaid);
2743
+ if (runtimePassport) {
2744
+ return {
2745
+ workspaceId: trimScope(runtimePassport.workspaceId),
2746
+ projectId: trimScope(runtimePassport.projectId),
2747
+ };
2748
+ }
2749
+ }
2750
+ return {
2751
+ workspaceId: null,
2752
+ projectId: null,
2753
+ };
2754
+ }
2547
2755
  getPrimaryBoundBinding(state) {
2548
2756
  return state.model_bindings.find((binding) => binding.status === 'bound' && typeof binding.gaid === 'string' && binding.gaid.trim());
2549
2757
  }
@@ -2562,6 +2770,28 @@ class ConnectV1Service {
2562
2770
  updatedAt: nowIso(),
2563
2771
  });
2564
2772
  }
2773
+ clearRuntimeSignalApiKeyForGaid(gaid, source = 'reset') {
2774
+ if (!gaid)
2775
+ return;
2776
+ const state = this.stateStore.readState();
2777
+ const binding = this.findBindingByGaid(state, gaid);
2778
+ this.credentialStore.clearRuntimeSignalApiKey(gaid);
2779
+ if (!binding)
2780
+ return;
2781
+ this.stateStore.upsertModelBinding({
2782
+ ...binding,
2783
+ runtimeSignalKeyPresent: false,
2784
+ runtimeSignalKeyRef: null,
2785
+ updatedAt: nowIso(),
2786
+ });
2787
+ this.stateStore.addEvidenceEvent({
2788
+ type: 'credential_invalid_observed',
2789
+ details: {
2790
+ source,
2791
+ gaid,
2792
+ },
2793
+ });
2794
+ }
2565
2795
  resolveRuntimeSignalApiKeyForEvent(state, passportGaid) {
2566
2796
  const binding = this.findBindingByGaid(state, passportGaid);
2567
2797
  return String(this.credentialStore.getRuntimeSignalApiKey(passportGaid)
@@ -2834,6 +3064,65 @@ class ConnectV1Service {
2834
3064
  boundGaid: boundBinding?.gaid ?? null,
2835
3065
  };
2836
3066
  }
3067
+ async emitRuntimeRunLog(input) {
3068
+ const state = this.stateStore.readState();
3069
+ const gaid = String(input.gaid || '').trim();
3070
+ const apiKey = String(input.apiKey || this.resolveRuntimeSignalApiKeyForEvent(state, gaid) || '').trim();
3071
+ if (!gaid || !apiKey) {
3072
+ return {
3073
+ ok: false,
3074
+ status: 0,
3075
+ body: {
3076
+ error: 'Runtime signal API key required. Run `forkit-connect c2 set-key --heartbeat-gaid <gaid> --heartbeat-key <key>` or pass --api-key from a secure environment.',
3077
+ code: 'MISSING_RUNTIME_SIGNAL_API_KEY',
3078
+ },
3079
+ contract: null,
3080
+ };
3081
+ }
3082
+ const startedAt = input.startedAt || nowIso();
3083
+ const endedAt = input.endedAt || (input.status && input.status !== 'running' ? startedAt : undefined);
3084
+ const runId = String(input.runId || `forkit-connect-${(0, node_crypto_1.randomUUID)()}`).trim();
3085
+ const promptTokens = Math.max(0, Math.trunc(input.promptTokens ?? 0));
3086
+ const completionTokens = Math.max(0, Math.trunc(input.completionTokens ?? 0));
3087
+ const totalTokens = promptTokens + completionTokens;
3088
+ const { workspaceId, projectId } = this.resolveRuntimeRunScope(state, gaid);
3089
+ return this.getApiClient(state).pushRuntimeRunLog({
3090
+ gaid,
3091
+ apiKey,
3092
+ schemaVersion: 'runtime.run.v1',
3093
+ run: {
3094
+ runId,
3095
+ passportGaid: gaid,
3096
+ provider: String(input.provider || 'custom').trim() || 'custom',
3097
+ model: String(input.model || 'unknown').trim() || 'unknown',
3098
+ serviceName: String(input.serviceName || 'Forkit Connect CLI').trim() || 'Forkit Connect CLI',
3099
+ serviceKind: String(input.serviceKind || 'custom').trim() || 'custom',
3100
+ ...(input.externalRunId ? { externalRunId: String(input.externalRunId).trim() } : {}),
3101
+ workspaceId,
3102
+ projectId,
3103
+ status: String(input.status || 'completed').trim() || 'completed',
3104
+ startedAt,
3105
+ ...(endedAt ? { endedAt } : {}),
3106
+ usage: {
3107
+ promptTokens,
3108
+ completionTokens,
3109
+ ...(input.cachedPromptTokens !== undefined ? { cachedPromptTokens: Math.max(0, Math.trunc(input.cachedPromptTokens)) } : {}),
3110
+ ...(input.reasoningTokens !== undefined ? { reasoningTokens: Math.max(0, Math.trunc(input.reasoningTokens)) } : {}),
3111
+ totalTokens,
3112
+ ...(input.latencyMs !== undefined ? { latencyMs: Math.max(0, Math.trunc(input.latencyMs)) } : {}),
3113
+ ...(input.estimatedCostCents !== undefined ? { estimatedCostCents: Math.max(0, Number(input.estimatedCostCents)) } : {}),
3114
+ currency: String(input.currency || 'USD').trim().toUpperCase().slice(0, 3) || 'USD',
3115
+ },
3116
+ ...(input.summary ? { summary: String(input.summary).trim().slice(0, 4000) } : {}),
3117
+ metadata: {
3118
+ source: 'forkit-connect-cli',
3119
+ safe_metadata_only: true,
3120
+ connect_device_id: state.connect_identity.connect_device_id,
3121
+ },
3122
+ },
3123
+ steps: [],
3124
+ });
3125
+ }
2837
3126
  findAgentByIdOrName(state, selector) {
2838
3127
  const normalized = selector.trim().toLowerCase();
2839
3128
  const exactId = state.detected_agents.find((agent) => agent.agent_id === selector || agent.agent_id.startsWith(selector));
@@ -4083,10 +4372,12 @@ class ConnectV1Service {
4083
4372
  continue;
4084
4373
  }
4085
4374
  const connectableModel = this.findConnectableModelForRuntime(finalState, runtimePassport);
4086
- const runtimeIsOnlyDuplicatingModelRegistration = Boolean(connectableModel
4375
+ const runtimeHasExactDuplicateBoundModel = this.runtimeHasExactBoundModelDuplicate(finalState, runtimePassport);
4376
+ const runtimeIsOnlyDuplicatingModelRegistration = Boolean(!connectableModel
4087
4377
  && runtimePassport.status !== 'unavailable'
4088
4378
  && runtimePassport.linked_model_gaids.length === 0
4089
- && runtimePassport.linked_agent_ids.length === 0);
4379
+ && runtimePassport.linked_agent_ids.length === 0
4380
+ && runtimeHasExactDuplicateBoundModel);
4090
4381
  if (runtimeIsOnlyDuplicatingModelRegistration) {
4091
4382
  continue;
4092
4383
  }
@@ -4240,13 +4531,16 @@ class ConnectV1Service {
4240
4531
  website_is_governance_authority: true,
4241
4532
  };
4242
4533
  }
4243
- getConnectStatusOverview() {
4534
+ getConnectStatusOverview(options) {
4244
4535
  const state = this.stateStore.readState();
4245
- const inbox = this.buildSmartRegistrationInbox();
4536
+ const includeInbox = options?.includeInbox !== false;
4537
+ const inbox = includeInbox ? this.buildSmartRegistrationInbox() : null;
4246
4538
  const sessionSummary = this.getC2SessionSummary();
4247
4539
  const binding = this.getEffectiveBindingState(state);
4248
4540
  const bindingReconnectRequired = this.bindingRequiresReconnect(state);
4249
4541
  const eventSummary = this.getC2EventSyncSummary(state);
4542
+ const discoveredModels = state.detected_models.filter((item) => item.status !== 'ignored').length;
4543
+ const connectedCount = state.model_bindings.filter((bindingItem) => bindingItem.status === 'bound').length;
4250
4544
  return {
4251
4545
  device_paired: state.connect_identity.connect_identity_status === 'paired',
4252
4546
  workspace_id: state.workspace_binding.workspaceId,
@@ -4254,18 +4548,18 @@ class ConnectV1Service {
4254
4548
  binding_state: binding?.binding.state ?? null,
4255
4549
  lifecycle_note: this.getBindingLifecycleNotice(state),
4256
4550
  daemon_status: this.getDaemonStatus().daemon_running ? 'running' : 'stopped',
4257
- models_discovered: state.detected_models.filter((item) => item.status !== 'ignored').length,
4551
+ models_discovered: discoveredModels,
4258
4552
  agents_discovered: state.detected_agents.length,
4259
4553
  runtimes_discovered: state.runtime_passports.length,
4260
4554
  paused_session_count: sessionSummary.paused_session_count,
4261
4555
  revoked_session_count: sessionSummary.revoked_session_count,
4262
4556
  credential_reconnect_needed: bindingReconnectRequired || sessionSummary.credential_reconnect_needed,
4263
- ready_to_connect_count: inbox.summary.ready_to_connect_count,
4264
- needs_confirmation_count: inbox.summary.needs_confirmation_count,
4265
- connected_count: inbox.summary.connected_count,
4557
+ ready_to_connect_count: inbox?.summary.ready_to_connect_count ?? discoveredModels,
4558
+ needs_confirmation_count: inbox?.summary.needs_confirmation_count ?? 0,
4559
+ connected_count: inbox?.summary.connected_count ?? connectedCount,
4266
4560
  c2_sync_pending: eventSummary.pendingSyncable,
4267
4561
  privacy_mode: 'metadata only',
4268
- next_recommended_action: inbox.summary.next_recommended_action,
4562
+ next_recommended_action: inbox?.summary.next_recommended_action ?? null,
4269
4563
  };
4270
4564
  }
4271
4565
  resolveRecommendedNextAction(state) {
@@ -4529,6 +4823,11 @@ class ConnectV1Service {
4529
4823
  const inbox = this.buildSmartRegistrationInbox();
4530
4824
  const state = this.stateStore.readState();
4531
4825
  const candidates = [];
4826
+ const inboxActionableCounts = {
4827
+ inbox_ready_to_connect_count: inbox.summary.ready_to_connect_count,
4828
+ inbox_needs_confirmation_count: inbox.summary.needs_confirmation_count,
4829
+ inbox_actionable_count: inbox.summary.ready_to_connect_count + inbox.summary.needs_confirmation_count,
4830
+ };
4532
4831
  const pulseSummary = state.last_pulse_summary;
4533
4832
  const sessionSummary = this.getC2SessionSummary();
4534
4833
  const pendingDraftEvent = [...state.evidence_events]
@@ -4583,6 +4882,7 @@ class ConnectV1Service {
4583
4882
  suggested_action: 'connect_model',
4584
4883
  severity: scopeMismatchDetected ? 'red' : 'amber',
4585
4884
  metadata: {
4885
+ ...inboxActionableCounts,
4586
4886
  item_id: entry.item_id,
4587
4887
  model_name: entry.display_name,
4588
4888
  runtime_gaid: entry.runtime_gaid,
@@ -4641,6 +4941,7 @@ class ConnectV1Service {
4641
4941
  suggested_action: 'review_agent',
4642
4942
  severity: scopeMismatchDetected ? 'red' : 'amber',
4643
4943
  metadata: {
4944
+ ...inboxActionableCounts,
4644
4945
  item_id: entry.item_id,
4645
4946
  agent_id: entry.agent_id,
4646
4947
  display_name: entry.display_name,
@@ -4675,6 +4976,7 @@ class ConnectV1Service {
4675
4976
  suggested_action: 'open_inbox',
4676
4977
  severity: 'green',
4677
4978
  metadata: {
4979
+ ...inboxActionableCounts,
4678
4980
  item_id: entry.item_id,
4679
4981
  matched_passport_gaid: entry.matched_passport_gaid,
4680
4982
  match_reason: entry.match_reason,
@@ -4705,6 +5007,7 @@ class ConnectV1Service {
4705
5007
  suggested_action: 'open_inbox',
4706
5008
  severity: 'amber',
4707
5009
  metadata: {
5010
+ ...inboxActionableCounts,
4708
5011
  item_id: entry.item_id,
4709
5012
  display_name: entry.display_name,
4710
5013
  relationship_summary: 'Existing Passport metadata changed',
@@ -4810,6 +5113,7 @@ class ConnectV1Service {
4810
5113
  suggested_action: 'open_inbox',
4811
5114
  severity: 'amber',
4812
5115
  metadata: {
5116
+ ...inboxActionableCounts,
4813
5117
  process_name: finding.process_name,
4814
5118
  reason: finding.reason,
4815
5119
  relationship_summary: 'System-detected runtime candidate',
@@ -4837,9 +5141,14 @@ class ConnectV1Service {
4837
5141
  suggested_action: 'view_pulse_history',
4838
5142
  severity: 'red',
4839
5143
  metadata: {
5144
+ runtime_gaid: entry.runtime_gaid ?? null,
4840
5145
  runtime_name: entry.runtime_name,
4841
5146
  model_name: entry.model_name,
4842
5147
  pulse_status: entry.pulse_status,
5148
+ bound_passport_gaid: entry.bound_passport_gaid ?? null,
5149
+ binding_status: entry.binding_status ?? null,
5150
+ registration_state: entry.registration_state ?? null,
5151
+ connection_classification: entry.connection_classification ?? null,
4843
5152
  relationship_summary: 'Existing runtime availability changed',
4844
5153
  source_summary: sourceSummary,
4845
5154
  scope_summary: null,
@@ -4900,8 +5209,205 @@ class ConnectV1Service {
4900
5209
  }
4901
5210
  return candidates;
4902
5211
  }
5212
+ getNotificationMetadataNumber(candidate, key) {
5213
+ const value = candidate.metadata[key];
5214
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
5215
+ }
5216
+ getMaxNotificationMetadataNumber(candidates, key) {
5217
+ return candidates.reduce((max, candidate) => Math.max(max, this.getNotificationMetadataNumber(candidate, key)), 0);
5218
+ }
5219
+ pluralizeNotificationNoun(count, singular, plural = `${singular}s`) {
5220
+ return `${count} ${count === 1 ? singular : plural}`;
5221
+ }
5222
+ buildConnectInboxReviewNotification(candidates) {
5223
+ const reviewCandidates = candidates.filter((candidate) => (candidate.type === 'model_detected'
5224
+ || candidate.type === 'agent_detected'
5225
+ || candidate.type === 'passport_match_suggested'
5226
+ || candidate.type === 'version_candidate_ready'
5227
+ || candidate.type === 'shadow_candidate'));
5228
+ if (reviewCandidates.length === 0)
5229
+ return null;
5230
+ const modelCount = reviewCandidates.filter((candidate) => candidate.type === 'model_detected').length;
5231
+ const agentCount = reviewCandidates.filter((candidate) => candidate.type === 'agent_detected').length;
5232
+ const runtimeCount = reviewCandidates.filter((candidate) => candidate.type === 'shadow_candidate').length;
5233
+ const confirmationCount = reviewCandidates.filter((candidate) => (candidate.type === 'passport_match_suggested'
5234
+ || candidate.type === 'version_candidate_ready')).length;
5235
+ const totalReadyCount = this.getMaxNotificationMetadataNumber(reviewCandidates, 'inbox_ready_to_connect_count');
5236
+ const totalConfirmationCount = this.getMaxNotificationMetadataNumber(reviewCandidates, 'inbox_needs_confirmation_count');
5237
+ const totalActionableCount = this.getMaxNotificationMetadataNumber(reviewCandidates, 'inbox_actionable_count');
5238
+ const hasHiddenInboxItems = totalActionableCount > reviewCandidates.length;
5239
+ const redCount = reviewCandidates.filter((candidate) => candidate.severity === 'red').length;
5240
+ const parts = [
5241
+ modelCount > 0 ? this.pluralizeNotificationNoun(modelCount, 'model') : null,
5242
+ agentCount > 0 ? this.pluralizeNotificationNoun(agentCount, 'agent') : null,
5243
+ runtimeCount > 0 ? this.pluralizeNotificationNoun(runtimeCount, 'runtime') : null,
5244
+ confirmationCount > 0 ? this.pluralizeNotificationNoun(confirmationCount, 'confirmation') : null,
5245
+ ].filter((item) => Boolean(item));
5246
+ const previewSummary = parts.length > 0 ? parts.join(', ') : this.pluralizeNotificationNoun(reviewCandidates.length, 'item');
5247
+ const inboxSummary = [
5248
+ totalReadyCount > 0 ? `${this.pluralizeNotificationNoun(totalReadyCount, 'item')} ready to connect` : null,
5249
+ totalConfirmationCount > 0 ? `${this.pluralizeNotificationNoun(totalConfirmationCount, 'item')} needing confirmation` : null,
5250
+ ].filter((item) => Boolean(item)).join(', ');
5251
+ const itemSummary = hasHiddenInboxItems && inboxSummary
5252
+ ? inboxSummary
5253
+ : previewSummary;
5254
+ const reviewSentence = hasHiddenInboxItems && inboxSummary
5255
+ ? `${this.pluralizeNotificationNoun(totalActionableCount, 'inbox item')} ${totalActionableCount === 1 ? 'needs' : 'need'} approval or confirmation before registration: ${itemSummary}`
5256
+ : `${itemSummary} ${reviewCandidates.length === 1 ? 'needs' : 'need'} approval or confirmation before registration`;
5257
+ return {
5258
+ id: 'connect-inbox-review',
5259
+ type: modelCount > 0 ? 'model_detected' : agentCount > 0 ? 'agent_detected' : 'shadow_candidate',
5260
+ title: 'Review required in Forkit Connect',
5261
+ message: joinNotificationSentences([
5262
+ reviewSentence,
5263
+ 'Nothing has been registered automatically',
5264
+ 'Open Connect to approve, defer, or ignore',
5265
+ ]),
5266
+ suggested_action: 'open_inbox',
5267
+ severity: redCount > 0 ? 'red' : 'amber',
5268
+ metadata: {
5269
+ notification_group: 'connect_inbox_review',
5270
+ grouped_count: reviewCandidates.length,
5271
+ model_count: modelCount,
5272
+ agent_count: agentCount,
5273
+ runtime_count: runtimeCount,
5274
+ confirmation_count: confirmationCount,
5275
+ inbox_ready_to_connect_count: totalReadyCount,
5276
+ inbox_needs_confirmation_count: totalConfirmationCount,
5277
+ inbox_actionable_count: totalActionableCount,
5278
+ red_count: redCount,
5279
+ relationship_summary: 'Connect inbox items need operator review',
5280
+ source_summary: 'Source: Forkit Connect local discovery',
5281
+ scope_summary: null,
5282
+ },
5283
+ };
5284
+ }
5285
+ buildRuntimeAttentionNotification(candidates) {
5286
+ const runtimeCandidates = candidates.filter((candidate) => (candidate.type === 'runtime_unavailable'
5287
+ && candidate.severity === 'red'
5288
+ && (normalizeDisplayText(candidate.metadata.bound_passport_gaid) !== null
5289
+ || candidate.metadata.binding_status === 'bound'
5290
+ || candidate.metadata.registration_state === 'registered'
5291
+ || candidate.metadata.connection_classification === 'registered_runtime')));
5292
+ if (runtimeCandidates.length === 0)
5293
+ return null;
5294
+ const modelCount = runtimeCandidates
5295
+ .filter((candidate) => normalizeDisplayText(candidate.metadata.model_name))
5296
+ .length;
5297
+ return {
5298
+ id: 'runtime-attention',
5299
+ type: 'runtime_unavailable',
5300
+ title: 'Runtime attention required',
5301
+ message: joinNotificationSentences([
5302
+ `${this.pluralizeNotificationNoun(runtimeCandidates.length, 'runtime')} reported an unavailable state`,
5303
+ modelCount > 0 ? `${this.pluralizeNotificationNoun(modelCount, 'model')} may be affected` : null,
5304
+ 'Open Connect to inspect runtime state',
5305
+ ]),
5306
+ suggested_action: 'view_pulse_history',
5307
+ severity: 'red',
5308
+ metadata: {
5309
+ notification_group: 'runtime_attention',
5310
+ grouped_count: runtimeCandidates.length,
5311
+ affected_model_count: modelCount,
5312
+ relationship_summary: 'Existing runtime availability changed',
5313
+ source_summary: 'Source: Connect runtime pulse',
5314
+ scope_summary: null,
5315
+ },
5316
+ };
5317
+ }
5318
+ buildGovernanceControlNotification(candidates) {
5319
+ const controlCandidates = candidates.filter((candidate) => (candidate.type === 'session_revoked'
5320
+ || candidate.type === 'session_paused'));
5321
+ if (controlCandidates.length === 0)
5322
+ return null;
5323
+ const revokedCount = controlCandidates.filter((candidate) => candidate.type === 'session_revoked').length;
5324
+ const pausedCount = controlCandidates.filter((candidate) => candidate.type === 'session_paused').length;
5325
+ const severity = revokedCount > 0 ? 'red' : 'amber';
5326
+ return {
5327
+ id: 'governance-control',
5328
+ type: revokedCount > 0 ? 'session_revoked' : 'session_paused',
5329
+ title: revokedCount > 0 ? 'Governed access was revoked' : 'Governed communication was paused',
5330
+ message: joinNotificationSentences([
5331
+ revokedCount > 0 ? `${this.pluralizeNotificationNoun(revokedCount, 'session')} revoked` : null,
5332
+ pausedCount > 0 ? `${this.pluralizeNotificationNoun(pausedCount, 'session')} paused` : null,
5333
+ 'Review the Connect inbox before continuing governed communication',
5334
+ ]),
5335
+ suggested_action: 'open_inbox',
5336
+ severity,
5337
+ metadata: {
5338
+ notification_group: 'governance_control',
5339
+ grouped_count: controlCandidates.length,
5340
+ revoked_count: revokedCount,
5341
+ paused_count: pausedCount,
5342
+ relationship_summary: 'Existing governed sessions need operator attention',
5343
+ source_summary: 'Source: Forkit governance control',
5344
+ scope_summary: null,
5345
+ },
5346
+ };
5347
+ }
5348
+ buildSyncAttentionNotification(candidates) {
5349
+ const syncCandidate = candidates.find((candidate) => (candidate.type === 'c2_sync_pending'
5350
+ && candidate.severity !== 'grey'));
5351
+ if (!syncCandidate)
5352
+ return null;
5353
+ const pendingEvents = this.getNotificationMetadataNumber(syncCandidate, 'pending_events');
5354
+ return {
5355
+ ...syncCandidate,
5356
+ id: 'c2-sync-attention',
5357
+ title: 'Sync attention required',
5358
+ message: joinNotificationSentences([
5359
+ pendingEvents > 0
5360
+ ? `${this.pluralizeNotificationNoun(pendingEvents, 'metadata update')} could not sync cleanly`
5361
+ : 'A governed metadata sync needs attention',
5362
+ 'Open Connect to review the local queue',
5363
+ ]),
5364
+ metadata: {
5365
+ ...syncCandidate.metadata,
5366
+ notification_group: 'c2_sync_attention',
5367
+ relationship_summary: 'Existing local metadata queue needs sync attention',
5368
+ },
5369
+ };
5370
+ }
5371
+ getNotificationDeliveryCandidates(candidates) {
5372
+ const reconnectCandidate = candidates.find((candidate) => candidate.type === 'credential_reconnect_needed');
5373
+ if (reconnectCandidate) {
5374
+ return [{
5375
+ ...reconnectCandidate,
5376
+ id: 'account-reconnect',
5377
+ title: 'Forkit Connect needs account approval',
5378
+ message: joinNotificationSentences([
5379
+ 'Reconnect this device before governed metadata sync can continue',
5380
+ String(reconnectCandidate.metadata.source_summary || ''),
5381
+ ]),
5382
+ metadata: {
5383
+ ...reconnectCandidate.metadata,
5384
+ notification_group: 'account_reconnect',
5385
+ },
5386
+ }];
5387
+ }
5388
+ const grouped = [];
5389
+ const governanceNotification = this.buildGovernanceControlNotification(candidates);
5390
+ if (governanceNotification)
5391
+ grouped.push(governanceNotification);
5392
+ const runtimeNotification = this.buildRuntimeAttentionNotification(candidates);
5393
+ if (runtimeNotification)
5394
+ grouped.push(runtimeNotification);
5395
+ const inboxReviewNotification = this.buildConnectInboxReviewNotification(candidates);
5396
+ if (inboxReviewNotification)
5397
+ grouped.push(inboxReviewNotification);
5398
+ const syncNotification = this.buildSyncAttentionNotification(candidates);
5399
+ if (syncNotification)
5400
+ grouped.push(syncNotification);
5401
+ return grouped.slice(0, 3);
5402
+ }
5403
+ getNotificationDeliveryPreview(candidates = this.getNotificationCandidates()) {
5404
+ return this.getNotificationDeliveryCandidates(candidates);
5405
+ }
4903
5406
  getNotificationTargetId(candidate) {
4904
5407
  const metadata = candidate.metadata;
5408
+ if (metadata.notification_group) {
5409
+ return String(metadata.notification_group);
5410
+ }
4905
5411
  switch (candidate.type) {
4906
5412
  case 'model_detected':
4907
5413
  return String(metadata.discovery_hash || candidate.id);
@@ -4955,10 +5461,13 @@ class ConnectV1Service {
4955
5461
  const deliveredAt = options?.deliveredAt ?? nowIso();
4956
5462
  let state = this.stateStore.readState();
4957
5463
  const candidates = options?.candidates ?? this.getNotificationCandidates();
5464
+ const deliveryCandidates = options?.force && options?.candidates
5465
+ ? candidates
5466
+ : this.getNotificationDeliveryCandidates(candidates);
4958
5467
  if (!state.connect_config.notifications_enabled && !options?.force) {
4959
5468
  return {
4960
5469
  delivered: 0,
4961
- suppressed: candidates.length,
5470
+ suppressed: deliveryCandidates.length,
4962
5471
  fallback: false,
4963
5472
  disabled: true,
4964
5473
  notifications: [],
@@ -4969,7 +5478,7 @@ class ConnectV1Service {
4969
5478
  let delivered = 0;
4970
5479
  let suppressed = 0;
4971
5480
  let fallback = false;
4972
- for (const candidate of candidates) {
5481
+ for (const candidate of deliveryCandidates) {
4973
5482
  if (!options?.force && this.wasNotificationRecentlyDelivered(state, candidate, deliveredAt)) {
4974
5483
  suppressed += 1;
4975
5484
  continue;
@@ -5784,29 +6293,30 @@ class ConnectV1Service {
5784
6293
  };
5785
6294
  }
5786
6295
  buildBuildSessionSummary(state, session) {
6296
+ const anchoredSession = this.anchorBuildSessionToBinding(state, session);
5787
6297
  const queuedDraft = state.sync_queue.some((item) => item.type === 'passport-draft'
5788
- && String(item.payload?.metadata?.build_session_id || '') === session.build_session_id);
5789
- const draftStatus = session.passport_gaid
6298
+ && String(item.payload?.metadata?.build_session_id || '') === anchoredSession.build_session_id);
6299
+ const draftStatus = anchoredSession.passport_gaid
5790
6300
  ? 'bound'
5791
- : session.draft_id
6301
+ : anchoredSession.draft_id
5792
6302
  ? 'draft_created'
5793
6303
  : queuedDraft
5794
6304
  ? 'draft_queued'
5795
6305
  : 'local_only';
5796
- const latestLifecycleEvent = session.lifecycle_events.at(-1)?.event_type ?? null;
5797
- const c2Status = this.countC2ForBuildSession(state, session.build_session_id);
6306
+ const latestLifecycleEvent = anchoredSession.lifecycle_events.at(-1)?.event_type ?? null;
6307
+ const c2Status = this.countC2ForBuildSession(state, anchoredSession.build_session_id);
5798
6308
  return {
5799
- build_session_id: session.build_session_id,
5800
- model_name: session.model_name,
5801
- framework: session.framework,
5802
- task: session.task,
5803
- build_status: session.status,
6309
+ build_session_id: anchoredSession.build_session_id,
6310
+ model_name: anchoredSession.model_name,
6311
+ framework: anchoredSession.framework,
6312
+ task: anchoredSession.task,
6313
+ build_status: anchoredSession.status,
5804
6314
  draft_status: draftStatus,
5805
- draft_id: session.draft_id,
5806
- passport_gaid: session.passport_gaid,
5807
- dataset_refs_count: session.dataset_refs.length,
5808
- versions_count: session.versions.length,
5809
- metrics_count: session.metrics.length,
6315
+ draft_id: anchoredSession.draft_id,
6316
+ passport_gaid: anchoredSession.passport_gaid,
6317
+ dataset_refs_count: anchoredSession.dataset_refs.length,
6318
+ versions_count: anchoredSession.versions.length,
6319
+ metrics_count: anchoredSession.metrics.length,
5810
6320
  latest_lifecycle_event: latestLifecycleEvent,
5811
6321
  c2_sync_status: c2Status.pending > 0 || c2Status.synced > 0 || c2Status.lastError
5812
6322
  ? {
@@ -5815,14 +6325,16 @@ class ConnectV1Service {
5815
6325
  last_error: c2Status.lastError,
5816
6326
  }
5817
6327
  : null,
5818
- updated_at: session.updated_at,
6328
+ updated_at: anchoredSession.updated_at,
5819
6329
  };
5820
6330
  }
5821
6331
  saveBuildSession(session) {
5822
- this.stateStore.upsertBuildSession({
6332
+ const state = this.stateStore.readState();
6333
+ const anchored = this.anchorBuildSessionToBinding(state, {
5823
6334
  ...session,
5824
6335
  updated_at: session.updated_at || nowIso(),
5825
6336
  });
6337
+ this.stateStore.upsertBuildSession(anchored);
5826
6338
  return this.requireBuildSession(this.stateStore.readState(), session.build_session_id);
5827
6339
  }
5828
6340
  shouldFallbackToQueuedTrainingDraft(status) {
@@ -5862,7 +6374,7 @@ class ConnectV1Service {
5862
6374
  ],
5863
6375
  artifact_refs: [],
5864
6376
  };
5865
- this.saveBuildSession(session);
6377
+ session = this.saveBuildSession(session);
5866
6378
  this.stateStore.addEvidenceEvent({
5867
6379
  type: 'training_session_initialized',
5868
6380
  details: {
@@ -5875,19 +6387,30 @@ class ConnectV1Service {
5875
6387
  this.queueTrainingC2Event(session, 'connect_training_initialized', {
5876
6388
  dataset_refs_count: datasetRefs.length,
5877
6389
  }, now);
5878
- this.recordC2LifecycleEvent({
5879
- eventType: 'connect_passport_draft_requested',
5880
- modelName: session.model_name,
5881
- passportGaid: null,
5882
- workspaceId: session.workspaceId,
5883
- projectId: session.projectId,
5884
- metadata: {
5885
- build_session_id: session.build_session_id,
5886
- source_mode: 'connect_training',
5887
- connection_status: 'building',
5888
- },
5889
- });
6390
+ if (!session.passport_gaid) {
6391
+ this.recordC2LifecycleEvent({
6392
+ eventType: 'connect_passport_draft_requested',
6393
+ modelName: session.model_name,
6394
+ passportGaid: null,
6395
+ workspaceId: session.workspaceId,
6396
+ projectId: session.projectId,
6397
+ metadata: {
6398
+ build_session_id: session.build_session_id,
6399
+ source_mode: 'connect_training',
6400
+ connection_status: 'building',
6401
+ },
6402
+ });
6403
+ }
5890
6404
  const payload = this.buildTrainingDraftPayload(session);
6405
+ if (session.passport_gaid) {
6406
+ return {
6407
+ buildSession: session,
6408
+ draftAction: 'bound',
6409
+ draftId: null,
6410
+ gaid: session.passport_gaid,
6411
+ payload,
6412
+ };
6413
+ }
5891
6414
  if (this.hasSessionRef()) {
5892
6415
  const result = await this.getApiClient(state).createPassportDraft(payload);
5893
6416
  if (!result.ok) {
@@ -6123,13 +6646,21 @@ class ConnectV1Service {
6123
6646
  }
6124
6647
  getTrainingStatus(buildSessionId) {
6125
6648
  const state = this.stateStore.readState();
6649
+ const maybePersistAnchoredSession = (session) => {
6650
+ const anchored = this.anchorBuildSessionToBinding(state, session);
6651
+ if (anchored !== session) {
6652
+ this.stateStore.upsertBuildSession(anchored);
6653
+ return anchored;
6654
+ }
6655
+ return session;
6656
+ };
6126
6657
  if (buildSessionId) {
6127
- const session = this.requireBuildSession(state, buildSessionId);
6658
+ const session = maybePersistAnchoredSession(this.requireBuildSession(state, buildSessionId));
6128
6659
  return this.buildBuildSessionSummary(state, session);
6129
6660
  }
6130
6661
  return [...state.build_sessions]
6131
6662
  .sort((left, right) => right.updated_at.localeCompare(left.updated_at))
6132
- .map((session) => this.buildBuildSessionSummary(state, session));
6663
+ .map((session) => this.buildBuildSessionSummary(state, maybePersistAnchoredSession(session)));
6133
6664
  }
6134
6665
  markBuildSessionDraftPublished(draftId, passportGaid, workspaceId, projectId) {
6135
6666
  const state = this.stateStore.readState();
@@ -7782,6 +8313,7 @@ class ConnectV1Service {
7782
8313
  const result = options?.processScoutResults
7783
8314
  ? await this.scanRuntime({ processScoutResults: options.processScoutResults })
7784
8315
  : await this.scanRuntime();
8316
+ this.syncEvolutionCandidates();
7785
8317
  const queue = result.discoveryMode === 'auto_draft'
7786
8318
  ? this.queueModelDraftsFromDetected()
7787
8319
  : emptyQueueSummary();
@@ -8129,7 +8661,7 @@ class ConnectV1Service {
8129
8661
  const secureStorage = this.credentialStore.getStatus();
8130
8662
  checks.push({
8131
8663
  name: 'secure_storage',
8132
- status: secureStorage.available ? 'PASS' : 'FAIL',
8664
+ status: !secureStorage.available ? 'FAIL' : secureStorage.plaintextFallbackActive ? 'WARN' : 'PASS',
8133
8665
  details: secureStorage.legacyPlaintextFilePresent
8134
8666
  ? `${secureStorage.detail} Legacy plaintext credential file still detected at ${this.credentialStore.getCredentialFilePath()}.`
8135
8667
  : secureStorage.detail,
@@ -8165,6 +8697,11 @@ class ConnectV1Service {
8165
8697
  status: 'WARN',
8166
8698
  details: 'No session reference stored. Set FORKIT_CONNECT_SESSION_REF or run connect with session binding.',
8167
8699
  });
8700
+ checks.push({
8701
+ name: 'backend_session_truth',
8702
+ status: 'WARN',
8703
+ details: 'session_state=missing. Local scope (if present) is cached-only until login verifies account truth.',
8704
+ });
8168
8705
  return checks;
8169
8706
  }
8170
8707
  checks.push({
@@ -8174,11 +8711,39 @@ class ConnectV1Service {
8174
8711
  ? 'Using session reference from FORKIT_CONNECT_SESSION_REF. Headless mode can run without local credential persistence.'
8175
8712
  : 'Secure session reference is present in local credential storage.',
8176
8713
  });
8714
+ const api = new api_1.ConnectApiClient({
8715
+ baseUrl: DEFAULT_BASE_URL,
8716
+ sessionRef: this.readSessionRef(),
8717
+ });
8718
+ let sessionTruth = 'unavailable';
8719
+ let profileStatus = null;
8720
+ try {
8721
+ const profileAccess = await api.getProfileAccess();
8722
+ profileStatus = profileAccess.status;
8723
+ if (profileAccess.ok) {
8724
+ sessionTruth = 'authorized';
8725
+ }
8726
+ else if (profileAccess.status === 401 || profileAccess.status === 403) {
8727
+ sessionTruth = 'expired';
8728
+ }
8729
+ }
8730
+ catch {
8731
+ sessionTruth = 'unavailable';
8732
+ profileStatus = null;
8733
+ }
8734
+ checks.push({
8735
+ name: 'backend_session_truth',
8736
+ status: sessionTruth === 'authorized' ? 'PASS' : 'WARN',
8737
+ details: sessionTruth === 'authorized'
8738
+ ? `session_state=authorized via GET /api/profiles/access (${profileStatus ?? 'ok'})`
8739
+ : sessionTruth === 'expired'
8740
+ ? `session_state=expired via GET /api/profiles/access (${profileStatus ?? 'unknown'}). Renew login before governed actions.`
8741
+ : `session_state=unavailable via GET /api/profiles/access (${profileStatus ?? 'timeout_or_network_error'}). Account truth is temporarily offline.`,
8742
+ });
8743
+ if (sessionTruth !== 'authorized') {
8744
+ return checks;
8745
+ }
8177
8746
  try {
8178
- const api = new api_1.ConnectApiClient({
8179
- baseUrl: DEFAULT_BASE_URL,
8180
- sessionRef: this.readSessionRef(),
8181
- });
8182
8747
  const mine = await api.getPassportsMine();
8183
8748
  checks.push({
8184
8749
  name: 'backend_passports_mine',
@@ -8228,6 +8793,11 @@ class ConnectV1Service {
8228
8793
  });
8229
8794
  return true;
8230
8795
  }
8796
+ ignoreDetectedModel(selector) {
8797
+ const state = this.stateStore.readState();
8798
+ const model = this.resolveModelSelection(selector, state);
8799
+ return this.markModelIgnored(model.model, model.digest);
8800
+ }
8231
8801
  deferDetectedModel(selector, deferHours = 24) {
8232
8802
  const state = this.stateStore.readState();
8233
8803
  const model = this.resolveModelSelection(selector, state);
@@ -8269,6 +8839,9 @@ class ConnectV1Service {
8269
8839
  this.stateStore.replaceRuntimePassports(nextRuntimePassports);
8270
8840
  return true;
8271
8841
  }
8842
+ ignoreRuntimeSuggestion(selector) {
8843
+ return this.markRuntimeIgnored(selector);
8844
+ }
8272
8845
  notImplemented(feature) {
8273
8846
  throw new Error(`NOT_IMPLEMENTED: ${feature}`);
8274
8847
  }