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/README.md +32 -4
- package/dist/cli.js +390 -99
- package/dist/launcher.js +1451 -15
- package/dist/v1/api.d.ts +62 -1
- package/dist/v1/api.js +131 -2
- package/dist/v1/credential-store.d.ts +1 -0
- package/dist/v1/credential-store.js +15 -3
- package/dist/v1/service.d.ts +57 -3
- package/dist/v1/service.js +633 -60
- package/dist/v1/state.d.ts +1 -0
- package/dist/v1/state.js +32 -12
- package/dist/v1/types.d.ts +55 -1
- package/package.json +9 -9
package/dist/v1/service.js
CHANGED
|
@@ -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
|
-
|
|
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 === '
|
|
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
|
-
|
|
132
|
-
params.set('focus', 'item');
|
|
143
|
+
routeToCommandReview();
|
|
133
144
|
}
|
|
134
|
-
else if (candidate.suggested_action === 'view_pulse_history'
|
|
135
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
4264
|
-
needs_confirmation_count: inbox
|
|
4265
|
-
connected_count: inbox
|
|
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
|
|
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:
|
|
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
|
|
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 || '') ===
|
|
5789
|
-
const draftStatus =
|
|
6298
|
+
&& String(item.payload?.metadata?.build_session_id || '') === anchoredSession.build_session_id);
|
|
6299
|
+
const draftStatus = anchoredSession.passport_gaid
|
|
5790
6300
|
? 'bound'
|
|
5791
|
-
:
|
|
6301
|
+
: anchoredSession.draft_id
|
|
5792
6302
|
? 'draft_created'
|
|
5793
6303
|
: queuedDraft
|
|
5794
6304
|
? 'draft_queued'
|
|
5795
6305
|
: 'local_only';
|
|
5796
|
-
const latestLifecycleEvent =
|
|
5797
|
-
const c2Status = this.countC2ForBuildSession(state,
|
|
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:
|
|
5800
|
-
model_name:
|
|
5801
|
-
framework:
|
|
5802
|
-
task:
|
|
5803
|
-
build_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:
|
|
5806
|
-
passport_gaid:
|
|
5807
|
-
dataset_refs_count:
|
|
5808
|
-
versions_count:
|
|
5809
|
-
metrics_count:
|
|
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:
|
|
6328
|
+
updated_at: anchoredSession.updated_at,
|
|
5819
6329
|
};
|
|
5820
6330
|
}
|
|
5821
6331
|
saveBuildSession(session) {
|
|
5822
|
-
this.stateStore.
|
|
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
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
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 ? '
|
|
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
|
}
|