gestalt-mobile 0.35.5 → 0.36.1
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 +60 -3
- package/dist/client/assets/index-9XMv2BvD.js +38 -0
- package/dist/client/assets/{index-CPQBJDsg.css → index-Bxv5Z2w4.css} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/client/features/chat/activity-summary.js +71 -0
- package/dist/server/client/features/chat/chat-projection.js +722 -0
- package/dist/server/client/features/chat/message-store.js +30 -0
- package/dist/server/server/app.js +1 -1
- package/dist/server/server/cli.js +8 -0
- package/dist/server/server/composition.contracts.js +193 -10
- package/dist/server/server/composition.js +283 -59
- package/dist/server/server/features/autopilot/register-routes.js +2 -2
- package/dist/server/server/features/autopilot/toggle/endpoint.js +5 -1
- package/dist/server/server/features/catalog/get-bootstrap/use-case.js +5 -3
- package/dist/server/server/features/org-plan-checkpoint/application/validate.js +40 -0
- package/dist/server/server/features/sessions/get-session/endpoint.js +1 -1
- package/dist/server/server/features/sessions/list-models/endpoint.js +16 -0
- package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +8 -3
- package/dist/server/server/features/sessions/list-sessions/endpoint.js +1 -1
- package/dist/server/server/features/sessions/model/relay-session.js +2 -0
- package/dist/server/server/features/sessions/promote-recent-thread/endpoint.js +6 -2
- package/dist/server/server/features/sessions/promote-recent-thread/use-case.js +7 -2
- package/dist/server/server/features/sessions/register-routes.js +8 -3
- package/dist/server/server/features/sessions/respond-interaction/endpoint.js +1 -1
- package/dist/server/server/features/sessions/select-model/endpoint.js +1 -1
- package/dist/server/server/features/sessions/start-session/request.js +1 -0
- package/dist/server/server/features/sessions/start-session/use-case.js +21 -7
- package/dist/server/server/features/skills/list-available/endpoint.js +4 -2
- package/dist/server/server/platform/catalog/provider-model-catalog.js +15 -0
- package/dist/server/server/platform/codex/server-request.js +14 -0
- package/dist/server/server/platform/codex/session-runtime.js +5 -1
- package/dist/server/server/platform/kimi/kimi-activity-facts.js +88 -0
- package/dist/server/server/platform/kimi/kimi-errors.js +30 -0
- package/dist/server/server/platform/kimi/kimi-model-catalog.js +63 -0
- package/dist/server/server/platform/kimi/kimi-normalizer.js +196 -0
- package/dist/server/server/platform/kimi/kimi-recent-session-lister.js +68 -0
- package/dist/server/server/platform/kimi/kimi-server-request.js +143 -0
- package/dist/server/server/platform/kimi/kimi-session-runtime.js +653 -0
- package/dist/server/server/platform/kimi/kimi-share-dir.js +65 -0
- package/dist/server/server/platform/kimi/kimi-skill-catalog.js +79 -0
- package/dist/server/server/platform/kimi/kimi-skills-materializer.js +41 -0
- package/dist/server/server/platform/kimi/kimi-web-client.js +63 -0
- package/dist/server/server/platform/kimi/kimi-web-server-manager.js +215 -0
- package/dist/server/server/platform/kimi/kimi-ws-client.js +154 -0
- package/dist/server/server/platform/persistence/migrate.js +2 -0
- package/dist/server/server/platform/persistence/sqlite-session-repository.js +3 -2
- package/dist/server/server/platform/skills/cached-skill-catalog.js +0 -0
- package/dist/server/shared/contracts/llm-provider.js +6 -0
- package/dist/server/shared/contracts/org-plan-attention.js +15 -1
- package/package.json +4 -3
- package/dist/client/assets/index-SZM7lpix.js +0 -38
|
@@ -13,9 +13,18 @@ import { FilesystemWorkspaceFiles } from './platform/filesystem/filesystem-works
|
|
|
13
13
|
import { protocolCompatibility } from './platform/codex/protocol-compatibility.js';
|
|
14
14
|
import { launchCodexAppServer } from './platform/codex/codex-process-launcher.js';
|
|
15
15
|
import { CodexModelCatalog } from './platform/codex/codex-model-catalog.js';
|
|
16
|
+
import { ProviderModelCatalog } from './platform/catalog/provider-model-catalog.js';
|
|
16
17
|
import { createRecentThreadLister } from './platform/codex/recent-thread-lister.js';
|
|
17
18
|
import { CodexSessionRuntime } from './platform/codex/session-runtime.js';
|
|
18
19
|
import { normalizeCodexNotification } from './platform/codex/normalizer.js';
|
|
20
|
+
import { KimiSessionRuntime } from './platform/kimi/kimi-session-runtime.js';
|
|
21
|
+
import { KimiWebServerManager } from './platform/kimi/kimi-web-server-manager.js';
|
|
22
|
+
import { KimiModelCatalog } from './platform/kimi/kimi-model-catalog.js';
|
|
23
|
+
import { KimiSkillCatalog } from './platform/kimi/kimi-skill-catalog.js';
|
|
24
|
+
import { createKimiRecentSessionLister } from './platform/kimi/kimi-recent-session-lister.js';
|
|
25
|
+
import { kimiStateBase } from './platform/kimi/kimi-share-dir.js';
|
|
26
|
+
import { normalizeKimiEvent } from './platform/kimi/kimi-normalizer.js';
|
|
27
|
+
import { decodeKimiActivityFacts } from './platform/kimi/kimi-activity-facts.js';
|
|
19
28
|
import { isMissingCodexThreadRollout } from './platform/codex/json-rpc-client.js';
|
|
20
29
|
import { migrate } from './platform/persistence/migrate.js';
|
|
21
30
|
import { openRelayDatabase } from './platform/persistence/sqlite.js';
|
|
@@ -37,9 +46,9 @@ import { RelaySession, } from './features/sessions/model/relay-session.js';
|
|
|
37
46
|
import { AgentActivityRegistry } from './features/agent-activity/registry.js';
|
|
38
47
|
import { toAgentActivityDto } from './features/agent-activity/activity-dto.js';
|
|
39
48
|
import { decodeAgentActivityFacts } from './platform/codex/activity-facts.js';
|
|
40
|
-
import { completedCommandId, isAgentCapacityRecoveryCall, isAutopilotWaitLeaseCall, resolvedServerRequestId, toPendingInteraction, } from './platform/codex/server-request.js';
|
|
49
|
+
import { completedCommandId, compactOrgPlanCheckpointKind, isAgentCapacityRecoveryCall, isAutopilotWaitLeaseCall, resolvedServerRequestId, toPendingInteraction, } from './platform/codex/server-request.js';
|
|
41
50
|
import { parseOrgPlanCheckpoint, toOrgPlanCheckpointToolResponse, } from '../shared/contracts/org-plan-checkpoint.js';
|
|
42
|
-
import { validOrgPlanCheckpoint } from './features/org-plan-checkpoint/application/validate.js';
|
|
51
|
+
import { resolveOrgPlanCheckpointSignal, validOrgPlanCheckpoint, } from './features/org-plan-checkpoint/application/validate.js';
|
|
43
52
|
import { isValidInteractionResponse, isValidQuizInteractionResponse, } from './features/sessions/interaction/response-validator.js';
|
|
44
53
|
import { promoteRecentThread } from './features/sessions/promote-recent-thread/use-case.js';
|
|
45
54
|
import { FilesystemSkillProfileStore } from './platform/skills/filesystem-skill-profile-store.js';
|
|
@@ -139,7 +148,30 @@ export async function composeRelayApp(options) {
|
|
|
139
148
|
((sessionId, code) => console.warn(`agent activity ${code} session=${sessionId}`)),
|
|
140
149
|
reconcile: async (sessionId) => {
|
|
141
150
|
const session = sessions.find(sessionId);
|
|
142
|
-
if (!session
|
|
151
|
+
if (!session)
|
|
152
|
+
return;
|
|
153
|
+
// Kimi sessions own no child-process projection; one detached history
|
|
154
|
+
// read supplies the whole reconciliation, mirroring the codex tail.
|
|
155
|
+
if (session.provider === 'kimi') {
|
|
156
|
+
if (!kimiRuntime)
|
|
157
|
+
return;
|
|
158
|
+
const history = await kimiRuntime.readHistory(session);
|
|
159
|
+
// Forget may commit while the detached history reader is still in
|
|
160
|
+
// flight. Revalidate ownership before handing its result to the
|
|
161
|
+
// registry, whose publisher intentionally keeps the journal strict.
|
|
162
|
+
if (!sessions.find(sessionId))
|
|
163
|
+
return;
|
|
164
|
+
const occurredAt = new Date().toISOString();
|
|
165
|
+
activity.observe({
|
|
166
|
+
sessionId,
|
|
167
|
+
occurredAt,
|
|
168
|
+
kind: history.activeTurnId ? 'turnStarted' : 'turnCompleted',
|
|
169
|
+
...(session.threadId ? { threadId: session.threadId } : {}),
|
|
170
|
+
...(history.activeTurnId ? { turnId: history.activeTurnId } : {}),
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (!runtime)
|
|
143
175
|
return;
|
|
144
176
|
const history = await runtime.readHistory(session);
|
|
145
177
|
// Forget may commit while the detached history reader is still in
|
|
@@ -327,13 +359,39 @@ export async function composeRelayApp(options) {
|
|
|
327
359
|
options.onAutopilotCoordinator?.(autopilot);
|
|
328
360
|
const workspaces = new FilesystemWorkspaceCatalog(root);
|
|
329
361
|
const workspaceFiles = new FilesystemWorkspaceFiles();
|
|
330
|
-
const
|
|
362
|
+
const homeDirectory = options.homeDirectory ?? homedir();
|
|
363
|
+
// Constructing the manager is cheap (no spawn until `ensure`); building it
|
|
364
|
+
// before the model catalog lets the catalog reuse the gestalt-owned servers.
|
|
365
|
+
const kimiManager = options.kimiServerManager ??
|
|
366
|
+
(options.installedKimiVersion != null && options.startAppServers
|
|
367
|
+
? new KimiWebServerManager({
|
|
368
|
+
stateBase: kimiStateBase(homeDirectory),
|
|
369
|
+
sourceShareDir: join(homeDirectory, '.kimi-code'),
|
|
370
|
+
})
|
|
371
|
+
: undefined);
|
|
372
|
+
const kimiModels = new KimiModelCatalog(kimiManager ?? null, kimiManager != null);
|
|
373
|
+
const models = new ProviderModelCatalog({
|
|
374
|
+
codex: options.installedCodexVersion === null
|
|
375
|
+
? { list: async () => [] }
|
|
376
|
+
: new CodexModelCatalog(root, options.launchAppServer ?? launchCodexAppServer),
|
|
377
|
+
kimi: kimiModels,
|
|
378
|
+
});
|
|
379
|
+
const sessionModels = options.sessionModelCatalog ?? {
|
|
380
|
+
list: (provider) => (provider === 'kimi' ? kimiModels.listForSession() : models.list(provider)),
|
|
381
|
+
};
|
|
331
382
|
const skillProfiles = new FilesystemSkillProfileStore(options.homeDirectory ?? homedir());
|
|
332
|
-
|
|
333
|
-
|
|
383
|
+
// kimi discovery is workspace-scoped, not profile-scoped: the profile only
|
|
384
|
+
// decides which discovered skills a session's server materializes.
|
|
385
|
+
const kimiSkillCatalog = new KimiSkillCatalog(kimiManager ?? null, kimiManager != null);
|
|
386
|
+
const skillCatalog = (provider, profile) => provider === 'kimi'
|
|
387
|
+
? kimiSkillCatalog
|
|
388
|
+
: new CodexSkillCatalog(profile, options.launchAppServer ?? launchCodexAppServer);
|
|
389
|
+
const editorSkillCatalog = new CachedSkillCatalog((provider, profile, workspace) => skillCatalog(provider, profile).list(workspace));
|
|
334
390
|
const workspacePlanCatalog = new FilesystemWorkspacePlanCatalog();
|
|
335
391
|
const resolveSkills = async (session) => {
|
|
336
|
-
const catalog =
|
|
392
|
+
const catalog = session.provider === 'kimi'
|
|
393
|
+
? await kimiSkillCatalog.list(session.workspacePath)
|
|
394
|
+
: await skillCatalog('codex', session.profile).list(session.workspacePath);
|
|
337
395
|
if (session.effectiveSkillSelection)
|
|
338
396
|
return compileSkillOverride({
|
|
339
397
|
discovered: catalog.skills,
|
|
@@ -353,6 +411,15 @@ export async function composeRelayApp(options) {
|
|
|
353
411
|
profiles: options.profiles,
|
|
354
412
|
launch: options.launchAppServer ?? launchCodexAppServer,
|
|
355
413
|
});
|
|
414
|
+
const kimiRecentSessions = createKimiRecentSessionLister({
|
|
415
|
+
servers: kimiManager ?? null,
|
|
416
|
+
available: kimiManager != null,
|
|
417
|
+
});
|
|
418
|
+
// The relay's recent list spans both llm services; execution-policy metadata
|
|
419
|
+
// stays a codex rollout concern, so the codex lister remains the source.
|
|
420
|
+
const recentThreadsForRelay = {
|
|
421
|
+
list: async () => [...(await recentThreads.list()), ...(await kimiRecentSessions.list())],
|
|
422
|
+
};
|
|
356
423
|
const recoverExecutionPolicy = async (session) => {
|
|
357
424
|
if (session.executionPolicy || !session.threadId)
|
|
358
425
|
return session;
|
|
@@ -362,9 +429,20 @@ export async function composeRelayApp(options) {
|
|
|
362
429
|
: session;
|
|
363
430
|
};
|
|
364
431
|
const protocol = protocolCompatibility(options.installedCodexVersion, generatedProtocolVersion);
|
|
432
|
+
const providerAvailability = {
|
|
433
|
+
codex: {
|
|
434
|
+
available: options.installedCodexVersion !== null && protocol.compatible,
|
|
435
|
+
...(options.installedCodexVersion === null ? {} : { version: options.installedCodexVersion }),
|
|
436
|
+
},
|
|
437
|
+
kimi: {
|
|
438
|
+
available: options.installedKimiVersion != null,
|
|
439
|
+
...(options.installedKimiVersion ? { version: options.installedKimiVersion } : {}),
|
|
440
|
+
},
|
|
441
|
+
};
|
|
365
442
|
const gitFetches = new GitFetchCoordinator(fetchUpstream);
|
|
366
443
|
const gitSummaries = new GitSummaryCache(inspectGit);
|
|
367
444
|
let recoverExitedSession = () => { };
|
|
445
|
+
let recoverExitedKimiSession = () => { };
|
|
368
446
|
let scheduleAgentCapacityRecovery = () => false;
|
|
369
447
|
let planMeasurementRefresh;
|
|
370
448
|
const publishInteractionResolved = (sessionId, requestId, occurredAt, outcome) => {
|
|
@@ -486,6 +564,36 @@ export async function composeRelayApp(options) {
|
|
|
486
564
|
};
|
|
487
565
|
let closing = false;
|
|
488
566
|
let runtime = null;
|
|
567
|
+
let kimiRuntime = null;
|
|
568
|
+
/**
|
|
569
|
+
* Shared acceptance path for a provider-neutral pending interaction: it
|
|
570
|
+
* enters the durable store, requests human attention, and notifies autopilot.
|
|
571
|
+
* Both the codex JSON-RPC handler and the kimi polling handler land here.
|
|
572
|
+
*/
|
|
573
|
+
const acceptPendingInteraction = (sessionId, rawInteraction, session) => {
|
|
574
|
+
const interaction = {
|
|
575
|
+
...rawInteraction,
|
|
576
|
+
turnId: session.activeTurnId,
|
|
577
|
+
requestedAt: new Date().toISOString(),
|
|
578
|
+
};
|
|
579
|
+
interactions.add(sessionId, interaction);
|
|
580
|
+
activity.observe({
|
|
581
|
+
sessionId,
|
|
582
|
+
occurredAt: interaction.requestedAt,
|
|
583
|
+
kind: 'interactionPending',
|
|
584
|
+
...(interaction.kind === 'orgPlanAttention'
|
|
585
|
+
? { attentionReason: interaction.payload.reason }
|
|
586
|
+
: {}),
|
|
587
|
+
});
|
|
588
|
+
autopilot.semanticEvent(sessionId, 'interactionChanged');
|
|
589
|
+
const updated = RelaySession.rehydrate(session).requestInteraction(interaction, new Date().toISOString()).snapshot;
|
|
590
|
+
sessions.save(updated);
|
|
591
|
+
events.publish(journal.append(sessionId, 'interaction.requested', interaction, updated.updatedAt));
|
|
592
|
+
if (interaction.kind === 'orgPlanAttention')
|
|
593
|
+
events.publish(journal.append(sessionId, 'org-plan.attention-required', interaction, updated.updatedAt));
|
|
594
|
+
autopilot.evaluate(sessionId);
|
|
595
|
+
return true;
|
|
596
|
+
};
|
|
489
597
|
const acceptPlanUpdate = (sessionId, update) => {
|
|
490
598
|
if (closing)
|
|
491
599
|
return;
|
|
@@ -568,10 +676,24 @@ export async function composeRelayApp(options) {
|
|
|
568
676
|
if (completedSession)
|
|
569
677
|
events.publish(journal.append(sessionId, 'session.updated', completedSession, occurredAt));
|
|
570
678
|
}, (sessionId, request, origin) => {
|
|
571
|
-
|
|
679
|
+
let rawInteraction = toPendingInteraction(request);
|
|
572
680
|
const session = withPendingInteractions(sessions.find(sessionId));
|
|
573
681
|
if (!session)
|
|
574
682
|
return false;
|
|
683
|
+
const compactCheckpointKind = compactOrgPlanCheckpointKind(request);
|
|
684
|
+
if (!rawInteraction && compactCheckpointKind) {
|
|
685
|
+
const retained = supervisedPlans.find(sessionId);
|
|
686
|
+
const identity = supervisedPlans.identity(sessionId);
|
|
687
|
+
const checkpoint = retained && identity
|
|
688
|
+
? resolveOrgPlanCheckpointSignal(compactCheckpointKind, retained, identity)
|
|
689
|
+
: null;
|
|
690
|
+
if (checkpoint)
|
|
691
|
+
rawInteraction = {
|
|
692
|
+
requestId: String(request.id),
|
|
693
|
+
kind: 'orgPlanCheckpoint',
|
|
694
|
+
payload: checkpoint,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
575
697
|
if (!rawInteraction && isAgentCapacityRecoveryCall(request)) {
|
|
576
698
|
const rootOwned = origin.kind === 'root' && origin.physicalTurnId === session.activeTurnId;
|
|
577
699
|
return (rootOwned &&
|
|
@@ -668,28 +790,7 @@ export async function composeRelayApp(options) {
|
|
|
668
790
|
// is cancelled solely by the JSON-RPC write-settlement callback.
|
|
669
791
|
return true;
|
|
670
792
|
}
|
|
671
|
-
|
|
672
|
-
...rawInteraction,
|
|
673
|
-
turnId: session.activeTurnId,
|
|
674
|
-
requestedAt: new Date().toISOString(),
|
|
675
|
-
};
|
|
676
|
-
interactions.add(sessionId, interaction);
|
|
677
|
-
activity.observe({
|
|
678
|
-
sessionId,
|
|
679
|
-
occurredAt: interaction.requestedAt,
|
|
680
|
-
kind: 'interactionPending',
|
|
681
|
-
...(interaction.kind === 'orgPlanAttention'
|
|
682
|
-
? { attentionReason: interaction.payload.reason }
|
|
683
|
-
: {}),
|
|
684
|
-
});
|
|
685
|
-
autopilot.semanticEvent(sessionId, 'interactionChanged');
|
|
686
|
-
const updated = RelaySession.rehydrate(session).requestInteraction(interaction, new Date().toISOString()).snapshot;
|
|
687
|
-
sessions.save(updated);
|
|
688
|
-
events.publish(journal.append(sessionId, 'interaction.requested', interaction, updated.updatedAt));
|
|
689
|
-
if (interaction.kind === 'orgPlanAttention')
|
|
690
|
-
events.publish(journal.append(sessionId, 'org-plan.attention-required', interaction, updated.updatedAt));
|
|
691
|
-
autopilot.evaluate(sessionId);
|
|
692
|
-
return true;
|
|
793
|
+
return acceptPendingInteraction(sessionId, rawInteraction, session);
|
|
693
794
|
}, (sessionId) => {
|
|
694
795
|
activity.disconnected(sessionId, new Date().toISOString());
|
|
695
796
|
failPendingCheckpointHandoffs(sessionId, false);
|
|
@@ -697,6 +798,59 @@ export async function composeRelayApp(options) {
|
|
|
697
798
|
recoverExitedSession(sessionId);
|
|
698
799
|
}, resolveSkills, planStatusSource, acceptPlanUpdate, options.planMeasurementBaseUrl, 30_000, 64, root)
|
|
699
800
|
: null;
|
|
801
|
+
const handleKimiNotification = (sessionId, event) => {
|
|
802
|
+
const occurredAt = new Date().toISOString();
|
|
803
|
+
const session = sessions.find(sessionId);
|
|
804
|
+
const context = kimiRuntime?.eventContext(sessionId) ?? {
|
|
805
|
+
resolveTurnId: () => null,
|
|
806
|
+
isChildAgent: () => false,
|
|
807
|
+
};
|
|
808
|
+
for (const fact of decodeKimiActivityFacts(sessionId, occurredAt, event, context))
|
|
809
|
+
activity.observe(fact);
|
|
810
|
+
const normalized = normalizeKimiEvent(sessionId, 0, occurredAt, event, {
|
|
811
|
+
...context,
|
|
812
|
+
workspacePath: session?.workspacePath,
|
|
813
|
+
activeTurnId: session?.activeTurnId,
|
|
814
|
+
});
|
|
815
|
+
if (!normalized)
|
|
816
|
+
return;
|
|
817
|
+
let completedSession;
|
|
818
|
+
if (normalized.type === 'turnCompleted' || normalized.type === 'turnInterrupted') {
|
|
819
|
+
const turnId = normalized.payload.turn?.id;
|
|
820
|
+
if (session && turnId && session.activeTurnId === turnId) {
|
|
821
|
+
completedSession = RelaySession.rehydrate(session).completeTurn(turnId, occurredAt).snapshot;
|
|
822
|
+
sessions.save(completedSession);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
events.publish(journal.append(sessionId, normalized.type, normalized.payload, occurredAt));
|
|
826
|
+
if (completedSession)
|
|
827
|
+
events.publish(journal.append(sessionId, 'session.updated', completedSession, occurredAt));
|
|
828
|
+
};
|
|
829
|
+
const handleKimiServerRequest = (sessionId, request) => {
|
|
830
|
+
const rawInteraction = toPendingInteraction(request);
|
|
831
|
+
const session = withPendingInteractions(sessions.find(sessionId));
|
|
832
|
+
return Boolean(rawInteraction && session && acceptPendingInteraction(sessionId, rawInteraction, session));
|
|
833
|
+
};
|
|
834
|
+
kimiRuntime = kimiManager
|
|
835
|
+
? new KimiSessionRuntime({
|
|
836
|
+
servers: kimiManager,
|
|
837
|
+
skillsFor: async (session) => {
|
|
838
|
+
const config = await resolveSkills(session);
|
|
839
|
+
return config?.map((entry) => ({
|
|
840
|
+
name: basename(entry.path),
|
|
841
|
+
path: entry.path,
|
|
842
|
+
enabled: entry.enabled,
|
|
843
|
+
}));
|
|
844
|
+
},
|
|
845
|
+
onNotification: (sessionId, event) => handleKimiNotification(sessionId, event),
|
|
846
|
+
onServerRequest: (sessionId, request) => handleKimiServerRequest(sessionId, request),
|
|
847
|
+
onExit: (sessionId) => {
|
|
848
|
+
activity.disconnected(sessionId, new Date().toISOString());
|
|
849
|
+
dismissPendingInteractions(sessionId, new Date().toISOString(), 'failed');
|
|
850
|
+
recoverExitedKimiSession(sessionId);
|
|
851
|
+
},
|
|
852
|
+
})
|
|
853
|
+
: null;
|
|
700
854
|
runtime?.onServerResponseSettled((sessionId, requestId, outcome) => {
|
|
701
855
|
const interaction = interactions.find(sessionId, requestId);
|
|
702
856
|
if (interaction?.kind !== 'orgPlanCheckpoint')
|
|
@@ -726,13 +880,27 @@ export async function composeRelayApp(options) {
|
|
|
726
880
|
prior.state !== 'ready' &&
|
|
727
881
|
prior.state !== 'turnActive' &&
|
|
728
882
|
(session.state === 'ready' || session.state === 'turnActive');
|
|
729
|
-
if (runtime &&
|
|
883
|
+
if ((runtime || kimiRuntime) &&
|
|
730
884
|
session.threadId &&
|
|
731
885
|
(!prior || prior.threadId !== session.threadId || becameRuntimeReady))
|
|
732
886
|
void activity.refresh(session.id);
|
|
733
887
|
if (becameRuntimeReady)
|
|
734
888
|
autopilot.restore(session.id);
|
|
735
889
|
};
|
|
890
|
+
if (kimiRuntime) {
|
|
891
|
+
// A gestalt-owned kimi web process exited or its websocket dropped: stop
|
|
892
|
+
// the durable session like the codex supervisor does, without a resume
|
|
893
|
+
// retry (a later activation reattaches through the normal writer path).
|
|
894
|
+
recoverExitedKimiSession = (sessionId) => {
|
|
895
|
+
const session = sessions.find(sessionId);
|
|
896
|
+
if (session) {
|
|
897
|
+
autopilot.cancel(sessionId, 'sessionEnded');
|
|
898
|
+
saveSession(RelaySession.rehydrate(session).stop(new Date().toISOString()).snapshot);
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
/** Routes a session to the runtime owning its provider's llm service. */
|
|
903
|
+
const ownerRuntime = (session) => session.provider === 'kimi' ? kimiRuntime : runtime;
|
|
736
904
|
if (runtime) {
|
|
737
905
|
const capacityRecoveries = new Set();
|
|
738
906
|
scheduleAgentCapacityRecovery = (sessionId, acknowledge) => {
|
|
@@ -842,6 +1010,7 @@ export async function composeRelayApp(options) {
|
|
|
842
1010
|
protocolVersion: generatedProtocolVersion,
|
|
843
1011
|
compatible: protocol.compatible,
|
|
844
1012
|
},
|
|
1013
|
+
providers: providerAvailability,
|
|
845
1014
|
};
|
|
846
1015
|
},
|
|
847
1016
|
},
|
|
@@ -859,7 +1028,7 @@ export async function composeRelayApp(options) {
|
|
|
859
1028
|
},
|
|
860
1029
|
logger: console,
|
|
861
1030
|
staticDir: options.staticDir,
|
|
862
|
-
recentThreads,
|
|
1031
|
+
recentThreads: recentThreadsForRelay,
|
|
863
1032
|
bootstrap: {
|
|
864
1033
|
workspaces,
|
|
865
1034
|
profiles: options.profiles,
|
|
@@ -875,6 +1044,7 @@ export async function composeRelayApp(options) {
|
|
|
875
1044
|
},
|
|
876
1045
|
versions: options.componentVersions,
|
|
877
1046
|
protocolCompatible: protocol.compatible,
|
|
1047
|
+
providers: providerAvailability,
|
|
878
1048
|
},
|
|
879
1049
|
sessionRoutes: {
|
|
880
1050
|
createId: randomUUID,
|
|
@@ -888,30 +1058,40 @@ export async function composeRelayApp(options) {
|
|
|
888
1058
|
skillProfiles,
|
|
889
1059
|
skillCatalog,
|
|
890
1060
|
defaultSkillProfile: options.explicitSkillProfile,
|
|
891
|
-
activate: runtime
|
|
1061
|
+
activate: runtime || kimiRuntime
|
|
892
1062
|
? async (session) => {
|
|
893
1063
|
const now = new Date().toISOString();
|
|
894
1064
|
dismissPendingInteractions(session.id, now);
|
|
895
|
-
const
|
|
896
|
-
|
|
1065
|
+
const owner = ownerRuntime(session);
|
|
1066
|
+
if (!owner)
|
|
1067
|
+
throw new Error('LLM_RUNTIME_UNAVAILABLE');
|
|
1068
|
+
return owner.start(session, now);
|
|
897
1069
|
}
|
|
898
1070
|
: undefined,
|
|
899
|
-
startTurn: runtime
|
|
1071
|
+
startTurn: runtime || kimiRuntime
|
|
900
1072
|
? async (session, text, clientUserMessageId) => {
|
|
901
1073
|
autopilot.manualSend(session.id);
|
|
902
|
-
|
|
1074
|
+
const owner = ownerRuntime(session);
|
|
1075
|
+
if (!owner)
|
|
1076
|
+
throw new Error('LLM_RUNTIME_UNAVAILABLE');
|
|
1077
|
+
return owner.startTurn(session, text, clientUserMessageId, new Date().toISOString());
|
|
903
1078
|
}
|
|
904
1079
|
: undefined,
|
|
905
|
-
ensureWriter: runtime
|
|
1080
|
+
ensureWriter: runtime || kimiRuntime
|
|
906
1081
|
? async (session) => {
|
|
907
1082
|
dismissPendingInteractions(session.id, new Date().toISOString());
|
|
908
|
-
|
|
1083
|
+
const owner = ownerRuntime(session);
|
|
1084
|
+
if (!owner)
|
|
1085
|
+
throw new Error('LLM_RUNTIME_UNAVAILABLE');
|
|
1086
|
+
return owner.ensureWriter(await recoverExecutionPolicy(session), new Date().toISOString());
|
|
909
1087
|
}
|
|
910
1088
|
: undefined,
|
|
911
|
-
releaseWriter: runtime
|
|
1089
|
+
releaseWriter: runtime || kimiRuntime
|
|
912
1090
|
? (id) => {
|
|
913
1091
|
dismissPendingInteractions(id, new Date().toISOString());
|
|
914
|
-
|
|
1092
|
+
const session = sessions.find(id);
|
|
1093
|
+
const owner = session ? ownerRuntime(session) : null;
|
|
1094
|
+
return owner ? owner.release(id) : Promise.resolve();
|
|
915
1095
|
}
|
|
916
1096
|
: undefined,
|
|
917
1097
|
onTurnStarted: (session) => {
|
|
@@ -930,24 +1110,41 @@ export async function composeRelayApp(options) {
|
|
|
930
1110
|
autopilotAudit: (id, limit) => journal.autopilotAuditTail(id, limit),
|
|
931
1111
|
refreshActivity: (id) => activity.refresh(id),
|
|
932
1112
|
models,
|
|
933
|
-
|
|
1113
|
+
sessionModels,
|
|
1114
|
+
readHistory: runtime || kimiRuntime
|
|
1115
|
+
? (session) => {
|
|
1116
|
+
const owner = ownerRuntime(session);
|
|
1117
|
+
if (!owner)
|
|
1118
|
+
return Promise.reject(new Error('LLM_RUNTIME_UNAVAILABLE'));
|
|
1119
|
+
return owner.readHistory(session);
|
|
1120
|
+
}
|
|
1121
|
+
: undefined,
|
|
934
1122
|
currentSequence: (sessionId) => journal.since(sessionId, 0).at(-1)?.sequence ?? 0,
|
|
935
1123
|
activityHistory: (sessionId, throughSequence) => journal.since(sessionId, 0).filter((event) => event.sequence <= throughSequence),
|
|
936
|
-
interruptTurn: runtime
|
|
937
|
-
? (session, turnId) =>
|
|
1124
|
+
interruptTurn: runtime || kimiRuntime
|
|
1125
|
+
? (session, turnId) => {
|
|
1126
|
+
const owner = ownerRuntime(session);
|
|
1127
|
+
return owner ? owner.interruptTurn(session, turnId) : Promise.resolve();
|
|
1128
|
+
}
|
|
938
1129
|
: undefined,
|
|
939
|
-
queueTurnInput: runtime
|
|
1130
|
+
queueTurnInput: runtime || kimiRuntime
|
|
940
1131
|
? (session, turnId, text, clientUserMessageId) => {
|
|
941
1132
|
autopilot.manualSend(session.id);
|
|
942
|
-
|
|
1133
|
+
const owner = ownerRuntime(session);
|
|
1134
|
+
if (!owner)
|
|
1135
|
+
return Promise.resolve();
|
|
1136
|
+
return owner.queueTurnInput(session, turnId, text, clientUserMessageId);
|
|
943
1137
|
}
|
|
944
1138
|
: undefined,
|
|
945
|
-
restore: runtime
|
|
1139
|
+
restore: runtime || kimiRuntime
|
|
946
1140
|
? async (session) => {
|
|
947
|
-
const
|
|
1141
|
+
const owner = ownerRuntime(session);
|
|
1142
|
+
if (!owner)
|
|
1143
|
+
throw new Error('LLM_RUNTIME_UNAVAILABLE');
|
|
1144
|
+
const restored = await owner.restoreWithOutcome(await recoverExecutionPolicy(session), new Date().toISOString());
|
|
948
1145
|
// An exit can be reported while resume is resolving. Do not let the
|
|
949
1146
|
// route persist a stale ready snapshot over that recovered exit.
|
|
950
|
-
if (!
|
|
1147
|
+
if (!owner.ownsWriter(session.id))
|
|
951
1148
|
return {
|
|
952
1149
|
...restored,
|
|
953
1150
|
session: RelaySession.rehydrate(restored.session).stop(new Date().toISOString())
|
|
@@ -956,15 +1153,26 @@ export async function composeRelayApp(options) {
|
|
|
956
1153
|
return restored;
|
|
957
1154
|
}
|
|
958
1155
|
: undefined,
|
|
959
|
-
ownsWriter:
|
|
960
|
-
|
|
1156
|
+
ownsWriter: (id) => {
|
|
1157
|
+
const session = sessions.find(id);
|
|
1158
|
+
const owner = session ? ownerRuntime(session) : null;
|
|
1159
|
+
return owner ? owner.ownsWriter(id) : false;
|
|
1160
|
+
},
|
|
1161
|
+
promoteRecent: runtime || kimiRuntime
|
|
961
1162
|
? (thread) => promoteRecentThread(thread, {
|
|
962
1163
|
createId: randomUUID,
|
|
963
1164
|
now: () => new Date().toISOString(),
|
|
964
1165
|
list: () => sessions.list(),
|
|
965
1166
|
save: saveSession,
|
|
966
|
-
read: (session) =>
|
|
967
|
-
|
|
1167
|
+
read: (session) => {
|
|
1168
|
+
const owner = ownerRuntime(session);
|
|
1169
|
+
if (!owner)
|
|
1170
|
+
throw new Error('LLM_RUNTIME_UNAVAILABLE');
|
|
1171
|
+
return owner.readHistory(session);
|
|
1172
|
+
},
|
|
1173
|
+
executionPolicy: (candidate) => candidate.provider === 'kimi'
|
|
1174
|
+
? Promise.resolve(undefined)
|
|
1175
|
+
: recentThreads.executionPolicy(candidate.id),
|
|
968
1176
|
})
|
|
969
1177
|
: undefined,
|
|
970
1178
|
release: (session) => {
|
|
@@ -977,17 +1185,28 @@ export async function composeRelayApp(options) {
|
|
|
977
1185
|
sessions.remove(id);
|
|
978
1186
|
},
|
|
979
1187
|
idempotency,
|
|
980
|
-
close: runtime
|
|
1188
|
+
close: runtime || kimiRuntime
|
|
981
1189
|
? (id) => {
|
|
982
1190
|
autopilot.cancel(id, 'sessionEnded');
|
|
983
1191
|
planMeasurementRefresh?.stop(id);
|
|
984
1192
|
dismissPendingInteractions(id, new Date().toISOString());
|
|
985
1193
|
activity.dispose(id);
|
|
986
|
-
|
|
1194
|
+
const session = sessions.find(id);
|
|
1195
|
+
const owner = session ? ownerRuntime(session) : null;
|
|
1196
|
+
return owner ? owner.release(id) : Promise.resolve();
|
|
987
1197
|
}
|
|
988
1198
|
: undefined,
|
|
989
|
-
replyInteraction: runtime
|
|
990
|
-
? (sessionId, requestId, value) =>
|
|
1199
|
+
replyInteraction: runtime || kimiRuntime
|
|
1200
|
+
? async (sessionId, requestId, value) => {
|
|
1201
|
+
const session = sessions.find(sessionId);
|
|
1202
|
+
const owner = session ? ownerRuntime(session) : null;
|
|
1203
|
+
if (await owner?.resolveServerRequest(sessionId, requestId, value))
|
|
1204
|
+
return 'accepted';
|
|
1205
|
+
// Codex reports false when its app-server has already cleared a
|
|
1206
|
+
// request. Kimi returns false for a failed provider delivery and
|
|
1207
|
+
// retains the pending request for retry.
|
|
1208
|
+
return session?.provider === 'kimi' ? 'unavailable' : 'cleared';
|
|
1209
|
+
}
|
|
991
1210
|
: undefined,
|
|
992
1211
|
interactionResolved: (sessionId, requestId, occurredAt, outcome) => {
|
|
993
1212
|
const remaining = interactions.list(sessionId);
|
|
@@ -1217,7 +1436,7 @@ export async function composeRelayApp(options) {
|
|
|
1217
1436
|
// not a human disable: a later fenced continuation can reacquire it safely.
|
|
1218
1437
|
if (session.desiredState === 'active') {
|
|
1219
1438
|
saveSession(RelaySession.rehydrate(session).stop(new Date().toISOString()).snapshot);
|
|
1220
|
-
await runtime?.release(session.id);
|
|
1439
|
+
await (session.provider === 'kimi' ? kimiRuntime : runtime)?.release(session.id);
|
|
1221
1440
|
}
|
|
1222
1441
|
await runtime?.watchPlanStatus(session);
|
|
1223
1442
|
autopilot.restore(session.id);
|
|
@@ -1230,7 +1449,10 @@ export async function composeRelayApp(options) {
|
|
|
1230
1449
|
app.addHook('onListen', async () => {
|
|
1231
1450
|
const profile = (await options.profiles.list()).find((item) => item.state === 'ok')?.name;
|
|
1232
1451
|
if (profile)
|
|
1233
|
-
await editorSkillCatalog.refresh(profile, root);
|
|
1452
|
+
await editorSkillCatalog.refresh('codex', profile, root);
|
|
1453
|
+
// Kimi discovery owns a provider process, so passive relay startup must not
|
|
1454
|
+
// refresh it. Explicit session creation and user-requested skill refreshes
|
|
1455
|
+
// enter KimiSkillCatalog through their normal operation paths instead.
|
|
1234
1456
|
await detachActiveSessions();
|
|
1235
1457
|
});
|
|
1236
1458
|
app.addHook('onClose', async () => {
|
|
@@ -1261,6 +1483,8 @@ export async function composeRelayApp(options) {
|
|
|
1261
1483
|
}
|
|
1262
1484
|
}
|
|
1263
1485
|
runtime?.stopAll();
|
|
1486
|
+
kimiRuntime?.stopAll();
|
|
1487
|
+
await kimiManager?.stopAll();
|
|
1264
1488
|
planStatusSource.closeAll();
|
|
1265
1489
|
database.close();
|
|
1266
1490
|
authorization?.close();
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
6
|
import { registerAutopilotToggle } from './toggle/endpoint.js';
|
|
7
|
-
export function registerAutopilotRoutes(app, coordinator, idempotency) {
|
|
8
|
-
registerAutopilotToggle(app, coordinator, idempotency);
|
|
7
|
+
export function registerAutopilotRoutes(app, coordinator, idempotency, providerFor) {
|
|
8
|
+
registerAutopilotToggle(app, coordinator, idempotency, providerFor);
|
|
9
9
|
}
|
|
@@ -4,13 +4,17 @@
|
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
|
-
export function registerAutopilotToggle(app, coordinator, idempotency) {
|
|
7
|
+
export function registerAutopilotToggle(app, coordinator, idempotency, providerFor) {
|
|
8
8
|
app.put('/api/sessions/:id/autopilot', async (request, reply) => {
|
|
9
9
|
const enabled = request.body?.enabled;
|
|
10
10
|
if (typeof enabled !== 'boolean')
|
|
11
11
|
return reply.code(400).send({ code: 'AUTOPILOT_ENABLED_REQUIRED' });
|
|
12
12
|
const key = request.headers['idempotency-key'];
|
|
13
13
|
const sessionId = request.params.id;
|
|
14
|
+
// Autopilot orchestration (executors, plan measurement, wait leases) is
|
|
15
|
+
// codex-only; kimi sessions must not toggle it at all, in either direction.
|
|
16
|
+
if (providerFor?.(sessionId) === 'kimi')
|
|
17
|
+
return reply.code(409).send({ code: 'AUTOPILOT_PROVIDER_UNSUPPORTED' });
|
|
14
18
|
const scope = `autopilot-toggle:${sessionId}`;
|
|
15
19
|
const fingerprint = createHash('sha256').update(String(enabled)).digest('hex');
|
|
16
20
|
if (typeof key === 'string' && key && idempotency) {
|
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
6
|
export async function getBootstrap(deps) {
|
|
7
|
-
const [workspaces, profiles,
|
|
7
|
+
const [workspaces, profiles, codexModels, kimiModels] = await Promise.all([
|
|
8
8
|
deps.workspaces.list(),
|
|
9
9
|
deps.profiles.list(),
|
|
10
|
-
deps.models?.list().catch(() => []) ?? [],
|
|
10
|
+
deps.models?.list('codex').catch(() => []) ?? [],
|
|
11
|
+
deps.models?.list('kimi').catch(() => []) ?? [],
|
|
11
12
|
]);
|
|
12
13
|
return {
|
|
13
14
|
workspaces,
|
|
14
15
|
profiles,
|
|
15
|
-
models,
|
|
16
|
+
models: { codex: codexModels, kimi: kimiModels },
|
|
16
17
|
sessions: deps.sessions.list(),
|
|
17
18
|
versions: deps.versions ?? [],
|
|
18
19
|
capabilities: {
|
|
@@ -20,6 +21,7 @@ export async function getBootstrap(deps) {
|
|
|
20
21
|
userInput: true,
|
|
21
22
|
git: true,
|
|
22
23
|
protocolCompatible: deps.protocolCompatible,
|
|
24
|
+
providers: deps.providers,
|
|
23
25
|
},
|
|
24
26
|
};
|
|
25
27
|
}
|
|
@@ -3,6 +3,46 @@
|
|
|
3
3
|
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
|
+
/** Expands a host-owned compact boundary signal from authoritative plan state. */
|
|
7
|
+
export function resolveOrgPlanCheckpointSignal(kind, plan, planIdentity) {
|
|
8
|
+
if (kind === 'terminalReviewAccepted')
|
|
9
|
+
return {
|
|
10
|
+
version: 1,
|
|
11
|
+
kind,
|
|
12
|
+
planIdentity,
|
|
13
|
+
verdict: 'ACCEPT',
|
|
14
|
+
};
|
|
15
|
+
const selected = plan.steps.find((step) => step.id === plan.currentStepId) ??
|
|
16
|
+
[...plan.steps].reverse().find((step) => step.state === 'DONE' || step.state === 'WIP');
|
|
17
|
+
if (!selected)
|
|
18
|
+
return null;
|
|
19
|
+
const l1Position = `L${plan.steps.indexOf(selected) + 1}`;
|
|
20
|
+
if (kind === 'l1Accepted')
|
|
21
|
+
return {
|
|
22
|
+
version: 1,
|
|
23
|
+
kind,
|
|
24
|
+
planIdentity,
|
|
25
|
+
l1Id: selected.id,
|
|
26
|
+
position: l1Position,
|
|
27
|
+
verdict: 'ACCEPT',
|
|
28
|
+
commit: { kind: 'notRequired' },
|
|
29
|
+
};
|
|
30
|
+
const child = [...selected.children].reverse().find((step) => step.state === 'DONE');
|
|
31
|
+
if (!child)
|
|
32
|
+
return null;
|
|
33
|
+
return {
|
|
34
|
+
version: 1,
|
|
35
|
+
kind,
|
|
36
|
+
planIdentity,
|
|
37
|
+
l1Id: selected.id,
|
|
38
|
+
l2Id: child.id,
|
|
39
|
+
position: `${l1Position}.${selected.children.indexOf(child) + 1}`,
|
|
40
|
+
status: 'DONE',
|
|
41
|
+
changes: 'Validated from the authoritative Org Plan boundary.',
|
|
42
|
+
files: 'Recorded by the supervised executor.',
|
|
43
|
+
tests: 'Focused verification recorded before the boundary.',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
6
46
|
export function validOrgPlanCheckpoint(input) {
|
|
7
47
|
const { checkpoint, plan, planIdentity, rootOwned } = input;
|
|
8
48
|
if (!rootOwned || checkpoint.planIdentity !== planIdentity)
|
|
@@ -27,7 +27,7 @@ export function registerGetSession(app, find, activity, autopilot, plan) {
|
|
|
27
27
|
pendingAttention: Boolean(session.pendingInteractions?.length),
|
|
28
28
|
observedAt: new Date().toISOString(),
|
|
29
29
|
}),
|
|
30
|
-
resumeCommand: session.threadId ? buildResumeCommand(session) : null,
|
|
30
|
+
resumeCommand: session.threadId && session.provider !== 'kimi' ? buildResumeCommand(session) : null,
|
|
31
31
|
});
|
|
32
32
|
});
|
|
33
33
|
}
|