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.
Files changed (51) hide show
  1. package/README.md +60 -3
  2. package/dist/client/assets/index-9XMv2BvD.js +38 -0
  3. package/dist/client/assets/{index-CPQBJDsg.css → index-Bxv5Z2w4.css} +1 -1
  4. package/dist/client/index.html +2 -2
  5. package/dist/server/client/features/chat/activity-summary.js +71 -0
  6. package/dist/server/client/features/chat/chat-projection.js +722 -0
  7. package/dist/server/client/features/chat/message-store.js +30 -0
  8. package/dist/server/server/app.js +1 -1
  9. package/dist/server/server/cli.js +8 -0
  10. package/dist/server/server/composition.contracts.js +193 -10
  11. package/dist/server/server/composition.js +283 -59
  12. package/dist/server/server/features/autopilot/register-routes.js +2 -2
  13. package/dist/server/server/features/autopilot/toggle/endpoint.js +5 -1
  14. package/dist/server/server/features/catalog/get-bootstrap/use-case.js +5 -3
  15. package/dist/server/server/features/org-plan-checkpoint/application/validate.js +40 -0
  16. package/dist/server/server/features/sessions/get-session/endpoint.js +1 -1
  17. package/dist/server/server/features/sessions/list-models/endpoint.js +16 -0
  18. package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +8 -3
  19. package/dist/server/server/features/sessions/list-sessions/endpoint.js +1 -1
  20. package/dist/server/server/features/sessions/model/relay-session.js +2 -0
  21. package/dist/server/server/features/sessions/promote-recent-thread/endpoint.js +6 -2
  22. package/dist/server/server/features/sessions/promote-recent-thread/use-case.js +7 -2
  23. package/dist/server/server/features/sessions/register-routes.js +8 -3
  24. package/dist/server/server/features/sessions/respond-interaction/endpoint.js +1 -1
  25. package/dist/server/server/features/sessions/select-model/endpoint.js +1 -1
  26. package/dist/server/server/features/sessions/start-session/request.js +1 -0
  27. package/dist/server/server/features/sessions/start-session/use-case.js +21 -7
  28. package/dist/server/server/features/skills/list-available/endpoint.js +4 -2
  29. package/dist/server/server/platform/catalog/provider-model-catalog.js +15 -0
  30. package/dist/server/server/platform/codex/server-request.js +14 -0
  31. package/dist/server/server/platform/codex/session-runtime.js +5 -1
  32. package/dist/server/server/platform/kimi/kimi-activity-facts.js +88 -0
  33. package/dist/server/server/platform/kimi/kimi-errors.js +30 -0
  34. package/dist/server/server/platform/kimi/kimi-model-catalog.js +63 -0
  35. package/dist/server/server/platform/kimi/kimi-normalizer.js +196 -0
  36. package/dist/server/server/platform/kimi/kimi-recent-session-lister.js +68 -0
  37. package/dist/server/server/platform/kimi/kimi-server-request.js +143 -0
  38. package/dist/server/server/platform/kimi/kimi-session-runtime.js +653 -0
  39. package/dist/server/server/platform/kimi/kimi-share-dir.js +65 -0
  40. package/dist/server/server/platform/kimi/kimi-skill-catalog.js +79 -0
  41. package/dist/server/server/platform/kimi/kimi-skills-materializer.js +41 -0
  42. package/dist/server/server/platform/kimi/kimi-web-client.js +63 -0
  43. package/dist/server/server/platform/kimi/kimi-web-server-manager.js +215 -0
  44. package/dist/server/server/platform/kimi/kimi-ws-client.js +154 -0
  45. package/dist/server/server/platform/persistence/migrate.js +2 -0
  46. package/dist/server/server/platform/persistence/sqlite-session-repository.js +3 -2
  47. package/dist/server/server/platform/skills/cached-skill-catalog.js +0 -0
  48. package/dist/server/shared/contracts/llm-provider.js +6 -0
  49. package/dist/server/shared/contracts/org-plan-attention.js +15 -1
  50. package/package.json +4 -3
  51. 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 || !runtime)
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 models = new CodexModelCatalog(root, options.launchAppServer ?? launchCodexAppServer);
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
- const skillCatalog = (profile) => new CodexSkillCatalog(profile, options.launchAppServer ?? launchCodexAppServer);
333
- const editorSkillCatalog = new CachedSkillCatalog((profile, workspace) => skillCatalog(profile).list(workspace));
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 = await skillCatalog(session.profile).list(session.workspacePath);
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
- const rawInteraction = toPendingInteraction(request);
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
- const interaction = {
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 started = await runtime.start(session, now);
896
- return started;
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
- return runtime.startTurn(session, text, clientUserMessageId, new Date().toISOString());
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
- return runtime.ensureWriter(await recoverExecutionPolicy(session), new Date().toISOString());
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
- return runtime.release(id);
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
- readHistory: runtime ? (session) => runtime.readHistory(session) : undefined,
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) => runtime.interruptTurn(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
- return runtime.queueTurnInput(session, turnId, text, clientUserMessageId);
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 restored = await runtime.restoreWithOutcome(await recoverExecutionPolicy(session), new Date().toISOString());
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 (!runtime.ownsWriter(session.id))
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: runtime ? (id) => runtime.ownsWriter(id) : undefined,
960
- promoteRecent: runtime
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) => runtime.readHistory(session),
967
- executionPolicy: (candidate) => recentThreads.executionPolicy(candidate.id),
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
- return runtime.release(id);
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) => runtime.resolveServerRequest(sessionId, requestId, value) ? 'accepted' : 'cleared'
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, models] = await Promise.all([
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
  }