@hyperdreamer/pi-webui 1.11.0-beta.8 → 1.11.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 +1 -1
- package/dist/client/assets/{CodeViewer-DdYnGA-X.js → CodeViewer-Jy1WSFR_.js} +1 -1
- package/dist/client/assets/{UnifiedDiffViewer-BSMTtVUW.js → UnifiedDiffViewer-iic0AurS.js} +1 -1
- package/dist/client/assets/{index-B5Kn_roi.js → index-6NlIF1nJ.js} +593 -451
- package/dist/client/index.html +1 -1
- package/dist/config.js +70 -11
- package/dist/config.js.map +1 -1
- package/dist/server/app.js +6 -1
- package/dist/server/app.js.map +1 -1
- package/dist/server/sessiond/sessionProxyRoutes.js +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond.js +38 -5
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/modelPolicySettingsPersistence.js +82 -0
- package/dist/server/sessions/modelPolicySettingsPersistence.js.map +1 -0
- package/dist/server/sessions/piSessionManagerGateway.js +227 -9
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +678 -242
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/rememberCurrentModelPolicy.js +62 -0
- package/dist/server/sessions/rememberCurrentModelPolicy.js.map +1 -0
- package/dist/server/sessions/sessionArchiveStore.js +6 -0
- package/dist/server/sessions/sessionArchiveStore.js.map +1 -1
- package/dist/server/sessions/sessionCreationSource.js +101 -0
- package/dist/server/sessions/sessionCreationSource.js.map +1 -0
- package/dist/server/sessions/sessionDefaultsRoutes.js +7 -1
- package/dist/server/sessions/sessionDefaultsRoutes.js.map +1 -1
- package/dist/server/sessions/sessionDefaultsService.js +85 -3
- package/dist/server/sessions/sessionDefaultsService.js.map +1 -1
- package/dist/server/sessions/sessionMetadataStore.js +107 -13
- package/dist/server/sessions/sessionMetadataStore.js.map +1 -1
- package/dist/server/sessions/sessionModelPolicy.js +23 -0
- package/dist/server/sessions/sessionModelPolicy.js.map +1 -1
- package/dist/server/sessions/sessionNameGenerator.js +5 -4
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
- package/dist/server/sessions/sessionReorder.js +117 -0
- package/dist/server/sessions/sessionReorder.js.map +1 -0
- package/dist/server/sessions/sessionRouteFastifyOptions.js +8 -0
- package/dist/server/sessions/sessionRouteFastifyOptions.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +187 -4
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/starterModelPolicyPreferenceStore.js +142 -29
- package/dist/server/sessions/starterModelPolicyPreferenceStore.js.map +1 -1
- package/dist/server/sessions/utilityModelExtension.js +197 -0
- package/dist/server/sessions/utilityModelExtension.js.map +1 -0
- package/dist/server/sessions/utilityModelResolver.js +93 -0
- package/dist/server/sessions/utilityModelResolver.js.map +1 -0
- package/dist/server/sessions/utilityModelSettingsRoutes.js +62 -0
- package/dist/server/sessions/utilityModelSettingsRoutes.js.map +1 -0
- package/dist/server/sessions/utilityModelSettingsService.js +114 -0
- package/dist/server/sessions/utilityModelSettingsService.js.map +1 -0
- package/dist/shared/apiTypes.d.ts +105 -2
- package/dist/shared/apiTypes.js +8 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +9 -0
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +4 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/docs/assets/pi-webui-desktop.png +0 -0
- package/docs/config.md +37 -8
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
|
10
10
|
import { SessionCommandService, clientSessionFromRuntime, } from "./sessionCommandService.js";
|
|
11
11
|
import { projectSessionTree, } from "./sessionTreeProjection.js";
|
|
12
12
|
import { SessionArchiveStore, } from "./sessionArchiveStore.js";
|
|
13
|
-
import { SessionMetadataStore } from "./sessionMetadataStore.js";
|
|
13
|
+
import { SessionMetadataStore, SessionMetadataOrderConflictError } from "./sessionMetadataStore.js";
|
|
14
14
|
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree, } from "./sessionArchiveTree.js";
|
|
15
15
|
import { deterministicSessionName, fallbackSessionName, generateShortSessionName, } from "./sessionNameGenerator.js";
|
|
16
16
|
import { computeEditPreview } from "./editPreview.js";
|
|
@@ -18,6 +18,7 @@ import { attachmentsToInlineImages, saveAttachmentsToWorkspace, } from "./attach
|
|
|
18
18
|
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
19
19
|
import { isKnownThinkingLevel } from "../../shared/thinkingLevels.js";
|
|
20
20
|
import { SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_LIMIT, } from "../../shared/apiTypes.js";
|
|
21
|
+
import { assertSubmittedSessionsCurrent, SessionReorderDomainError, validateSessionReorder, } from "./sessionReorder.js";
|
|
21
22
|
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
|
22
23
|
import { createSpawnSessionToolDefinition, } from "./spawnSessionTool.js";
|
|
23
24
|
import { createSubsessionToolDefinitions, } from "./spawnSubsessionTool.js";
|
|
@@ -29,7 +30,13 @@ import { SessionNotificationStore, } from "./sessionNotificationStore.js";
|
|
|
29
30
|
import { plainTextTheme } from "./plainTextTheme.js";
|
|
30
31
|
import { SessionUnreadStore, } from "./sessionUnreadStore.js";
|
|
31
32
|
import { createModelTierRegistry, isModelTier, runtimeThinkingLevels, } from "./modelTierRegistry.js";
|
|
32
|
-
import {
|
|
33
|
+
import { createUtilityModelExtension, } from "./utilityModelExtension.js";
|
|
34
|
+
import { createUtilityModelResolver, runWithUtilityModelFallback, } from "./utilityModelResolver.js";
|
|
35
|
+
import { inspectSessionModelPolicy, planSessionModelPolicyInitialization, planSessionModelPolicyUpdate, serializeSessionModelPolicy, SESSION_MODEL_POLICY_CUSTOM_TYPE, } from "./sessionModelPolicy.js";
|
|
36
|
+
import { inspectSessionCreationRootEligibility, inspectSessionCreationSource, serializeSessionCreationSource, SESSION_CREATION_SOURCE_CUSTOM_TYPE, } from "./sessionCreationSource.js";
|
|
37
|
+
import { RememberCurrentModelPolicyCommand, } from "./rememberCurrentModelPolicy.js";
|
|
38
|
+
import { StarterModelPolicyPreferenceStore } from "./starterModelPolicyPreferenceStore.js";
|
|
39
|
+
import { captureModelPolicySettings, modelPolicySettingsPersistence, restoreModelPolicySettings, settleModelPolicySettings, } from "./modelPolicySettingsPersistence.js";
|
|
33
40
|
const noopLogger = {
|
|
34
41
|
info() {
|
|
35
42
|
/* no-op */
|
|
@@ -43,6 +50,33 @@ const MIN_GENERATION_RATE_ELAPSED_MS = 500;
|
|
|
43
50
|
function noop() {
|
|
44
51
|
// Intentionally empty default unsubscribe callback.
|
|
45
52
|
}
|
|
53
|
+
function normalizeError(error) {
|
|
54
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
55
|
+
}
|
|
56
|
+
class SessionInitializationCleanupAggregateError extends AggregateError {
|
|
57
|
+
constructor(initializationError, cleanupFailures, completeInitialization) {
|
|
58
|
+
super([initializationError, ...cleanupFailures], completeInitialization
|
|
59
|
+
? `Complete session initialization failed and rollback was incomplete: ${initializationError.message}`
|
|
60
|
+
: `Session initialization failed and cleanup was incomplete: ${initializationError.message}`);
|
|
61
|
+
this.initializationError = initializationError;
|
|
62
|
+
this.cleanupFailures = [...cleanupFailures];
|
|
63
|
+
this.completeInitialization = completeInitialization;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function sessionInitializationCleanupFailure(error, cleanupFailures, completeInitialization = false) {
|
|
67
|
+
if (error instanceof SessionInitializationCleanupAggregateError) {
|
|
68
|
+
if (cleanupFailures.length === 0)
|
|
69
|
+
return error;
|
|
70
|
+
return new SessionInitializationCleanupAggregateError(error.initializationError, [...error.cleanupFailures, ...cleanupFailures], error.completeInitialization || completeInitialization);
|
|
71
|
+
}
|
|
72
|
+
const initializationError = normalizeError(error);
|
|
73
|
+
if (cleanupFailures.length === 0)
|
|
74
|
+
return initializationError;
|
|
75
|
+
return new SessionInitializationCleanupAggregateError(initializationError, cleanupFailures, completeInitialization);
|
|
76
|
+
}
|
|
77
|
+
function completeInitializationFailure(error, rollbackFailures) {
|
|
78
|
+
return sessionInitializationCleanupFailure(error, rollbackFailures, true);
|
|
79
|
+
}
|
|
46
80
|
function spawnTargetError(decision) {
|
|
47
81
|
if (decision.reason === "not-registered")
|
|
48
82
|
return new Error("Spawning session is not in a registered project");
|
|
@@ -284,17 +318,34 @@ export function createPiWebUiCustomToolDefinitions(cwd, delegationEnabled, spawn
|
|
|
284
318
|
: []),
|
|
285
319
|
];
|
|
286
320
|
}
|
|
287
|
-
function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subsessions, modelPolicy
|
|
321
|
+
export function createDefaultRuntimeFactory(modelRuntime, sessionManagers, utilityModelResolver, logger, spawn, subsessions, modelPolicy, sdk = {
|
|
322
|
+
createServices: createAgentSessionServices,
|
|
323
|
+
createFromServices: createAgentSessionFromServices,
|
|
324
|
+
}) {
|
|
288
325
|
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled, }) => {
|
|
289
|
-
const
|
|
326
|
+
const runtimeRefs = {};
|
|
327
|
+
const services = await sdk.createServices({
|
|
290
328
|
cwd,
|
|
291
329
|
agentDir,
|
|
292
330
|
modelRuntime,
|
|
331
|
+
resourceLoaderOptions: {
|
|
332
|
+
extensionFactories: [
|
|
333
|
+
createUtilityModelExtension({
|
|
334
|
+
// Pi's ExtensionContext exposes Model<any>; production candidates still come from ModelRuntime's Model<Api> snapshot.
|
|
335
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
336
|
+
resolver: utilityModelResolver,
|
|
337
|
+
modelRuntime,
|
|
338
|
+
refs: runtimeRefs,
|
|
339
|
+
logger,
|
|
340
|
+
}),
|
|
341
|
+
],
|
|
342
|
+
},
|
|
293
343
|
});
|
|
344
|
+
runtimeRefs.settingsManager = services.settingsManager;
|
|
294
345
|
const resolvedDelegationToolsEnabled = delegationToolsEnabled ??
|
|
295
346
|
(await sessionAllowsDelegationTools(sessionManager, sessionManagers));
|
|
296
347
|
const customTools = createPiWebUiCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions, modelPolicy);
|
|
297
|
-
const result = await
|
|
348
|
+
const result = await sdk.createFromServices({
|
|
298
349
|
services,
|
|
299
350
|
sessionManager,
|
|
300
351
|
customTools,
|
|
@@ -304,6 +355,7 @@ function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subse
|
|
|
304
355
|
? {}
|
|
305
356
|
: { thinkingLevel: initialThinkingLevel }),
|
|
306
357
|
});
|
|
358
|
+
runtimeRefs.streamFunction = result.session.agent.streamFunction;
|
|
307
359
|
return { ...result, services, diagnostics: services.diagnostics };
|
|
308
360
|
};
|
|
309
361
|
}
|
|
@@ -415,13 +467,40 @@ export class PiSessionService {
|
|
|
415
467
|
this.pendingUnreadMutations = [];
|
|
416
468
|
this.unreadPublicationFlushRequested = false;
|
|
417
469
|
this.unreadPublicationStopped = false;
|
|
470
|
+
/**
|
|
471
|
+
* Serializes reorder against every other mutation that can change sibling
|
|
472
|
+
* membership, pin state, or current/archive membership (pin, unpin,
|
|
473
|
+
* detach-parent, archive, archiveMany, archiveTree, restore, and cleanup),
|
|
474
|
+
* so no such mutation can interleave with a reorder's before/after catalog
|
|
475
|
+
* reads and its atomic metadata write.
|
|
476
|
+
*/
|
|
477
|
+
this.sessionOrderMutationQueue = Promise.resolve();
|
|
418
478
|
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
|
419
479
|
this.metadataStore = deps.metadataStore ?? new SessionMetadataStore();
|
|
480
|
+
this.clearParentSessionFile = deps.clearParentSession ?? clearParentSession;
|
|
420
481
|
this.agentDir = deps.agentDir;
|
|
421
482
|
this.sessionManager = deps.sessionManager;
|
|
422
483
|
this.modelRuntime = deps.modelRuntime;
|
|
423
484
|
this.spawnTargets = deps.spawnTargets;
|
|
424
485
|
this.logger = deps.logger ?? noopLogger;
|
|
486
|
+
this.utilityModelResolver =
|
|
487
|
+
deps.utilityModelResolver ??
|
|
488
|
+
createUtilityModelResolver({
|
|
489
|
+
loadConfig: () => {
|
|
490
|
+
const loaded = loadPiWebUiConfig();
|
|
491
|
+
return {
|
|
492
|
+
...(loaded.config.utilityModels === undefined
|
|
493
|
+
? {}
|
|
494
|
+
: { utilityModels: loaded.config.utilityModels }),
|
|
495
|
+
...(loaded.utilityModelsError === undefined
|
|
496
|
+
? {}
|
|
497
|
+
: { utilityModelsError: loaded.utilityModelsError }),
|
|
498
|
+
};
|
|
499
|
+
},
|
|
500
|
+
modelRuntime: this.modelRuntime,
|
|
501
|
+
thinkingLevelsForModel: runtimeThinkingLevels,
|
|
502
|
+
logger: this.logger,
|
|
503
|
+
});
|
|
425
504
|
this.now = deps.now ?? (() => new Date());
|
|
426
505
|
this.notificationStore =
|
|
427
506
|
deps.notificationStore ?? new SessionNotificationStore();
|
|
@@ -433,7 +512,7 @@ export class PiSessionService {
|
|
|
433
512
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
434
513
|
this.createRuntime =
|
|
435
514
|
deps.createRuntime ??
|
|
436
|
-
createDefaultRuntimeFactory(this.modelRuntime, this.sessionManager, this.spawnTargets === undefined
|
|
515
|
+
createDefaultRuntimeFactory(this.modelRuntime, this.sessionManager, this.utilityModelResolver, this.logger, this.spawnTargets === undefined
|
|
437
516
|
? undefined
|
|
438
517
|
: (input) => this.spawnSession(input), !subsessionsActive
|
|
439
518
|
? undefined
|
|
@@ -467,6 +546,11 @@ export class PiSessionService {
|
|
|
467
546
|
supportedThinkingLevels: runtimeThinkingLevels,
|
|
468
547
|
});
|
|
469
548
|
this.workspaceActivity = deps.workspaceActivity;
|
|
549
|
+
this.rememberCurrentModelPolicyCommand = new RememberCurrentModelPolicyCommand({
|
|
550
|
+
loadSnapshot: (session) => this.confirmedPolicySnapshot(session),
|
|
551
|
+
preferenceStore: deps.starterModelPolicyPreferenceStore ??
|
|
552
|
+
new StarterModelPolicyPreferenceStore(),
|
|
553
|
+
});
|
|
470
554
|
this.heartbeat = setInterval(() => {
|
|
471
555
|
this.publishHeartbeats();
|
|
472
556
|
}, deps.heartbeatIntervalMs ?? 2000);
|
|
@@ -530,51 +614,53 @@ export class PiSessionService {
|
|
|
530
614
|
return previewResponseFromPlan(await this.cleanupPlan(request));
|
|
531
615
|
}
|
|
532
616
|
async cleanup(request) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
617
|
+
return await this.runSessionOrderMutation(async () => {
|
|
618
|
+
const plan = await this.cleanupPlan(request);
|
|
619
|
+
if (plan.deleteRecords.length > 0 &&
|
|
620
|
+
this.archiveStore.deleteArchived === undefined &&
|
|
621
|
+
this.archiveStore.deleteArchivedMany === undefined)
|
|
622
|
+
throw new Error("Archive store does not support deletion");
|
|
623
|
+
const archiveInputs = [];
|
|
624
|
+
const readyArchiveInputs = [];
|
|
625
|
+
const deleteRecords = [];
|
|
626
|
+
const readyDeleteRecords = [];
|
|
627
|
+
const skippedBusySessionIds = new Set(plan.skippedBusySessionIds);
|
|
628
|
+
for (const input of plan.archiveInputs) {
|
|
629
|
+
if (this.activeSessionHasWork(input.sessionId)) {
|
|
630
|
+
skippedBusySessionIds.add(input.sessionId);
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
await this.closeActive(input.sessionId, {
|
|
634
|
+
kind: "clear",
|
|
635
|
+
reason: "archive",
|
|
636
|
+
});
|
|
637
|
+
readyArchiveInputs.push(input);
|
|
547
638
|
}
|
|
548
|
-
await this.
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
639
|
+
await this.archiveStoreArchiveMany(readyArchiveInputs);
|
|
640
|
+
archiveInputs.push(...readyArchiveInputs);
|
|
641
|
+
await this.forgetUnreadSessions(readyArchiveInputs);
|
|
642
|
+
for (const record of plan.deleteRecords) {
|
|
643
|
+
if (this.activeSessionHasWork(record.sessionId)) {
|
|
644
|
+
skippedBusySessionIds.add(record.sessionId);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
await this.closeActive(record.sessionId, {
|
|
648
|
+
kind: "clear",
|
|
649
|
+
reason: "delete",
|
|
650
|
+
});
|
|
651
|
+
readyDeleteRecords.push(record);
|
|
561
652
|
}
|
|
562
|
-
await this.
|
|
563
|
-
|
|
564
|
-
|
|
653
|
+
await this.ensureArchivedRecordsMoved(readyDeleteRecords);
|
|
654
|
+
const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
|
|
655
|
+
deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
|
|
656
|
+
await this.forgetUnreadSessions(deleteRecords);
|
|
657
|
+
return summarizeSessionCleanupExecution({
|
|
658
|
+
archiveInputs,
|
|
659
|
+
deleteRecords,
|
|
660
|
+
thresholds: plan.thresholds,
|
|
661
|
+
generatedAt: plan.generatedAt,
|
|
662
|
+
skippedBusySessionIds: [...skippedBusySessionIds],
|
|
565
663
|
});
|
|
566
|
-
readyDeleteRecords.push(record);
|
|
567
|
-
}
|
|
568
|
-
await this.ensureArchivedRecordsMoved(readyDeleteRecords);
|
|
569
|
-
const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
|
|
570
|
-
deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
|
|
571
|
-
await this.forgetUnreadSessions(deleteRecords);
|
|
572
|
-
return summarizeSessionCleanupExecution({
|
|
573
|
-
archiveInputs,
|
|
574
|
-
deleteRecords,
|
|
575
|
-
thresholds: plan.thresholds,
|
|
576
|
-
generatedAt: plan.generatedAt,
|
|
577
|
-
skippedBusySessionIds: [...skippedBusySessionIds],
|
|
578
664
|
});
|
|
579
665
|
}
|
|
580
666
|
async forceCleanup() {
|
|
@@ -649,12 +735,68 @@ export class PiSessionService {
|
|
|
649
735
|
await this.publishUnreadMutations([]);
|
|
650
736
|
}
|
|
651
737
|
async list(cwd) {
|
|
652
|
-
const [sessions, archivedRecords,
|
|
738
|
+
const [sessions, archivedRecords, metadata] = await Promise.all([
|
|
653
739
|
this.sessionManager.list(cwd),
|
|
654
740
|
this.archiveStore.list(),
|
|
655
|
-
this.metadataStore.
|
|
741
|
+
this.metadataStore.snapshot(),
|
|
656
742
|
]);
|
|
657
|
-
|
|
743
|
+
return this.listFromSnapshots(cwd, sessions, archivedRecords, metadata);
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Run `operation` inside the exclusive ordering queue shared by every
|
|
747
|
+
* mutation that can change sibling membership, pin state, or
|
|
748
|
+
* current/archive membership, so reorder's before/after catalog reads
|
|
749
|
+
* never interleave with a concurrent pin, detach, archive, or restore.
|
|
750
|
+
*/
|
|
751
|
+
async runSessionOrderMutation(operation) {
|
|
752
|
+
const previous = this.sessionOrderMutationQueue;
|
|
753
|
+
let release = () => undefined;
|
|
754
|
+
this.sessionOrderMutationQueue = new Promise((resolve) => { release = resolve; });
|
|
755
|
+
await previous.catch(() => undefined);
|
|
756
|
+
try {
|
|
757
|
+
return await operation();
|
|
758
|
+
}
|
|
759
|
+
finally {
|
|
760
|
+
release();
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async reorder(ref, request) {
|
|
764
|
+
return await this.runSessionOrderMutation(async () => {
|
|
765
|
+
const target = { id: ref.id, cwd: canonicalizeStoredCwd(ref.cwd) };
|
|
766
|
+
const before = await this.listReorderCatalog(request.catalogCwds);
|
|
767
|
+
const validated = validateSessionReorder(target, request, uniqueSessionsByIdentity(before));
|
|
768
|
+
try {
|
|
769
|
+
await this.metadataStore.replaceOrder(validated.sessionPaths, request.scope, request.pinned);
|
|
770
|
+
}
|
|
771
|
+
catch (error) {
|
|
772
|
+
if (error instanceof SessionMetadataOrderConflictError) {
|
|
773
|
+
throw new SessionReorderDomainError("conflict", error.message);
|
|
774
|
+
}
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
777
|
+
const after = await this.listReorderCatalog(request.catalogCwds);
|
|
778
|
+
assertSubmittedSessionsCurrent(target, request, uniqueSessionsByIdentity(after));
|
|
779
|
+
return validated.response;
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Read one coherent metadata/archive view across every requested CWD.
|
|
784
|
+
* Gateway session-list reads run in parallel per CWD, but the metadata and
|
|
785
|
+
* archive snapshots are each taken exactly once for the whole call, so the
|
|
786
|
+
* result is one atomic before/after catalog rather than one per CWD.
|
|
787
|
+
*/
|
|
788
|
+
async listReorderCatalog(cwds) {
|
|
789
|
+
const metadataPromise = this.metadataStore.snapshot();
|
|
790
|
+
const archivePromise = this.archiveStore.list();
|
|
791
|
+
const sessionListsPromise = Promise.all(cwds.map((cwd) => this.sessionManager.list(cwd)));
|
|
792
|
+
const [metadata, archivedRecords, sessionLists] = await Promise.all([
|
|
793
|
+
metadataPromise,
|
|
794
|
+
archivePromise,
|
|
795
|
+
sessionListsPromise,
|
|
796
|
+
]);
|
|
797
|
+
return (await Promise.all(cwds.map((cwd, index) => this.listFromSnapshots(cwd, sessionLists[index] ?? [], archivedRecords, metadata)))).flat();
|
|
798
|
+
}
|
|
799
|
+
async listFromSnapshots(cwd, sessions, archivedRecords, metadata) {
|
|
658
800
|
const sessionsById = new Map(sessions.map((session) => [session.id, session]));
|
|
659
801
|
const archivedForCwd = await Promise.all(archivedRecords
|
|
660
802
|
.filter((record) => record.cwd === cwd)
|
|
@@ -665,7 +807,7 @@ export class PiSessionService {
|
|
|
665
807
|
}
|
|
666
808
|
const unarchivedSessions = sessions
|
|
667
809
|
.filter((session) => !archivedById.has(session.id))
|
|
668
|
-
.map((entry) => mergeSessionMetadata(clientSessionFromListEntry(entry),
|
|
810
|
+
.map((entry) => mergeSessionMetadata(clientSessionFromListEntry(entry), metadata));
|
|
669
811
|
const reconcilableSessionIds = this.reconcilableSessionIds(cwd, unarchivedSessions.map((session) => session.id), archivedById);
|
|
670
812
|
this.workspaceActivity?.reconcileSessionActivity(cwd, reconcilableSessionIds);
|
|
671
813
|
await this.publishUnreadMutations(this.unreadStore.reconcileCwd(canonicalizeStoredCwd(cwd), reconcilableSessionIds));
|
|
@@ -673,13 +815,30 @@ export class PiSessionService {
|
|
|
673
815
|
.sort(compareArchivedRecords)
|
|
674
816
|
.map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
|
|
675
817
|
.filter(isDefined)
|
|
676
|
-
.map((session) => mergeSessionMetadata(session,
|
|
818
|
+
.map((session) => mergeSessionMetadata(session, metadata));
|
|
677
819
|
return [...unarchivedSessions, ...archivedSessions];
|
|
678
820
|
}
|
|
679
821
|
async start(cwd, options) {
|
|
822
|
+
const creationSource = options?.creationSource;
|
|
823
|
+
const initialModelPolicy = options?.initialModelPolicy;
|
|
824
|
+
if (creationSource !== undefined || initialModelPolicy !== undefined) {
|
|
825
|
+
if (creationSource === undefined)
|
|
826
|
+
throw new Error("A complete initial model policy requires a session creation source");
|
|
827
|
+
if (initialModelPolicy === undefined)
|
|
828
|
+
throw new Error("A session creation source requires a complete initial model policy");
|
|
829
|
+
return this.startSession(cwd, {
|
|
830
|
+
creationSource,
|
|
831
|
+
initializeModelPolicy: {
|
|
832
|
+
kind: "complete-policy",
|
|
833
|
+
policy: initialModelPolicy,
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
}
|
|
680
837
|
const modelPolicy = options?.modelPolicy;
|
|
681
838
|
return this.startSession(cwd, {
|
|
682
|
-
initializeModelPolicy: modelPolicy
|
|
839
|
+
initializeModelPolicy: modelPolicy === undefined
|
|
840
|
+
? { kind: "runtime-default" }
|
|
841
|
+
: { kind: "legacy-update", update: modelPolicy },
|
|
683
842
|
});
|
|
684
843
|
}
|
|
685
844
|
async startSession(cwd, options) {
|
|
@@ -695,11 +854,15 @@ export class PiSessionService {
|
|
|
695
854
|
...(options.creationProvenance === undefined
|
|
696
855
|
? {}
|
|
697
856
|
: { creationProvenance: options.creationProvenance }),
|
|
857
|
+
...(options.creationSource === undefined
|
|
858
|
+
? {}
|
|
859
|
+
: { creationSource: options.creationSource }),
|
|
698
860
|
...(options.initializeModelPolicy === undefined
|
|
699
861
|
? {}
|
|
700
862
|
: { initializeModelPolicy: options.initializeModelPolicy }),
|
|
701
863
|
});
|
|
702
864
|
const { session } = active.runtime;
|
|
865
|
+
const creationSource = validSessionCreationSource(session.sessionManager);
|
|
703
866
|
const created = {
|
|
704
867
|
id: session.sessionId,
|
|
705
868
|
path: session.sessionFile ?? "",
|
|
@@ -714,6 +877,7 @@ export class PiSessionService {
|
|
|
714
877
|
...(options.parentSession === undefined
|
|
715
878
|
? {}
|
|
716
879
|
: { parentSessionPath: options.parentSession }),
|
|
880
|
+
...(creationSource === undefined ? {} : { creationSource }),
|
|
717
881
|
};
|
|
718
882
|
// Broadcast so other clients (and the spawning agent's UI) can add the new
|
|
719
883
|
// session to their list without a manual reload.
|
|
@@ -1199,6 +1363,9 @@ export class PiSessionService {
|
|
|
1199
1363
|
session: this.statusFromSession(session),
|
|
1200
1364
|
};
|
|
1201
1365
|
}
|
|
1366
|
+
async rememberCurrentModelPolicy(ref) {
|
|
1367
|
+
return this.rememberCurrentModelPolicyCommand.remember(ref);
|
|
1368
|
+
}
|
|
1202
1369
|
/**
|
|
1203
1370
|
* The only interface that changes a session's Exact/Tiered policy. Applies one
|
|
1204
1371
|
* complete transition atomically and publishes a single confirmed status; a
|
|
@@ -1688,173 +1855,181 @@ export class PiSessionService {
|
|
|
1688
1855
|
});
|
|
1689
1856
|
}
|
|
1690
1857
|
async archive(ref) {
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1858
|
+
await this.runSessionOrderMutation(async () => {
|
|
1859
|
+
const session = await this.getOrOpen(ref);
|
|
1860
|
+
if (this.hasActiveWork(session))
|
|
1861
|
+
throw new Error("Stop current session activity before archiving");
|
|
1862
|
+
await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session }], "Stop current session activity before archiving", async () => {
|
|
1863
|
+
const archiveInput = await this.archiveInputForSession(session);
|
|
1864
|
+
await this.closeActive(session.sessionId, {
|
|
1865
|
+
kind: "clear",
|
|
1866
|
+
reason: "archive",
|
|
1867
|
+
});
|
|
1868
|
+
await this.archiveStore.archive(archiveInput);
|
|
1869
|
+
await this.forgetUnreadSessions([archiveInput]);
|
|
1699
1870
|
});
|
|
1700
|
-
await this.archiveStore.archive(archiveInput);
|
|
1701
|
-
await this.forgetUnreadSessions([archiveInput]);
|
|
1702
1871
|
});
|
|
1703
1872
|
}
|
|
1704
1873
|
async archiveMany(refs) {
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
const
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
const active = this.activeForLookup(bulkRefToLookup(ref));
|
|
1723
|
-
const listed = findListedSessionForBulkRef(sessionContext, ref);
|
|
1724
|
-
const resolvedSessionId = active?.runtime.session.sessionId ?? listed?.id ?? ref.id;
|
|
1725
|
-
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1726
|
-
failures.push({
|
|
1727
|
-
sessionId: resolvedSessionId,
|
|
1728
|
-
error: "Stop current session activity before archiving",
|
|
1729
|
-
});
|
|
1730
|
-
continue;
|
|
1731
|
-
}
|
|
1732
|
-
try {
|
|
1733
|
-
if (listed !== undefined) {
|
|
1734
|
-
planItems.push({ input: archiveInputFromListEntry(listed) });
|
|
1874
|
+
return await this.runSessionOrderMutation(async () => {
|
|
1875
|
+
const uniqueRefs = uniqueBulkSessionRefs(refs);
|
|
1876
|
+
const [archivedRecords, sessionContext] = await Promise.all([
|
|
1877
|
+
this.archiveStore.list(),
|
|
1878
|
+
this.bulkSessionLookupContext(uniqueRefs),
|
|
1879
|
+
]);
|
|
1880
|
+
const failures = [];
|
|
1881
|
+
const alreadyArchivedSessionIds = [];
|
|
1882
|
+
const unreadArchivedIdentities = [];
|
|
1883
|
+
const planItems = [];
|
|
1884
|
+
for (const ref of uniqueRefs) {
|
|
1885
|
+
const archived = findArchivedRecordForBulkRef(archivedRecords, ref);
|
|
1886
|
+
if (archived !== undefined) {
|
|
1887
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(archived.sessionId, "archive"));
|
|
1888
|
+
alreadyArchivedSessionIds.push(archived.sessionId);
|
|
1889
|
+
unreadArchivedIdentities.push(archived);
|
|
1890
|
+
continue;
|
|
1735
1891
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1892
|
+
const active = this.activeForLookup(bulkRefToLookup(ref));
|
|
1893
|
+
const listed = findListedSessionForBulkRef(sessionContext, ref);
|
|
1894
|
+
const resolvedSessionId = active?.runtime.session.sessionId ?? listed?.id ?? ref.id;
|
|
1895
|
+
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1896
|
+
failures.push({
|
|
1897
|
+
sessionId: resolvedSessionId,
|
|
1898
|
+
error: "Stop current session activity before archiving",
|
|
1739
1899
|
});
|
|
1900
|
+
continue;
|
|
1740
1901
|
}
|
|
1741
|
-
else {
|
|
1742
|
-
failures.push({ sessionId: ref.id, error: "Session not found" });
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
catch (error) {
|
|
1746
|
-
failures.push({
|
|
1747
|
-
sessionId: resolvedSessionId,
|
|
1748
|
-
error: errorMessage(error),
|
|
1749
|
-
});
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
const readyPlanItems = [];
|
|
1753
|
-
for (const item of planItems) {
|
|
1754
|
-
const active = this.activeForLookup({
|
|
1755
|
-
id: item.input.sessionId,
|
|
1756
|
-
cwd: item.input.cwd,
|
|
1757
|
-
});
|
|
1758
|
-
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1759
|
-
failures.push({
|
|
1760
|
-
sessionId: item.input.sessionId,
|
|
1761
|
-
error: "Stop current session activity before archiving",
|
|
1762
|
-
});
|
|
1763
|
-
continue;
|
|
1764
|
-
}
|
|
1765
|
-
readyPlanItems.push(active === undefined ? item : { ...item, active });
|
|
1766
|
-
}
|
|
1767
|
-
const readyInputs = [];
|
|
1768
|
-
const archivedSessionIds = [...alreadyArchivedSessionIds];
|
|
1769
|
-
await this.runTreeExclusiveOperation(readyPlanItems.map(({ input, active }) => ({
|
|
1770
|
-
sessionId: input.sessionId,
|
|
1771
|
-
...(active === undefined
|
|
1772
|
-
? {}
|
|
1773
|
-
: { session: active.runtime.session, runtime: active.runtime }),
|
|
1774
|
-
})), "Stop current session activity before archiving", async () => {
|
|
1775
|
-
for (const item of readyPlanItems) {
|
|
1776
1902
|
try {
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1903
|
+
if (listed !== undefined) {
|
|
1904
|
+
planItems.push({ input: archiveInputFromListEntry(listed) });
|
|
1905
|
+
}
|
|
1906
|
+
else if (active !== undefined) {
|
|
1907
|
+
planItems.push({
|
|
1908
|
+
input: archiveInputFromActiveSession(active.runtime.session),
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
else {
|
|
1912
|
+
failures.push({ sessionId: ref.id, error: "Session not found" });
|
|
1913
|
+
}
|
|
1782
1914
|
}
|
|
1783
1915
|
catch (error) {
|
|
1784
1916
|
failures.push({
|
|
1785
|
-
sessionId:
|
|
1917
|
+
sessionId: resolvedSessionId,
|
|
1786
1918
|
error: errorMessage(error),
|
|
1787
1919
|
});
|
|
1788
1920
|
}
|
|
1789
1921
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1922
|
+
const readyPlanItems = [];
|
|
1923
|
+
for (const item of planItems) {
|
|
1924
|
+
const active = this.activeForLookup({
|
|
1925
|
+
id: item.input.sessionId,
|
|
1926
|
+
cwd: item.input.cwd,
|
|
1927
|
+
});
|
|
1928
|
+
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1797
1929
|
failures.push({
|
|
1798
|
-
sessionId: input.sessionId,
|
|
1799
|
-
error:
|
|
1930
|
+
sessionId: item.input.sessionId,
|
|
1931
|
+
error: "Stop current session activity before archiving",
|
|
1800
1932
|
});
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
readyPlanItems.push(active === undefined ? item : { ...item, active });
|
|
1801
1936
|
}
|
|
1937
|
+
const readyInputs = [];
|
|
1938
|
+
const archivedSessionIds = [...alreadyArchivedSessionIds];
|
|
1939
|
+
await this.runTreeExclusiveOperation(readyPlanItems.map(({ input, active }) => ({
|
|
1940
|
+
sessionId: input.sessionId,
|
|
1941
|
+
...(active === undefined
|
|
1942
|
+
? {}
|
|
1943
|
+
: { session: active.runtime.session, runtime: active.runtime }),
|
|
1944
|
+
})), "Stop current session activity before archiving", async () => {
|
|
1945
|
+
for (const item of readyPlanItems) {
|
|
1946
|
+
try {
|
|
1947
|
+
await this.closeActive(item.input.sessionId, {
|
|
1948
|
+
kind: "clear",
|
|
1949
|
+
reason: "archive",
|
|
1950
|
+
});
|
|
1951
|
+
readyInputs.push(item.input);
|
|
1952
|
+
}
|
|
1953
|
+
catch (error) {
|
|
1954
|
+
failures.push({
|
|
1955
|
+
sessionId: item.input.sessionId,
|
|
1956
|
+
error: errorMessage(error),
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
try {
|
|
1961
|
+
const archived = await this.archiveStoreArchiveMany(readyInputs);
|
|
1962
|
+
archivedSessionIds.push(...archived.map((record) => record.sessionId));
|
|
1963
|
+
unreadArchivedIdentities.push(...archived);
|
|
1964
|
+
}
|
|
1965
|
+
catch (error) {
|
|
1966
|
+
for (const input of readyInputs)
|
|
1967
|
+
failures.push({
|
|
1968
|
+
sessionId: input.sessionId,
|
|
1969
|
+
error: errorMessage(error),
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
await this.forgetUnreadSessions(unreadArchivedIdentities);
|
|
1974
|
+
return {
|
|
1975
|
+
archived: true,
|
|
1976
|
+
archivedSessionIds: uniqueStrings(archivedSessionIds),
|
|
1977
|
+
failures,
|
|
1978
|
+
generatedAt: new Date().toISOString(),
|
|
1979
|
+
};
|
|
1802
1980
|
});
|
|
1803
|
-
await this.forgetUnreadSessions(unreadArchivedIdentities);
|
|
1804
|
-
return {
|
|
1805
|
-
archived: true,
|
|
1806
|
-
archivedSessionIds: uniqueStrings(archivedSessionIds),
|
|
1807
|
-
failures,
|
|
1808
|
-
generatedAt: new Date().toISOString(),
|
|
1809
|
-
};
|
|
1810
1981
|
}
|
|
1811
1982
|
async archiveTree(ref) {
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1983
|
+
return await this.runSessionOrderMutation(async () => {
|
|
1984
|
+
const session = await this.getOrOpen(ref);
|
|
1985
|
+
const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
|
|
1986
|
+
const root = findArchiveCandidateByIdOrPrefix(catalog, session.sessionId) ??
|
|
1987
|
+
archiveCandidateFromActiveSession(session, false);
|
|
1988
|
+
const plan = planSessionArchiveTree(root, catalog);
|
|
1989
|
+
const busy = plan.targets
|
|
1990
|
+
.map((target) => target.activeSession)
|
|
1991
|
+
.find((target) => target !== undefined && this.hasActiveWork(target));
|
|
1992
|
+
if (busy !== undefined)
|
|
1993
|
+
throw new Error(`Stop current session activity before archiving ${sessionDisplayName(busy)}`);
|
|
1994
|
+
const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target));
|
|
1995
|
+
await this.runTreeExclusiveOperation(plan.unarchivedTargets.map((target) => ({
|
|
1996
|
+
sessionId: target.id,
|
|
1997
|
+
...(target.activeSession === undefined
|
|
1998
|
+
? {}
|
|
1999
|
+
: { session: target.activeSession }),
|
|
2000
|
+
})), `Stop current session activity before archiving ${sessionDisplayName(session)}`, async () => {
|
|
2001
|
+
for (const target of plan.targets) {
|
|
2002
|
+
if (target.archived)
|
|
2003
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(target.id, "archive"));
|
|
2004
|
+
}
|
|
2005
|
+
for (const input of archiveInputs)
|
|
2006
|
+
await this.closeActive(input.sessionId, {
|
|
2007
|
+
kind: "clear",
|
|
2008
|
+
reason: "archive",
|
|
2009
|
+
});
|
|
2010
|
+
await this.archiveStoreArchiveMany(archiveInputs);
|
|
2011
|
+
});
|
|
2012
|
+
await this.forgetUnreadSessions(plan.targets.map((target) => ({ sessionId: target.id, cwd: target.cwd })));
|
|
2013
|
+
return {
|
|
2014
|
+
archived: true,
|
|
2015
|
+
sessionIds: archiveInputs.map((input) => input.sessionId),
|
|
2016
|
+
archivedCount: archiveInputs.length,
|
|
2017
|
+
skippedAlreadyArchivedCount: plan.skippedAlreadyArchivedCount,
|
|
2018
|
+
};
|
|
1839
2019
|
});
|
|
1840
|
-
await this.forgetUnreadSessions(plan.targets.map((target) => ({ sessionId: target.id, cwd: target.cwd })));
|
|
1841
|
-
return {
|
|
1842
|
-
archived: true,
|
|
1843
|
-
sessionIds: archiveInputs.map((input) => input.sessionId),
|
|
1844
|
-
archivedCount: archiveInputs.length,
|
|
1845
|
-
skippedAlreadyArchivedCount: plan.skippedAlreadyArchivedCount,
|
|
1846
|
-
};
|
|
1847
2020
|
}
|
|
1848
2021
|
async restore(ref) {
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
2022
|
+
await this.runSessionOrderMutation(async () => {
|
|
2023
|
+
const archived = await this.getArchived(ref);
|
|
2024
|
+
if (archived === undefined)
|
|
2025
|
+
throw new Error("Session not found");
|
|
2026
|
+
await this.closeActive(archived.sessionId, {
|
|
2027
|
+
kind: "clear",
|
|
2028
|
+
reason: "restore",
|
|
2029
|
+
});
|
|
2030
|
+
await this.archiveStore.restore(archived.sessionId);
|
|
2031
|
+
await this.forgetUnreadSessions([archived]);
|
|
1855
2032
|
});
|
|
1856
|
-
await this.archiveStore.restore(archived.sessionId);
|
|
1857
|
-
await this.forgetUnreadSessions([archived]);
|
|
1858
2033
|
}
|
|
1859
2034
|
async deleteArchived(ref) {
|
|
1860
2035
|
const record = await this.getArchived(ref);
|
|
@@ -1978,35 +2153,43 @@ export class PiSessionService {
|
|
|
1978
2153
|
this.publishStatus(reopenedSession);
|
|
1979
2154
|
}
|
|
1980
2155
|
async detachParent(ref) {
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
2156
|
+
await this.runSessionOrderMutation(async () => {
|
|
2157
|
+
const session = await this.getOrOpen(ref);
|
|
2158
|
+
const sessionFile = session.sessionFile;
|
|
2159
|
+
if (sessionFile === undefined || sessionFile === "")
|
|
2160
|
+
throw new Error("Session is not persisted");
|
|
2161
|
+
await this.clearParentSessionFile(sessionFile);
|
|
2162
|
+
clearParentSessionHeader(session.sessionManager);
|
|
2163
|
+
await this.metadataStore.clearOrder(sessionFile);
|
|
2164
|
+
this.unregisterSubsession(session.sessionId);
|
|
2165
|
+
await this.forgetUnreadSessions([
|
|
2166
|
+
{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() },
|
|
2167
|
+
]);
|
|
2168
|
+
});
|
|
1991
2169
|
}
|
|
1992
2170
|
async pin(ref) {
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2171
|
+
return await this.runSessionOrderMutation(async () => {
|
|
2172
|
+
const session = await this.getOrOpen(ref);
|
|
2173
|
+
const sessionFile = session.sessionFile;
|
|
2174
|
+
if (sessionFile === undefined || sessionFile === "")
|
|
2175
|
+
throw new Error("Session is not persisted");
|
|
2176
|
+
await this.metadataStore.pin(sessionFile);
|
|
2177
|
+
return this.sessionInfoFromActive(session, { pinned: true });
|
|
2178
|
+
});
|
|
1999
2179
|
}
|
|
2000
2180
|
async unpin(ref) {
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2181
|
+
return await this.runSessionOrderMutation(async () => {
|
|
2182
|
+
const session = await this.getOrOpen(ref);
|
|
2183
|
+
const sessionFile = session.sessionFile;
|
|
2184
|
+
if (sessionFile === undefined || sessionFile === "")
|
|
2185
|
+
throw new Error("Session is not persisted");
|
|
2186
|
+
await this.metadataStore.unpin(sessionFile);
|
|
2187
|
+
return this.sessionInfoFromActive(session, { pinned: false });
|
|
2188
|
+
});
|
|
2007
2189
|
}
|
|
2008
2190
|
sessionInfoFromActive(session, overrides) {
|
|
2009
2191
|
const parentSessionPath = session.sessionManager.getHeader?.()?.parentSession;
|
|
2192
|
+
const creationSource = validSessionCreationSource(session.sessionManager);
|
|
2010
2193
|
return {
|
|
2011
2194
|
id: session.sessionId,
|
|
2012
2195
|
path: session.sessionFile ?? "",
|
|
@@ -2020,6 +2203,7 @@ export class PiSessionService {
|
|
|
2020
2203
|
? {}
|
|
2021
2204
|
: { name: session.sessionName }),
|
|
2022
2205
|
...(parentSessionPath === undefined ? {} : { parentSessionPath }),
|
|
2206
|
+
...(creationSource === undefined ? {} : { creationSource }),
|
|
2023
2207
|
...overrides,
|
|
2024
2208
|
};
|
|
2025
2209
|
}
|
|
@@ -2459,6 +2643,7 @@ export class PiSessionService {
|
|
|
2459
2643
|
: notificationGeneration === undefined
|
|
2460
2644
|
? "registered"
|
|
2461
2645
|
: "external";
|
|
2646
|
+
let completeInitialization;
|
|
2462
2647
|
if (notificationOwnership === "registered") {
|
|
2463
2648
|
const notificationIdentity = notificationIdentityForSession(runtime.session);
|
|
2464
2649
|
const existingCandidate = this.notificationStore.beginReplacementForSession(notificationIdentity.sessionId, notificationIdentity.cwd);
|
|
@@ -2483,7 +2668,6 @@ export class PiSessionService {
|
|
|
2483
2668
|
await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
|
|
2484
2669
|
}
|
|
2485
2670
|
await this.bindSessionExtensions(runtime.session, notificationGeneration);
|
|
2486
|
-
this.bindRuntime(active);
|
|
2487
2671
|
runtime.setRebindSession(async (session) => {
|
|
2488
2672
|
const priorGeneration = notificationGeneration;
|
|
2489
2673
|
let candidateGeneration;
|
|
@@ -2512,10 +2696,20 @@ export class PiSessionService {
|
|
|
2512
2696
|
throw error;
|
|
2513
2697
|
}
|
|
2514
2698
|
});
|
|
2515
|
-
|
|
2516
|
-
|
|
2699
|
+
if (options.initializeModelPolicy?.kind === "complete-policy") {
|
|
2700
|
+
if (options.creationSource === undefined) {
|
|
2701
|
+
throw new Error("A complete initial model policy requires a session creation source");
|
|
2702
|
+
}
|
|
2703
|
+
completeInitialization =
|
|
2704
|
+
await this.initializeCompleteSessionModelPolicy(runtime.session, options.initializeModelPolicy.policy, options.creationSource);
|
|
2705
|
+
}
|
|
2706
|
+
else if (options.initializeModelPolicy !== undefined) {
|
|
2517
2707
|
await this.initializeSessionModelPolicy(runtime.session, options.initializeModelPolicy);
|
|
2518
2708
|
}
|
|
2709
|
+
if (options.creationSource !== undefined &&
|
|
2710
|
+
options.initializeModelPolicy?.kind !== "complete-policy")
|
|
2711
|
+
throw new Error("A session creation source requires a complete initial model policy");
|
|
2712
|
+
this.bindRuntime(active);
|
|
2519
2713
|
if (notificationOwnership === "replacement" &&
|
|
2520
2714
|
notificationGeneration !== undefined) {
|
|
2521
2715
|
this.publishNotificationMutations(this.notificationStore.commitReplacement(notificationGeneration));
|
|
@@ -2548,13 +2742,28 @@ export class PiSessionService {
|
|
|
2548
2742
|
if (removedActive) {
|
|
2549
2743
|
this.workspaceActivity?.removeSession(runtime.session.sessionId, runtime.session.sessionManager.getCwd());
|
|
2550
2744
|
}
|
|
2745
|
+
let failure = error;
|
|
2746
|
+
if (completeInitialization !== undefined) {
|
|
2747
|
+
const rollbackFailures = await this.rollbackCompleteSessionInitialization(runtime.session, completeInitialization);
|
|
2748
|
+
failure = completeInitializationFailure(error, rollbackFailures);
|
|
2749
|
+
}
|
|
2750
|
+
const teardownFailures = [];
|
|
2551
2751
|
try {
|
|
2552
2752
|
await runtime.session.abort();
|
|
2553
2753
|
}
|
|
2554
|
-
|
|
2754
|
+
catch (abortError) {
|
|
2755
|
+
teardownFailures.push(normalizeError(abortError));
|
|
2756
|
+
}
|
|
2757
|
+
try {
|
|
2555
2758
|
await runtime.dispose();
|
|
2556
2759
|
}
|
|
2557
|
-
|
|
2760
|
+
catch (disposeError) {
|
|
2761
|
+
teardownFailures.push(normalizeError(disposeError));
|
|
2762
|
+
}
|
|
2763
|
+
if (teardownFailures.length > 0) {
|
|
2764
|
+
failure = sessionInitializationCleanupFailure(failure, teardownFailures, completeInitialization !== undefined);
|
|
2765
|
+
}
|
|
2766
|
+
throw failure;
|
|
2558
2767
|
}
|
|
2559
2768
|
}
|
|
2560
2769
|
async bindSessionExtensions(session, generation) {
|
|
@@ -2885,12 +3094,22 @@ export class PiSessionService {
|
|
|
2885
3094
|
const model = session.model;
|
|
2886
3095
|
if (model === undefined)
|
|
2887
3096
|
return;
|
|
2888
|
-
void generateShortSessionName(session.agent.streamFunction, model, firstMessage)
|
|
3097
|
+
void runWithUtilityModelFallback(this.utilityModelResolver, "lightweight", { model, thinkingLevel: "minimal" }, (attempt) => generateShortSessionName(session.agent.streamFunction, attempt.model, firstMessage, attempt.thinkingLevel), (attempt, error) => {
|
|
3098
|
+
this.logger.info({
|
|
3099
|
+
provider: attempt.model.provider,
|
|
3100
|
+
modelId: attempt.model.id,
|
|
3101
|
+
thinkingLevel: attempt.thinkingLevel,
|
|
3102
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3103
|
+
}, "utility title model failed");
|
|
3104
|
+
})
|
|
2889
3105
|
.then((name) => {
|
|
2890
3106
|
this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage));
|
|
2891
3107
|
})
|
|
2892
|
-
.catch(() => {
|
|
2893
|
-
this.
|
|
3108
|
+
.catch((error) => {
|
|
3109
|
+
this.logger.info({
|
|
3110
|
+
sessionId: session.sessionId,
|
|
3111
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3112
|
+
}, "failed to apply generated session name");
|
|
2894
3113
|
});
|
|
2895
3114
|
}
|
|
2896
3115
|
applyGeneratedSessionName(session, name) {
|
|
@@ -3241,8 +3460,57 @@ export class PiSessionService {
|
|
|
3241
3460
|
};
|
|
3242
3461
|
}
|
|
3243
3462
|
policyEntries(session) {
|
|
3244
|
-
return
|
|
3245
|
-
|
|
3463
|
+
return session.sessionManager.getBranch();
|
|
3464
|
+
}
|
|
3465
|
+
async confirmedPolicySnapshot(ref) {
|
|
3466
|
+
const session = await this.getOrOpen(ref);
|
|
3467
|
+
const sessionFile = session.sessionManager.getSessionFile();
|
|
3468
|
+
const persisted = sessionFileExists(sessionFile);
|
|
3469
|
+
const manager = persisted
|
|
3470
|
+
? this.sessionManager.open(sessionFile)
|
|
3471
|
+
: session.sessionManager;
|
|
3472
|
+
const activeEntries = manager.getBranch();
|
|
3473
|
+
// Creation provenance belongs to the durable root file, even when its
|
|
3474
|
+
// original entry is not on the currently selected conversation branch.
|
|
3475
|
+
const creationSourceEntries = manager.getEntries?.() ?? activeEntries;
|
|
3476
|
+
const creationSource = inspectSessionCreationSource(creationSourceEntries);
|
|
3477
|
+
return {
|
|
3478
|
+
cwd: canonicalizeStoredCwd(manager.getCwd()),
|
|
3479
|
+
creationSource,
|
|
3480
|
+
persisted,
|
|
3481
|
+
rootEligibility: this.sessionCreationRootEligibility(manager, creationSource),
|
|
3482
|
+
modelPolicy: inspectSessionModelPolicy(activeEntries, this.exactSelectionFromSession(session)),
|
|
3483
|
+
transitionInFlight: this.isModelPolicyMutationActive(session),
|
|
3484
|
+
};
|
|
3485
|
+
}
|
|
3486
|
+
sessionCreationRootEligibility(manager, source) {
|
|
3487
|
+
const sessionFile = manager.getSessionFile();
|
|
3488
|
+
const header = manager.getHeader?.();
|
|
3489
|
+
if (sessionFile === undefined || sessionFile === "") {
|
|
3490
|
+
return {
|
|
3491
|
+
kind: "ineligible",
|
|
3492
|
+
reason: "the session has no durable transcript path",
|
|
3493
|
+
};
|
|
3494
|
+
}
|
|
3495
|
+
if (header === undefined || header === null || typeof header.id !== "string") {
|
|
3496
|
+
return {
|
|
3497
|
+
kind: "ineligible",
|
|
3498
|
+
reason: "the session header does not identify its root",
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3501
|
+
if (header.id !== manager.getSessionId()) {
|
|
3502
|
+
return {
|
|
3503
|
+
kind: "ineligible",
|
|
3504
|
+
reason: "the session header id does not match the opened session",
|
|
3505
|
+
};
|
|
3506
|
+
}
|
|
3507
|
+
return inspectSessionCreationRootEligibility(source, {
|
|
3508
|
+
sessionId: header.id,
|
|
3509
|
+
sessionFile,
|
|
3510
|
+
...(header.parentSession === undefined
|
|
3511
|
+
? {}
|
|
3512
|
+
: { parentSession: header.parentSession }),
|
|
3513
|
+
});
|
|
3246
3514
|
}
|
|
3247
3515
|
inspectAndCacheSessionModelPolicy(session) {
|
|
3248
3516
|
const inspection = inspectSessionModelPolicy(this.policyEntries(session), this.exactSelectionFromSession(session));
|
|
@@ -3260,26 +3528,113 @@ export class PiSessionService {
|
|
|
3260
3528
|
return (this.modelPolicyRuntimeBlocks.get(session.sessionId) ??
|
|
3261
3529
|
this.modelPolicyEntryReasons.get(session));
|
|
3262
3530
|
}
|
|
3263
|
-
/**
|
|
3264
|
-
* Root-session policy initialization. `true` records the runtime's own tuple;
|
|
3265
|
-
* an update applies the requested transition. Both complete before the caller
|
|
3266
|
-
* of `start()` sees the session, so no prompt can precede the policy.
|
|
3267
|
-
*/
|
|
3531
|
+
/** Root-session policy initialization, completed before the root is visible. */
|
|
3268
3532
|
async initializeSessionModelPolicy(session, initializer) {
|
|
3269
3533
|
const current = {
|
|
3270
3534
|
mode: "exact",
|
|
3271
3535
|
exact: this.exactSelectionFromSession(session),
|
|
3272
3536
|
};
|
|
3273
|
-
if (initializer ===
|
|
3537
|
+
if (initializer.kind === "runtime-default") {
|
|
3274
3538
|
this.appendSessionModelPolicy(session, current);
|
|
3275
3539
|
this.inspectAndCacheSessionModelPolicy(session);
|
|
3276
3540
|
return;
|
|
3277
3541
|
}
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3542
|
+
if (initializer.kind === "legacy-update") {
|
|
3543
|
+
// Root creation reports failure through its own abort/dispose cleanup
|
|
3544
|
+
// rather than publishing a transition for a session no client has seen.
|
|
3545
|
+
await this.applySessionModelPolicy(session, current, initializer.update, {
|
|
3546
|
+
action: "initialize the session model policy",
|
|
3547
|
+
});
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
throw new Error("A complete initial model policy requires durable plus-root initialization");
|
|
3551
|
+
}
|
|
3552
|
+
async initializeCompleteSessionModelPolicy(session, policy, source) {
|
|
3553
|
+
const plan = planSessionModelPolicyInitialization(policy, (tier) => {
|
|
3554
|
+
const resolved = this.modelTierRegistry.resolve(tier);
|
|
3555
|
+
return {
|
|
3556
|
+
model: { provider: resolved.model.provider, id: resolved.model.id },
|
|
3557
|
+
thinkingLevel: resolved.thinkingLevel,
|
|
3558
|
+
};
|
|
3282
3559
|
});
|
|
3560
|
+
// Only the active target is checked against the current runtime catalog.
|
|
3561
|
+
// The inactive branch remains remembered even when it is unavailable.
|
|
3562
|
+
const target = await this.resolveAvailableExactSelection(session, plan.target);
|
|
3563
|
+
const settings = modelPolicySettingsPersistence(session.settingsManager);
|
|
3564
|
+
await settleModelPolicySettings(settings, "before complete session initialization");
|
|
3565
|
+
this.assertModelPolicyMutationIdle(session, "initialize the session model policy");
|
|
3566
|
+
const initialization = {
|
|
3567
|
+
previousSelection: this.exactSelectionFromSession(session),
|
|
3568
|
+
previousSettings: captureModelPolicySettings(settings),
|
|
3569
|
+
};
|
|
3570
|
+
try {
|
|
3571
|
+
await this.runSessionModelPolicyMutation(session, "initialize the session model policy", async () => {
|
|
3572
|
+
await this.applyExactSelection(session, target);
|
|
3573
|
+
// Pi queues global default writes behind its setters and swallows their
|
|
3574
|
+
// storage failures, so prove durability before any transcript record
|
|
3575
|
+
// exists: everything after this point must be reversible.
|
|
3576
|
+
await settleModelPolicySettings(settings, "while applying initial model defaults");
|
|
3577
|
+
this.appendSessionModelPolicy(session, plan.policy);
|
|
3578
|
+
this.verifyPersistedSessionModelPolicy(session, plan.policy);
|
|
3579
|
+
this.appendSessionCreationSource(session, source);
|
|
3580
|
+
await this.commitAndVerifyInitialSessionEntries(session, plan.policy);
|
|
3581
|
+
});
|
|
3582
|
+
this.inspectAndCacheSessionModelPolicy(session);
|
|
3583
|
+
return initialization;
|
|
3584
|
+
}
|
|
3585
|
+
catch (error) {
|
|
3586
|
+
const rollbackFailures = await this.rollbackCompleteSessionInitialization(session, initialization);
|
|
3587
|
+
throw completeInitializationFailure(error, rollbackFailures);
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
async rollbackCompleteSessionInitialization(session, initialization) {
|
|
3591
|
+
const failures = [];
|
|
3592
|
+
if (!(await this.restoreExactSelection(session, initialization.previousSelection))) {
|
|
3593
|
+
failures.push(new Error("the previous runtime model and thinking pair was not restored"));
|
|
3594
|
+
}
|
|
3595
|
+
try {
|
|
3596
|
+
await restoreModelPolicySettings(modelPolicySettingsPersistence(session.settingsManager), initialization.previousSettings);
|
|
3597
|
+
}
|
|
3598
|
+
catch (error) {
|
|
3599
|
+
failures.push(normalizeError(error));
|
|
3600
|
+
}
|
|
3601
|
+
try {
|
|
3602
|
+
await this.sessionManager.discardInitialEntries?.(session.sessionManager);
|
|
3603
|
+
}
|
|
3604
|
+
catch (error) {
|
|
3605
|
+
failures.push(normalizeError(error));
|
|
3606
|
+
}
|
|
3607
|
+
try {
|
|
3608
|
+
this.inspectAndCacheSessionModelPolicy(session);
|
|
3609
|
+
}
|
|
3610
|
+
catch (error) {
|
|
3611
|
+
failures.push(normalizeError(error));
|
|
3612
|
+
}
|
|
3613
|
+
return failures;
|
|
3614
|
+
}
|
|
3615
|
+
async commitAndVerifyInitialSessionEntries(session, expectedPolicy) {
|
|
3616
|
+
if (this.sessionManager.commitInitialEntries === undefined) {
|
|
3617
|
+
throw new Error("Cannot durably commit the complete initial session policy and provenance");
|
|
3618
|
+
}
|
|
3619
|
+
await this.sessionManager.commitInitialEntries(session.sessionManager);
|
|
3620
|
+
const sessionFile = session.sessionManager.getSessionFile();
|
|
3621
|
+
if (!sessionFileExists(sessionFile))
|
|
3622
|
+
throw new Error("Cannot verify the durable initial session transcript");
|
|
3623
|
+
const reopened = this.sessionManager.open(sessionFile);
|
|
3624
|
+
const entries = reopened.getEntries?.() ?? reopened.getBranch();
|
|
3625
|
+
const policyInspection = inspectSessionModelPolicy(entries, expectedPolicy.exact);
|
|
3626
|
+
if (policyInspection.kind !== "persisted" ||
|
|
3627
|
+
!sameSessionModelPolicy(policyInspection.policy, expectedPolicy)) {
|
|
3628
|
+
throw new Error("Cannot verify the complete initial session model policy after reopen");
|
|
3629
|
+
}
|
|
3630
|
+
const sourceInspection = inspectSessionCreationSource(entries);
|
|
3631
|
+
if (sourceInspection.kind !== "valid") {
|
|
3632
|
+
throw new Error("Cannot verify the session creation source after reopen");
|
|
3633
|
+
}
|
|
3634
|
+
const rootEligibility = this.sessionCreationRootEligibility(reopened, sourceInspection);
|
|
3635
|
+
if (rootEligibility.kind !== "eligible") {
|
|
3636
|
+
throw new Error(`Cannot verify the durable plus-session root origin: ${rootEligibility.reason}`);
|
|
3637
|
+
}
|
|
3283
3638
|
}
|
|
3284
3639
|
/**
|
|
3285
3640
|
* The single policy transition path. Validates the complete target before any
|
|
@@ -3539,6 +3894,35 @@ export class PiSessionService {
|
|
|
3539
3894
|
}
|
|
3540
3895
|
session.sessionManager.appendCustomEntry(SESSION_MODEL_POLICY_CUSTOM_TYPE, serializeSessionModelPolicy(policy));
|
|
3541
3896
|
}
|
|
3897
|
+
verifyPersistedSessionModelPolicy(session, expected) {
|
|
3898
|
+
const inspection = this.inspectAndCacheSessionModelPolicy(session);
|
|
3899
|
+
if (inspection.kind !== "persisted" ||
|
|
3900
|
+
!sameSessionModelPolicy(inspection.policy, expected)) {
|
|
3901
|
+
throw new Error("Cannot verify the complete initial session model policy");
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
appendSessionCreationSource(session, source) {
|
|
3905
|
+
if (session.sessionManager.appendCustomEntry === undefined) {
|
|
3906
|
+
throw new Error("Cannot persist the session creation source: session persistence is unavailable");
|
|
3907
|
+
}
|
|
3908
|
+
const sessionFile = session.sessionManager.getSessionFile();
|
|
3909
|
+
if (sessionFile === undefined || sessionFile === "") {
|
|
3910
|
+
throw new Error("Cannot persist the session creation source without a session file");
|
|
3911
|
+
}
|
|
3912
|
+
if (session.sessionManager.getSessionId() !== session.sessionId) {
|
|
3913
|
+
throw new Error("Cannot persist the session creation source for a mismatched session id");
|
|
3914
|
+
}
|
|
3915
|
+
session.sessionManager.appendCustomEntry(SESSION_CREATION_SOURCE_CUSTOM_TYPE, serializeSessionCreationSource(source, {
|
|
3916
|
+
sessionId: session.sessionId,
|
|
3917
|
+
sessionFile,
|
|
3918
|
+
}));
|
|
3919
|
+
const inspection = inspectSessionCreationSource(this.policyEntries(session));
|
|
3920
|
+
const rootEligibility = this.sessionCreationRootEligibility(session.sessionManager, inspection);
|
|
3921
|
+
if (inspection.kind !== "valid" ||
|
|
3922
|
+
rootEligibility.kind !== "eligible") {
|
|
3923
|
+
throw new Error("Cannot verify the session creation source");
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3542
3926
|
modelPolicyStatusFromSession(session) {
|
|
3543
3927
|
const inspection = this.modelPolicyInspections.get(session);
|
|
3544
3928
|
if (inspection === undefined)
|
|
@@ -3668,6 +4052,19 @@ function findSessionByIdOrPrefix(sessions, sessionId) {
|
|
|
3668
4052
|
function uniqueStrings(values) {
|
|
3669
4053
|
return [...new Set(values)];
|
|
3670
4054
|
}
|
|
4055
|
+
/**
|
|
4056
|
+
* Reconcile duplicate catalog entries by canonical CWD + id, matching the
|
|
4057
|
+
* last-writer-wins convention other catalog reconciliation helpers use. The
|
|
4058
|
+
* reorder loader can read the same session more than once when overlapping
|
|
4059
|
+
* catalog CWDs are requested (e.g. a cross-workspace children scope).
|
|
4060
|
+
*/
|
|
4061
|
+
function uniqueSessionsByIdentity(sessions) {
|
|
4062
|
+
const byIdentity = new Map();
|
|
4063
|
+
for (const session of sessions) {
|
|
4064
|
+
byIdentity.set(JSON.stringify([canonicalizeStoredCwd(session.cwd), session.id]), session);
|
|
4065
|
+
}
|
|
4066
|
+
return [...byIdentity.values()];
|
|
4067
|
+
}
|
|
3671
4068
|
function errorMessage(error) {
|
|
3672
4069
|
return error instanceof Error ? error.message : String(error);
|
|
3673
4070
|
}
|
|
@@ -3704,6 +4101,9 @@ function clientSessionFromListEntry(session) {
|
|
|
3704
4101
|
...(session.parentSessionPath === undefined
|
|
3705
4102
|
? {}
|
|
3706
4103
|
: { parentSessionPath: session.parentSessionPath }),
|
|
4104
|
+
...(session.creationSource === undefined
|
|
4105
|
+
? {}
|
|
4106
|
+
: { creationSource: session.creationSource }),
|
|
3707
4107
|
};
|
|
3708
4108
|
}
|
|
3709
4109
|
function archiveInputFromListEntry(session) {
|
|
@@ -3719,6 +4119,9 @@ function archiveInputFromListEntry(session) {
|
|
|
3719
4119
|
...(session.parentSessionPath === undefined
|
|
3720
4120
|
? {}
|
|
3721
4121
|
: { parentSessionPath: session.parentSessionPath }),
|
|
4122
|
+
...(session.creationSource === undefined
|
|
4123
|
+
? {}
|
|
4124
|
+
: { creationSource: session.creationSource }),
|
|
3722
4125
|
};
|
|
3723
4126
|
}
|
|
3724
4127
|
function archiveInputFromActiveSession(session) {
|
|
@@ -3726,6 +4129,7 @@ function archiveInputFromActiveSession(session) {
|
|
|
3726
4129
|
if (sessionFile === undefined || sessionFile === "")
|
|
3727
4130
|
throw new Error("Session is not persisted");
|
|
3728
4131
|
const parentSessionPath = session.sessionManager.getHeader?.()?.parentSession;
|
|
4132
|
+
const creationSource = validSessionCreationSource(session.sessionManager);
|
|
3729
4133
|
return {
|
|
3730
4134
|
sessionId: session.sessionId,
|
|
3731
4135
|
cwd: session.sessionManager.getCwd(),
|
|
@@ -3736,6 +4140,7 @@ function archiveInputFromActiveSession(session) {
|
|
|
3736
4140
|
firstMessage: "",
|
|
3737
4141
|
...(session.sessionName === undefined ? {} : { name: session.sessionName }),
|
|
3738
4142
|
...(parentSessionPath === undefined ? {} : { parentSessionPath }),
|
|
4143
|
+
...(creationSource === undefined ? {} : { creationSource }),
|
|
3739
4144
|
};
|
|
3740
4145
|
}
|
|
3741
4146
|
function archiveCandidateFromListEntry(session) {
|
|
@@ -3808,6 +4213,7 @@ function clientSessionFromArchivedRecord(record, fallback) {
|
|
|
3808
4213
|
return undefined;
|
|
3809
4214
|
const name = record.name ?? fallback?.name;
|
|
3810
4215
|
const parentSessionPath = record.parentSessionPath ?? fallback?.parentSessionPath;
|
|
4216
|
+
const creationSource = record.creationSource ?? fallback?.creationSource;
|
|
3811
4217
|
return {
|
|
3812
4218
|
id: record.sessionId,
|
|
3813
4219
|
path,
|
|
@@ -3818,10 +4224,22 @@ function clientSessionFromArchivedRecord(record, fallback) {
|
|
|
3818
4224
|
messageCount,
|
|
3819
4225
|
firstMessage,
|
|
3820
4226
|
...(parentSessionPath === undefined ? {} : { parentSessionPath }),
|
|
4227
|
+
...(creationSource === undefined ? {} : { creationSource }),
|
|
3821
4228
|
archived: true,
|
|
3822
4229
|
archivedAt: record.archivedAt,
|
|
3823
4230
|
};
|
|
3824
4231
|
}
|
|
4232
|
+
function validSessionCreationSource(manager) {
|
|
4233
|
+
const inspection = inspectSessionCreationSource(manager.getEntries?.() ?? manager.getBranch());
|
|
4234
|
+
return inspection.kind === "valid" ? inspection.source : undefined;
|
|
4235
|
+
}
|
|
4236
|
+
function sameSessionModelPolicy(left, right) {
|
|
4237
|
+
return (left.mode === right.mode &&
|
|
4238
|
+
left.tier === right.tier &&
|
|
4239
|
+
left.exact.model.provider === right.exact.model.provider &&
|
|
4240
|
+
left.exact.model.id === right.exact.model.id &&
|
|
4241
|
+
left.exact.thinkingLevel === right.exact.thinkingLevel);
|
|
4242
|
+
}
|
|
3825
4243
|
function addSessionName(names, name) {
|
|
3826
4244
|
const trimmed = name?.replace(/\s+/g, " ").trim();
|
|
3827
4245
|
if (trimmed !== undefined && trimmed !== "")
|
|
@@ -3837,10 +4255,28 @@ function archivedTimestamp(record) {
|
|
|
3837
4255
|
function isDefined(value) {
|
|
3838
4256
|
return value !== undefined;
|
|
3839
4257
|
}
|
|
3840
|
-
function mergeSessionMetadata(session,
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
4258
|
+
function mergeSessionMetadata(session, metadata) {
|
|
4259
|
+
const entry = metadata[session.path];
|
|
4260
|
+
const pinned = entry?.pinned === true;
|
|
4261
|
+
const scope = session.parentSessionPath === undefined
|
|
4262
|
+
? { kind: "root", cwd: canonicalizeStoredCwd(session.cwd) }
|
|
4263
|
+
: { kind: "children", parentSessionPath: session.parentSessionPath };
|
|
4264
|
+
const order = entry?.order;
|
|
4265
|
+
const manualOrder = session.archived !== true
|
|
4266
|
+
&& order?.pinned === pinned
|
|
4267
|
+
&& sessionReorderScopesEqual(order.scope, scope)
|
|
4268
|
+
? order.position
|
|
4269
|
+
: undefined;
|
|
4270
|
+
return {
|
|
4271
|
+
...session,
|
|
4272
|
+
...(pinned ? { pinned: true } : {}),
|
|
4273
|
+
...(manualOrder === undefined ? {} : { manualOrder }),
|
|
4274
|
+
};
|
|
4275
|
+
}
|
|
4276
|
+
function sessionReorderScopesEqual(a, b) {
|
|
4277
|
+
if (a.kind === "root")
|
|
4278
|
+
return b.kind === "root" && a.cwd === b.cwd;
|
|
4279
|
+
return b.kind === "children" && a.parentSessionPath === b.parentSessionPath;
|
|
3844
4280
|
}
|
|
3845
4281
|
/**
|
|
3846
4282
|
* Resolve the delegation capability from server-owned, persisted session
|