gestalt-mobile 0.35.4 → 0.36.0
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 +191 -8
- package/dist/server/server/composition.js +266 -56
- package/dist/server/server/features/autopilot/application/policy.js +2 -2
- 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/plans/application/parse-supervised-plan.js +33 -8
- 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/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/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';
|
|
@@ -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;
|
|
@@ -668,28 +776,7 @@ export async function composeRelayApp(options) {
|
|
|
668
776
|
// is cancelled solely by the JSON-RPC write-settlement callback.
|
|
669
777
|
return true;
|
|
670
778
|
}
|
|
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;
|
|
779
|
+
return acceptPendingInteraction(sessionId, rawInteraction, session);
|
|
693
780
|
}, (sessionId) => {
|
|
694
781
|
activity.disconnected(sessionId, new Date().toISOString());
|
|
695
782
|
failPendingCheckpointHandoffs(sessionId, false);
|
|
@@ -697,6 +784,59 @@ export async function composeRelayApp(options) {
|
|
|
697
784
|
recoverExitedSession(sessionId);
|
|
698
785
|
}, resolveSkills, planStatusSource, acceptPlanUpdate, options.planMeasurementBaseUrl, 30_000, 64, root)
|
|
699
786
|
: null;
|
|
787
|
+
const handleKimiNotification = (sessionId, event) => {
|
|
788
|
+
const occurredAt = new Date().toISOString();
|
|
789
|
+
const session = sessions.find(sessionId);
|
|
790
|
+
const context = kimiRuntime?.eventContext(sessionId) ?? {
|
|
791
|
+
resolveTurnId: () => null,
|
|
792
|
+
isChildAgent: () => false,
|
|
793
|
+
};
|
|
794
|
+
for (const fact of decodeKimiActivityFacts(sessionId, occurredAt, event, context))
|
|
795
|
+
activity.observe(fact);
|
|
796
|
+
const normalized = normalizeKimiEvent(sessionId, 0, occurredAt, event, {
|
|
797
|
+
...context,
|
|
798
|
+
workspacePath: session?.workspacePath,
|
|
799
|
+
activeTurnId: session?.activeTurnId,
|
|
800
|
+
});
|
|
801
|
+
if (!normalized)
|
|
802
|
+
return;
|
|
803
|
+
let completedSession;
|
|
804
|
+
if (normalized.type === 'turnCompleted' || normalized.type === 'turnInterrupted') {
|
|
805
|
+
const turnId = normalized.payload.turn?.id;
|
|
806
|
+
if (session && turnId && session.activeTurnId === turnId) {
|
|
807
|
+
completedSession = RelaySession.rehydrate(session).completeTurn(turnId, occurredAt).snapshot;
|
|
808
|
+
sessions.save(completedSession);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
events.publish(journal.append(sessionId, normalized.type, normalized.payload, occurredAt));
|
|
812
|
+
if (completedSession)
|
|
813
|
+
events.publish(journal.append(sessionId, 'session.updated', completedSession, occurredAt));
|
|
814
|
+
};
|
|
815
|
+
const handleKimiServerRequest = (sessionId, request) => {
|
|
816
|
+
const rawInteraction = toPendingInteraction(request);
|
|
817
|
+
const session = withPendingInteractions(sessions.find(sessionId));
|
|
818
|
+
return Boolean(rawInteraction && session && acceptPendingInteraction(sessionId, rawInteraction, session));
|
|
819
|
+
};
|
|
820
|
+
kimiRuntime = kimiManager
|
|
821
|
+
? new KimiSessionRuntime({
|
|
822
|
+
servers: kimiManager,
|
|
823
|
+
skillsFor: async (session) => {
|
|
824
|
+
const config = await resolveSkills(session);
|
|
825
|
+
return config?.map((entry) => ({
|
|
826
|
+
name: basename(entry.path),
|
|
827
|
+
path: entry.path,
|
|
828
|
+
enabled: entry.enabled,
|
|
829
|
+
}));
|
|
830
|
+
},
|
|
831
|
+
onNotification: (sessionId, event) => handleKimiNotification(sessionId, event),
|
|
832
|
+
onServerRequest: (sessionId, request) => handleKimiServerRequest(sessionId, request),
|
|
833
|
+
onExit: (sessionId) => {
|
|
834
|
+
activity.disconnected(sessionId, new Date().toISOString());
|
|
835
|
+
dismissPendingInteractions(sessionId, new Date().toISOString(), 'failed');
|
|
836
|
+
recoverExitedKimiSession(sessionId);
|
|
837
|
+
},
|
|
838
|
+
})
|
|
839
|
+
: null;
|
|
700
840
|
runtime?.onServerResponseSettled((sessionId, requestId, outcome) => {
|
|
701
841
|
const interaction = interactions.find(sessionId, requestId);
|
|
702
842
|
if (interaction?.kind !== 'orgPlanCheckpoint')
|
|
@@ -726,13 +866,27 @@ export async function composeRelayApp(options) {
|
|
|
726
866
|
prior.state !== 'ready' &&
|
|
727
867
|
prior.state !== 'turnActive' &&
|
|
728
868
|
(session.state === 'ready' || session.state === 'turnActive');
|
|
729
|
-
if (runtime &&
|
|
869
|
+
if ((runtime || kimiRuntime) &&
|
|
730
870
|
session.threadId &&
|
|
731
871
|
(!prior || prior.threadId !== session.threadId || becameRuntimeReady))
|
|
732
872
|
void activity.refresh(session.id);
|
|
733
873
|
if (becameRuntimeReady)
|
|
734
874
|
autopilot.restore(session.id);
|
|
735
875
|
};
|
|
876
|
+
if (kimiRuntime) {
|
|
877
|
+
// A gestalt-owned kimi web process exited or its websocket dropped: stop
|
|
878
|
+
// the durable session like the codex supervisor does, without a resume
|
|
879
|
+
// retry (a later activation reattaches through the normal writer path).
|
|
880
|
+
recoverExitedKimiSession = (sessionId) => {
|
|
881
|
+
const session = sessions.find(sessionId);
|
|
882
|
+
if (session) {
|
|
883
|
+
autopilot.cancel(sessionId, 'sessionEnded');
|
|
884
|
+
saveSession(RelaySession.rehydrate(session).stop(new Date().toISOString()).snapshot);
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
/** Routes a session to the runtime owning its provider's llm service. */
|
|
889
|
+
const ownerRuntime = (session) => session.provider === 'kimi' ? kimiRuntime : runtime;
|
|
736
890
|
if (runtime) {
|
|
737
891
|
const capacityRecoveries = new Set();
|
|
738
892
|
scheduleAgentCapacityRecovery = (sessionId, acknowledge) => {
|
|
@@ -842,6 +996,7 @@ export async function composeRelayApp(options) {
|
|
|
842
996
|
protocolVersion: generatedProtocolVersion,
|
|
843
997
|
compatible: protocol.compatible,
|
|
844
998
|
},
|
|
999
|
+
providers: providerAvailability,
|
|
845
1000
|
};
|
|
846
1001
|
},
|
|
847
1002
|
},
|
|
@@ -859,7 +1014,7 @@ export async function composeRelayApp(options) {
|
|
|
859
1014
|
},
|
|
860
1015
|
logger: console,
|
|
861
1016
|
staticDir: options.staticDir,
|
|
862
|
-
recentThreads,
|
|
1017
|
+
recentThreads: recentThreadsForRelay,
|
|
863
1018
|
bootstrap: {
|
|
864
1019
|
workspaces,
|
|
865
1020
|
profiles: options.profiles,
|
|
@@ -875,6 +1030,7 @@ export async function composeRelayApp(options) {
|
|
|
875
1030
|
},
|
|
876
1031
|
versions: options.componentVersions,
|
|
877
1032
|
protocolCompatible: protocol.compatible,
|
|
1033
|
+
providers: providerAvailability,
|
|
878
1034
|
},
|
|
879
1035
|
sessionRoutes: {
|
|
880
1036
|
createId: randomUUID,
|
|
@@ -888,30 +1044,40 @@ export async function composeRelayApp(options) {
|
|
|
888
1044
|
skillProfiles,
|
|
889
1045
|
skillCatalog,
|
|
890
1046
|
defaultSkillProfile: options.explicitSkillProfile,
|
|
891
|
-
activate: runtime
|
|
1047
|
+
activate: runtime || kimiRuntime
|
|
892
1048
|
? async (session) => {
|
|
893
1049
|
const now = new Date().toISOString();
|
|
894
1050
|
dismissPendingInteractions(session.id, now);
|
|
895
|
-
const
|
|
896
|
-
|
|
1051
|
+
const owner = ownerRuntime(session);
|
|
1052
|
+
if (!owner)
|
|
1053
|
+
throw new Error('LLM_RUNTIME_UNAVAILABLE');
|
|
1054
|
+
return owner.start(session, now);
|
|
897
1055
|
}
|
|
898
1056
|
: undefined,
|
|
899
|
-
startTurn: runtime
|
|
1057
|
+
startTurn: runtime || kimiRuntime
|
|
900
1058
|
? async (session, text, clientUserMessageId) => {
|
|
901
1059
|
autopilot.manualSend(session.id);
|
|
902
|
-
|
|
1060
|
+
const owner = ownerRuntime(session);
|
|
1061
|
+
if (!owner)
|
|
1062
|
+
throw new Error('LLM_RUNTIME_UNAVAILABLE');
|
|
1063
|
+
return owner.startTurn(session, text, clientUserMessageId, new Date().toISOString());
|
|
903
1064
|
}
|
|
904
1065
|
: undefined,
|
|
905
|
-
ensureWriter: runtime
|
|
1066
|
+
ensureWriter: runtime || kimiRuntime
|
|
906
1067
|
? async (session) => {
|
|
907
1068
|
dismissPendingInteractions(session.id, new Date().toISOString());
|
|
908
|
-
|
|
1069
|
+
const owner = ownerRuntime(session);
|
|
1070
|
+
if (!owner)
|
|
1071
|
+
throw new Error('LLM_RUNTIME_UNAVAILABLE');
|
|
1072
|
+
return owner.ensureWriter(await recoverExecutionPolicy(session), new Date().toISOString());
|
|
909
1073
|
}
|
|
910
1074
|
: undefined,
|
|
911
|
-
releaseWriter: runtime
|
|
1075
|
+
releaseWriter: runtime || kimiRuntime
|
|
912
1076
|
? (id) => {
|
|
913
1077
|
dismissPendingInteractions(id, new Date().toISOString());
|
|
914
|
-
|
|
1078
|
+
const session = sessions.find(id);
|
|
1079
|
+
const owner = session ? ownerRuntime(session) : null;
|
|
1080
|
+
return owner ? owner.release(id) : Promise.resolve();
|
|
915
1081
|
}
|
|
916
1082
|
: undefined,
|
|
917
1083
|
onTurnStarted: (session) => {
|
|
@@ -930,24 +1096,41 @@ export async function composeRelayApp(options) {
|
|
|
930
1096
|
autopilotAudit: (id, limit) => journal.autopilotAuditTail(id, limit),
|
|
931
1097
|
refreshActivity: (id) => activity.refresh(id),
|
|
932
1098
|
models,
|
|
933
|
-
|
|
1099
|
+
sessionModels,
|
|
1100
|
+
readHistory: runtime || kimiRuntime
|
|
1101
|
+
? (session) => {
|
|
1102
|
+
const owner = ownerRuntime(session);
|
|
1103
|
+
if (!owner)
|
|
1104
|
+
return Promise.reject(new Error('LLM_RUNTIME_UNAVAILABLE'));
|
|
1105
|
+
return owner.readHistory(session);
|
|
1106
|
+
}
|
|
1107
|
+
: undefined,
|
|
934
1108
|
currentSequence: (sessionId) => journal.since(sessionId, 0).at(-1)?.sequence ?? 0,
|
|
935
1109
|
activityHistory: (sessionId, throughSequence) => journal.since(sessionId, 0).filter((event) => event.sequence <= throughSequence),
|
|
936
|
-
interruptTurn: runtime
|
|
937
|
-
? (session, turnId) =>
|
|
1110
|
+
interruptTurn: runtime || kimiRuntime
|
|
1111
|
+
? (session, turnId) => {
|
|
1112
|
+
const owner = ownerRuntime(session);
|
|
1113
|
+
return owner ? owner.interruptTurn(session, turnId) : Promise.resolve();
|
|
1114
|
+
}
|
|
938
1115
|
: undefined,
|
|
939
|
-
queueTurnInput: runtime
|
|
1116
|
+
queueTurnInput: runtime || kimiRuntime
|
|
940
1117
|
? (session, turnId, text, clientUserMessageId) => {
|
|
941
1118
|
autopilot.manualSend(session.id);
|
|
942
|
-
|
|
1119
|
+
const owner = ownerRuntime(session);
|
|
1120
|
+
if (!owner)
|
|
1121
|
+
return Promise.resolve();
|
|
1122
|
+
return owner.queueTurnInput(session, turnId, text, clientUserMessageId);
|
|
943
1123
|
}
|
|
944
1124
|
: undefined,
|
|
945
|
-
restore: runtime
|
|
1125
|
+
restore: runtime || kimiRuntime
|
|
946
1126
|
? async (session) => {
|
|
947
|
-
const
|
|
1127
|
+
const owner = ownerRuntime(session);
|
|
1128
|
+
if (!owner)
|
|
1129
|
+
throw new Error('LLM_RUNTIME_UNAVAILABLE');
|
|
1130
|
+
const restored = await owner.restoreWithOutcome(await recoverExecutionPolicy(session), new Date().toISOString());
|
|
948
1131
|
// An exit can be reported while resume is resolving. Do not let the
|
|
949
1132
|
// route persist a stale ready snapshot over that recovered exit.
|
|
950
|
-
if (!
|
|
1133
|
+
if (!owner.ownsWriter(session.id))
|
|
951
1134
|
return {
|
|
952
1135
|
...restored,
|
|
953
1136
|
session: RelaySession.rehydrate(restored.session).stop(new Date().toISOString())
|
|
@@ -956,15 +1139,26 @@ export async function composeRelayApp(options) {
|
|
|
956
1139
|
return restored;
|
|
957
1140
|
}
|
|
958
1141
|
: undefined,
|
|
959
|
-
ownsWriter:
|
|
960
|
-
|
|
1142
|
+
ownsWriter: (id) => {
|
|
1143
|
+
const session = sessions.find(id);
|
|
1144
|
+
const owner = session ? ownerRuntime(session) : null;
|
|
1145
|
+
return owner ? owner.ownsWriter(id) : false;
|
|
1146
|
+
},
|
|
1147
|
+
promoteRecent: runtime || kimiRuntime
|
|
961
1148
|
? (thread) => promoteRecentThread(thread, {
|
|
962
1149
|
createId: randomUUID,
|
|
963
1150
|
now: () => new Date().toISOString(),
|
|
964
1151
|
list: () => sessions.list(),
|
|
965
1152
|
save: saveSession,
|
|
966
|
-
read: (session) =>
|
|
967
|
-
|
|
1153
|
+
read: (session) => {
|
|
1154
|
+
const owner = ownerRuntime(session);
|
|
1155
|
+
if (!owner)
|
|
1156
|
+
throw new Error('LLM_RUNTIME_UNAVAILABLE');
|
|
1157
|
+
return owner.readHistory(session);
|
|
1158
|
+
},
|
|
1159
|
+
executionPolicy: (candidate) => candidate.provider === 'kimi'
|
|
1160
|
+
? Promise.resolve(undefined)
|
|
1161
|
+
: recentThreads.executionPolicy(candidate.id),
|
|
968
1162
|
})
|
|
969
1163
|
: undefined,
|
|
970
1164
|
release: (session) => {
|
|
@@ -977,17 +1171,28 @@ export async function composeRelayApp(options) {
|
|
|
977
1171
|
sessions.remove(id);
|
|
978
1172
|
},
|
|
979
1173
|
idempotency,
|
|
980
|
-
close: runtime
|
|
1174
|
+
close: runtime || kimiRuntime
|
|
981
1175
|
? (id) => {
|
|
982
1176
|
autopilot.cancel(id, 'sessionEnded');
|
|
983
1177
|
planMeasurementRefresh?.stop(id);
|
|
984
1178
|
dismissPendingInteractions(id, new Date().toISOString());
|
|
985
1179
|
activity.dispose(id);
|
|
986
|
-
|
|
1180
|
+
const session = sessions.find(id);
|
|
1181
|
+
const owner = session ? ownerRuntime(session) : null;
|
|
1182
|
+
return owner ? owner.release(id) : Promise.resolve();
|
|
987
1183
|
}
|
|
988
1184
|
: undefined,
|
|
989
|
-
replyInteraction: runtime
|
|
990
|
-
? (sessionId, requestId, value) =>
|
|
1185
|
+
replyInteraction: runtime || kimiRuntime
|
|
1186
|
+
? async (sessionId, requestId, value) => {
|
|
1187
|
+
const session = sessions.find(sessionId);
|
|
1188
|
+
const owner = session ? ownerRuntime(session) : null;
|
|
1189
|
+
if (await owner?.resolveServerRequest(sessionId, requestId, value))
|
|
1190
|
+
return 'accepted';
|
|
1191
|
+
// Codex reports false when its app-server has already cleared a
|
|
1192
|
+
// request. Kimi returns false for a failed provider delivery and
|
|
1193
|
+
// retains the pending request for retry.
|
|
1194
|
+
return session?.provider === 'kimi' ? 'unavailable' : 'cleared';
|
|
1195
|
+
}
|
|
991
1196
|
: undefined,
|
|
992
1197
|
interactionResolved: (sessionId, requestId, occurredAt, outcome) => {
|
|
993
1198
|
const remaining = interactions.list(sessionId);
|
|
@@ -1217,7 +1422,7 @@ export async function composeRelayApp(options) {
|
|
|
1217
1422
|
// not a human disable: a later fenced continuation can reacquire it safely.
|
|
1218
1423
|
if (session.desiredState === 'active') {
|
|
1219
1424
|
saveSession(RelaySession.rehydrate(session).stop(new Date().toISOString()).snapshot);
|
|
1220
|
-
await runtime?.release(session.id);
|
|
1425
|
+
await (session.provider === 'kimi' ? kimiRuntime : runtime)?.release(session.id);
|
|
1221
1426
|
}
|
|
1222
1427
|
await runtime?.watchPlanStatus(session);
|
|
1223
1428
|
autopilot.restore(session.id);
|
|
@@ -1230,7 +1435,10 @@ export async function composeRelayApp(options) {
|
|
|
1230
1435
|
app.addHook('onListen', async () => {
|
|
1231
1436
|
const profile = (await options.profiles.list()).find((item) => item.state === 'ok')?.name;
|
|
1232
1437
|
if (profile)
|
|
1233
|
-
await editorSkillCatalog.refresh(profile, root);
|
|
1438
|
+
await editorSkillCatalog.refresh('codex', profile, root);
|
|
1439
|
+
// Kimi discovery owns a provider process, so passive relay startup must not
|
|
1440
|
+
// refresh it. Explicit session creation and user-requested skill refreshes
|
|
1441
|
+
// enter KimiSkillCatalog through their normal operation paths instead.
|
|
1234
1442
|
await detachActiveSessions();
|
|
1235
1443
|
});
|
|
1236
1444
|
app.addHook('onClose', async () => {
|
|
@@ -1261,6 +1469,8 @@ export async function composeRelayApp(options) {
|
|
|
1261
1469
|
}
|
|
1262
1470
|
}
|
|
1263
1471
|
runtime?.stopAll();
|
|
1472
|
+
kimiRuntime?.stopAll();
|
|
1473
|
+
await kimiManager?.stopAll();
|
|
1264
1474
|
planStatusSource.closeAll();
|
|
1265
1475
|
database.close();
|
|
1266
1476
|
authorization?.close();
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
|
-
export const AUTOPILOT_PROMPT_VERSION = '
|
|
7
|
-
export const AUTOPILOT_CONTINUATION_PROMPT = 'Inspect the active supervised Org Plan. Refer to every L1 as L<a> and each nested L2 as L<a>.<b>, using one-based positions. Spawn exactly one executor per L1 and pass the exact literal task_name l<a>: l1 for L1, l2 for L2, and so on. Never create an L2-specific task name or append a title, role, nickname, plan name, or generated label. A validated DONE L2 and an accepted L1 are mandatory answer boundaries. The corresponding gestalt_org_plan_checkpoint call must be the last tool call of that root turn: immediately emit exactly one boundary final and end the turn, without followup_task, review, executor launch, or later milestone work. On this later Autopilot turn, resume the same executor after an L2 boundary or launch the next executor after an accepted-L1 boundary. Treat a status question as an interruption: answer briefly, then perform the applicable continuation in the same turn. Before sending any blocker response or yielding because progress cannot continue safely, call gestalt_org_plan_attention for the matching decision-table blocker; never rely on blocker prose as the signal. Do not call it for routine progress or a recoverable failure. Before yielding for known long work, call gestalt_autopilot_wait_lease version 2 with relevant observable wake conditions and a bounded maxWaitMs; it is one episode, so reassess before registering another in a later turn. For GitHub PR CI, start an owned gh pr checks --watch process and lease processExited plus processResultAvailable while it runs. When the Autopilot probe is active, register the compatible wait lease, declare genuine attention, or immediately do actionable work; never acknowledge waiting in prose. Do not send a status-only response.';
|
|
6
|
+
export const AUTOPILOT_PROMPT_VERSION = 'v10';
|
|
7
|
+
export const AUTOPILOT_CONTINUATION_PROMPT = 'Inspect the active supervised Org Plan. Refer to every L1 as L<a> and each nested L2 as L<a>.<b>, using one-based positions. Spawn exactly one executor per L1 and pass the exact literal task_name l<a>: l1 for L1, l2 for L2, and so on. Never create an L2-specific task name or append a title, role, nickname, plan name, or generated label. A validated DONE L2 and an accepted L1 are mandatory answer boundaries. The corresponding gestalt_org_plan_checkpoint call must be the last tool call of that root turn: immediately emit exactly one boundary final and end the turn, without followup_task, review, executor launch, or later milestone work. Roll up commentary, files, verification, and commands since the previous boundary into that one answer; all later milestone activity belongs to a new root turn. On this later Autopilot turn, resume the same executor after an L2 boundary or launch the next executor after an accepted-L1 boundary. Treat a status question as an interruption: answer briefly, then perform the applicable continuation in the same turn. Before sending any blocker response or yielding because progress cannot continue safely, call gestalt_org_plan_attention for the matching decision-table blocker; never rely on blocker prose as the signal. Do not call it for routine progress or a recoverable failure. Before yielding for known long work, call gestalt_autopilot_wait_lease version 2 with relevant observable wake conditions and a bounded maxWaitMs; it is one episode, so reassess before registering another in a later turn. Yield only when its response contains accepted:true; accepted:false means no continuation was registered and requires same-turn supervision. For GitHub PR CI, start an owned gh pr checks --watch process and lease processExited plus processResultAvailable while it runs. When the Autopilot probe is active, register the compatible wait lease, declare genuine attention, or immediately do actionable work; never acknowledge waiting in prose. Do not send a status-only response.';
|
|
8
8
|
export const AUTOPILOT_EXECUTOR_CONTINUATION_PROMPT = 'Continue the same assigned Org L1 from its durable state. A prior turn ending did not complete the objective. Consume any supplied process result and take the next legal L2 action. Whenever an L2 reaches DONE, return its concise structured evidence and end the executor turn so the root can publish that mandatory answer boundary. Also report at the L1 review boundary or through structured attention.';
|
|
9
9
|
/** Builds a physical launch instruction while keeping durable L1 identity canonical. */
|
|
10
10
|
export function autopilotExecutorLaunchPrompt(identity) {
|
|
@@ -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
|
}
|
|
@@ -201,9 +201,10 @@ function measurementFor(heading) {
|
|
|
201
201
|
if (value === undefined)
|
|
202
202
|
continue;
|
|
203
203
|
if (property.endsWith('_AT')) {
|
|
204
|
-
|
|
204
|
+
const instant = normalizeUtcIsoInstant(value);
|
|
205
|
+
if (!instant)
|
|
205
206
|
return null;
|
|
206
|
-
measurement[field] =
|
|
207
|
+
measurement[field] = instant;
|
|
207
208
|
continue;
|
|
208
209
|
}
|
|
209
210
|
if (!/^\d+$/.test(value))
|
|
@@ -215,14 +216,38 @@ function measurementFor(heading) {
|
|
|
215
216
|
}
|
|
216
217
|
return Object.keys(measurement).length === 0 ? undefined : measurement;
|
|
217
218
|
}
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
|
|
219
|
+
function normalizeUtcIsoInstant(value) {
|
|
220
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{3}))?(Z|[+-]\d{2}:\d{2})$/.exec(value);
|
|
221
|
+
if (!match)
|
|
222
|
+
return null;
|
|
223
|
+
const [, yearValue, monthValue, dayValue, hourValue, minuteValue, secondValue, , zone] = match;
|
|
224
|
+
const [year, month, day, hour, minute, second] = [
|
|
225
|
+
yearValue,
|
|
226
|
+
monthValue,
|
|
227
|
+
dayValue,
|
|
228
|
+
hourValue,
|
|
229
|
+
minuteValue,
|
|
230
|
+
secondValue,
|
|
231
|
+
].map(Number);
|
|
232
|
+
const offset = zone === 'Z' ? null : zone.slice(1).split(':').map(Number);
|
|
233
|
+
if (year < 1 ||
|
|
234
|
+
month < 1 ||
|
|
235
|
+
month > 12 ||
|
|
236
|
+
day < 1 ||
|
|
237
|
+
day > new Date(Date.UTC(year, month, 0)).getUTCDate() ||
|
|
238
|
+
hour > 23 ||
|
|
239
|
+
minute > 59 ||
|
|
240
|
+
second > 59 ||
|
|
241
|
+
(offset && (offset[0] > 23 || offset[1] > 59)))
|
|
242
|
+
return null;
|
|
221
243
|
const instant = new Date(value);
|
|
222
244
|
if (Number.isNaN(instant.getTime()))
|
|
223
|
-
return
|
|
224
|
-
|
|
225
|
-
|
|
245
|
+
return null;
|
|
246
|
+
if (zone === 'Z') {
|
|
247
|
+
const canonical = instant.toISOString();
|
|
248
|
+
return value === canonical || value === canonical.replace('.000Z', 'Z') ? value : null;
|
|
249
|
+
}
|
|
250
|
+
return instant.toISOString().replace('.000Z', 'Z');
|
|
226
251
|
}
|
|
227
252
|
function descriptionFor(heading) {
|
|
228
253
|
const fields = heading.level === 1 ? ['Effort', 'Goal', 'Notes'] : ['Why', 'Change', 'Tests', 'Done when'];
|
|
@@ -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
|
}
|