presubmit 0.1.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/LICENSE +202 -0
- package/README.md +51 -0
- package/THIRD_PARTY_NOTICES.md +24 -0
- package/apps/web/dist/assets/FileCode-CZZH52o7.js +5 -0
- package/apps/web/dist/assets/FileCode-DovDO7Jh.css +1 -0
- package/apps/web/dist/assets/SettingsPage-CRvOwdjt.js +4 -0
- package/apps/web/dist/assets/SettingsPage-UbYSCwJj.css +1 -0
- package/apps/web/dist/assets/TerminalPanel-0sw-J6YS.css +1 -0
- package/apps/web/dist/assets/TerminalPanel-BqUjPWav.js +16 -0
- package/apps/web/dist/assets/index-BhqNNFja.js +54 -0
- package/apps/web/dist/assets/index-DD8VXy8H.css +1 -0
- package/apps/web/dist/assets/presubmit-mark-BBQ19Fmr.svg +7 -0
- package/apps/web/dist/assets/presubmit-mark-busy-CwBC-7on.svg +8 -0
- package/apps/web/dist/assets/react-vendor-wiHys0m2.js +9 -0
- package/apps/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/apps/web/dist/index.html +6 -0
- package/apps/web/dist/service-worker.js +11 -0
- package/dist/apps/server/src/agent-builder.js +204 -0
- package/dist/apps/server/src/attach.js +163 -0
- package/dist/apps/server/src/attachments.js +86 -0
- package/dist/apps/server/src/browser-access.js +131 -0
- package/dist/apps/server/src/check-provenance.js +95 -0
- package/dist/apps/server/src/check-supervisor.js +112 -0
- package/dist/apps/server/src/checks.js +402 -0
- package/dist/apps/server/src/child-collaboration-guidance.js +2 -0
- package/dist/apps/server/src/child-sessions.js +191 -0
- package/dist/apps/server/src/cli.js +162 -0
- package/dist/apps/server/src/conversation-search.js +12 -0
- package/dist/apps/server/src/coordinator-messages.js +72 -0
- package/dist/apps/server/src/dev-auth.js +54 -0
- package/dist/apps/server/src/dev-server.js +201 -0
- package/dist/apps/server/src/dev-worker.js +48 -0
- package/dist/apps/server/src/diagnostics.js +105 -0
- package/dist/apps/server/src/fast-mode.js +8 -0
- package/dist/apps/server/src/folder-picker.js +54 -0
- package/dist/apps/server/src/git-blobs.js +62 -0
- package/dist/apps/server/src/git-handoff.js +127 -0
- package/dist/apps/server/src/history-sources.js +59 -0
- package/dist/apps/server/src/hub.js +159 -0
- package/dist/apps/server/src/internal-transport.js +38 -0
- package/dist/apps/server/src/local-embeddings.js +70 -0
- package/dist/apps/server/src/ollama-setup.js +56 -0
- package/dist/apps/server/src/planning.js +30 -0
- package/dist/apps/server/src/project-agents.js +276 -0
- package/dist/apps/server/src/project-memory.js +94 -0
- package/dist/apps/server/src/projects.js +106 -0
- package/dist/apps/server/src/queued-launch.js +13 -0
- package/dist/apps/server/src/reply-search.js +172 -0
- package/dist/apps/server/src/review-context.js +60 -0
- package/dist/apps/server/src/review-refresh.js +14 -0
- package/dist/apps/server/src/reviewers.js +75 -0
- package/dist/apps/server/src/search-settings.js +22 -0
- package/dist/apps/server/src/semantic-history.js +186 -0
- package/dist/apps/server/src/server.js +4338 -0
- package/dist/apps/server/src/session-changes.js +181 -0
- package/dist/apps/server/src/session-router-settings.js +198 -0
- package/dist/apps/server/src/session-workspaces.js +135 -0
- package/dist/apps/server/src/sessions.js +579 -0
- package/dist/apps/server/src/shared-auth.js +70 -0
- package/dist/apps/server/src/shell-evaluator-settings.js +53 -0
- package/dist/apps/server/src/store.js +203 -0
- package/dist/apps/server/src/tailscale-process.js +115 -0
- package/dist/apps/server/src/tailscale-serve-worker.js +33 -0
- package/dist/apps/server/src/tailscale-serve.js +276 -0
- package/dist/apps/server/src/task-capacity.js +92 -0
- package/dist/apps/server/src/task-graph.js +190 -0
- package/dist/apps/server/src/task-logs.js +74 -0
- package/dist/apps/server/src/task-numbers.js +25 -0
- package/dist/apps/server/src/task-orchestrator.js +1088 -0
- package/dist/apps/server/src/terminal.js +182 -0
- package/dist/apps/server/src/terminals.js +80 -0
- package/dist/apps/server/src/tool-safety.js +2 -0
- package/dist/apps/server/src/workspace-lock.js +140 -0
- package/dist/apps/server/src/workspace-watcher.js +65 -0
- package/dist/apps/server/src/workspace.js +517 -0
- package/dist/packages/agent-pi/src/agent-builder-tool.js +32 -0
- package/dist/packages/agent-pi/src/agent-capabilities.js +44 -0
- package/dist/packages/agent-pi/src/ask-user.js +86 -0
- package/dist/packages/agent-pi/src/auth.js +510 -0
- package/dist/packages/agent-pi/src/check-tools.js +72 -0
- package/dist/packages/agent-pi/src/command-policy.js +246 -0
- package/dist/packages/agent-pi/src/coordinator-tools.js +122 -0
- package/dist/packages/agent-pi/src/fast-mode.js +67 -0
- package/dist/packages/agent-pi/src/file-tools.js +163 -0
- package/dist/packages/agent-pi/src/history-tool.js +40 -0
- package/dist/packages/agent-pi/src/history.js +34 -0
- package/dist/packages/agent-pi/src/index.js +177 -0
- package/dist/packages/agent-pi/src/ipc.js +314 -0
- package/dist/packages/agent-pi/src/jev-session-router.js +408 -0
- package/dist/packages/agent-pi/src/jev-tools.js +71 -0
- package/dist/packages/agent-pi/src/local-model.js +277 -0
- package/dist/packages/agent-pi/src/memory-tools.js +119 -0
- package/dist/packages/agent-pi/src/model.js +25 -0
- package/dist/packages/agent-pi/src/planning.js +206 -0
- package/dist/packages/agent-pi/src/resource-loader.js +23 -0
- package/dist/packages/agent-pi/src/review-tools.js +158 -0
- package/dist/packages/agent-pi/src/review-transition.js +73 -0
- package/dist/packages/agent-pi/src/review.js +40 -0
- package/dist/packages/agent-pi/src/routing.js +27 -0
- package/dist/packages/agent-pi/src/session-setup.js +118 -0
- package/dist/packages/agent-pi/src/shell-context.js +51 -0
- package/dist/packages/agent-pi/src/shell-policy.js +281 -0
- package/dist/packages/agent-pi/src/shell-safety.js +205 -0
- package/dist/packages/agent-pi/src/shell.js +121 -0
- package/dist/packages/agent-pi/src/snapshot-permissions.js +12 -0
- package/dist/packages/agent-pi/src/spike.js +41 -0
- package/dist/packages/agent-pi/src/task-tools.js +53 -0
- package/dist/packages/agent-pi/src/types.js +1 -0
- package/dist/packages/agent-pi/src/usage.js +30 -0
- package/dist/packages/agent-pi/src/worker.js +602 -0
- package/dist/packages/protocol/src/activity-summary.js +77 -0
- package/dist/packages/protocol/src/agents.js +301 -0
- package/dist/packages/protocol/src/browser-access.js +1 -0
- package/dist/packages/protocol/src/checks.js +40 -0
- package/dist/packages/protocol/src/child-sessions.js +28 -0
- package/dist/packages/protocol/src/command-policy.js +3 -0
- package/dist/packages/protocol/src/coordinator.js +135 -0
- package/dist/packages/protocol/src/delegation.js +40 -0
- package/dist/packages/protocol/src/git-diff.js +71 -0
- package/dist/packages/protocol/src/git-handoff.js +1 -0
- package/dist/packages/protocol/src/index.js +1 -0
- package/dist/packages/protocol/src/memory.js +46 -0
- package/dist/packages/protocol/src/metrics.js +70 -0
- package/dist/packages/protocol/src/planning.js +138 -0
- package/dist/packages/protocol/src/retrieval.js +36 -0
- package/dist/packages/protocol/src/reviewers.js +17 -0
- package/dist/packages/protocol/src/search.js +16 -0
- package/dist/packages/protocol/src/session-changes.js +29 -0
- package/dist/packages/protocol/src/session-router.js +123 -0
- package/dist/packages/protocol/src/shell-evaluator.js +43 -0
- package/dist/packages/protocol/src/task-graph.js +1 -0
- package/dist/packages/protocol/src/token-usage.js +17 -0
- package/dist/packages/protocol/src/user-questions.js +106 -0
- package/package.json +71 -0
- package/vendor/lancedb/NODEJS_THIRD_PARTY_LICENSES.md +668 -0
- package/vendor/lancedb/RUST_THIRD_PARTY_LICENSES.html +14607 -0
- package/vendor/lancedb/dist/arrow.d.ts +296 -0
- package/vendor/lancedb/dist/arrow.js +1145 -0
- package/vendor/lancedb/dist/arrow_type.d.ts +9 -0
- package/vendor/lancedb/dist/arrow_type.js +29 -0
- package/vendor/lancedb/dist/connection.d.ts +508 -0
- package/vendor/lancedb/dist/connection.js +298 -0
- package/vendor/lancedb/dist/embedding/embedding_function.d.ts +103 -0
- package/vendor/lancedb/dist/embedding/embedding_function.js +192 -0
- package/vendor/lancedb/dist/embedding/index.d.ts +37 -0
- package/vendor/lancedb/dist/embedding/index.js +108 -0
- package/vendor/lancedb/dist/embedding/openai.d.ts +16 -0
- package/vendor/lancedb/dist/embedding/openai.js +81 -0
- package/vendor/lancedb/dist/embedding/registry.d.ts +94 -0
- package/vendor/lancedb/dist/embedding/registry.js +221 -0
- package/vendor/lancedb/dist/embedding/transformers.d.ts +36 -0
- package/vendor/lancedb/dist/embedding/transformers.js +110 -0
- package/vendor/lancedb/dist/header.d.ts +162 -0
- package/vendor/lancedb/dist/header.js +217 -0
- package/vendor/lancedb/dist/index.d.ts +246 -0
- package/vendor/lancedb/dist/index.js +176 -0
- package/vendor/lancedb/dist/indices.d.ts +721 -0
- package/vendor/lancedb/dist/indices.js +166 -0
- package/vendor/lancedb/dist/materialized_view.d.ts +69 -0
- package/vendor/lancedb/dist/materialized_view.js +112 -0
- package/vendor/lancedb/dist/merge.d.ts +104 -0
- package/vendor/lancedb/dist/merge.js +120 -0
- package/vendor/lancedb/dist/native.d.ts +1193 -0
- package/vendor/lancedb/dist/native.js +688 -0
- package/vendor/lancedb/dist/oauth.d.ts +66 -0
- package/vendor/lancedb/dist/oauth.js +15 -0
- package/vendor/lancedb/dist/otel.d.ts +26 -0
- package/vendor/lancedb/dist/otel.js +114 -0
- package/vendor/lancedb/dist/permutation.d.ts +143 -0
- package/vendor/lancedb/dist/permutation.js +184 -0
- package/vendor/lancedb/dist/query.d.ts +663 -0
- package/vendor/lancedb/dist/query.js +966 -0
- package/vendor/lancedb/dist/rerankers/index.d.ts +5 -0
- package/vendor/lancedb/dist/rerankers/index.js +19 -0
- package/vendor/lancedb/dist/rerankers/rrf.d.ts +14 -0
- package/vendor/lancedb/dist/rerankers/rrf.js +28 -0
- package/vendor/lancedb/dist/sanitize.d.ts +32 -0
- package/vendor/lancedb/dist/sanitize.js +544 -0
- package/vendor/lancedb/dist/scannable.d.ts +92 -0
- package/vendor/lancedb/dist/scannable.js +200 -0
- package/vendor/lancedb/dist/schema.d.ts +16 -0
- package/vendor/lancedb/dist/schema.js +387 -0
- package/vendor/lancedb/dist/table.d.ts +1069 -0
- package/vendor/lancedb/dist/table.js +498 -0
- package/vendor/lancedb/dist/util.d.ts +14 -0
- package/vendor/lancedb/dist/util.js +77 -0
- package/vendor/lancedb/license_header.txt +2 -0
- package/vendor/lancedb/package.json +56 -0
- package/vendor/transformers/LICENSE +202 -0
- package/vendor/transformers/dist/transformers.cjs +30844 -0
- package/vendor/transformers/dist/transformers.mjs +31404 -0
- package/vendor/transformers/package.json +55 -0
- package/vendor/transformers/types/backends/onnx.d.ts +30 -0
- package/vendor/transformers/types/configs.d.ts +85 -0
- package/vendor/transformers/types/env.d.ts +110 -0
- package/vendor/transformers/types/generation/configuration_utils.d.ts +320 -0
- package/vendor/transformers/types/generation/logits_process.d.ts +354 -0
- package/vendor/transformers/types/generation/logits_sampler.d.ts +51 -0
- package/vendor/transformers/types/generation/parameters.d.ts +47 -0
- package/vendor/transformers/types/generation/stopping_criteria.d.ts +81 -0
- package/vendor/transformers/types/generation/streamers.d.ts +81 -0
- package/vendor/transformers/types/models/whisper/common_whisper.d.ts +8 -0
- package/vendor/transformers/types/models/whisper/generation_whisper.d.ts +76 -0
- package/vendor/transformers/types/models.d.ts +3684 -0
- package/vendor/transformers/types/ops/registry.d.ts +11 -0
- package/vendor/transformers/types/pipelines.d.ts +2402 -0
- package/vendor/transformers/types/processors.d.ts +924 -0
- package/vendor/transformers/types/tokenizers.d.ts +990 -0
- package/vendor/transformers/types/transformers.d.ts +13 -0
- package/vendor/transformers/types/utils/audio.d.ts +130 -0
- package/vendor/transformers/types/utils/constants.d.ts +2 -0
- package/vendor/transformers/types/utils/core.d.ts +98 -0
- package/vendor/transformers/types/utils/data-structures.d.ts +236 -0
- package/vendor/transformers/types/utils/devices.d.ts +18 -0
- package/vendor/transformers/types/utils/dtypes.d.ts +19 -0
- package/vendor/transformers/types/utils/generic.d.ts +11 -0
- package/vendor/transformers/types/utils/hub.d.ts +152 -0
- package/vendor/transformers/types/utils/image.d.ts +119 -0
- package/vendor/transformers/types/utils/maths.d.ts +280 -0
- package/vendor/transformers/types/utils/tensor.d.ts +418 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import { activeCheck } from '../../../packages/protocol/src/checks.js';
|
|
2
|
+
import { projectTaskGraph } from './task-graph.js';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { sumTokenUsage } from '../../../packages/protocol/src/token-usage.js';
|
|
6
|
+
import { searchExcerpt, searchPattern, } from '../../../packages/protocol/src/search.js';
|
|
7
|
+
import { searchConversation } from './conversation-search.js';
|
|
8
|
+
import { SemanticHistory } from './semantic-history.js';
|
|
9
|
+
import { historySources } from './history-sources.js';
|
|
10
|
+
import { createSessionWorkspace, selectSessionBranch, validateSessionWorkspace, } from './session-workspaces.js';
|
|
11
|
+
function durableSetup(setup) {
|
|
12
|
+
const { routingTrace: _routingTrace, ...durable } = setup;
|
|
13
|
+
return durable;
|
|
14
|
+
}
|
|
15
|
+
export function summarizeSession(record, state, error) {
|
|
16
|
+
const active = state?.runs.find((run) => run.status === 'running');
|
|
17
|
+
const pending = state?.approvals.filter((a) => a.status === 'pending' && (a.runId === active?.id || Boolean(a.checkId))).length ?? 0;
|
|
18
|
+
const pendingQuestions = state?.userQuestions?.filter((question) => question.status === 'pending' && question.runId === active?.id).length ?? 0;
|
|
19
|
+
// Assigned sessions share the parent's delegation view, so only the parent rolls up requests.
|
|
20
|
+
const latestAttempts = new Map((record.assignment ? [] : (state?.delegation?.attempts ?? [])).map((attempt) => [
|
|
21
|
+
attempt.taskId,
|
|
22
|
+
attempt,
|
|
23
|
+
]));
|
|
24
|
+
const childPendingApprovals = [...latestAttempts.values()].reduce((count, attempt) => count +
|
|
25
|
+
(['starting', 'running'].includes(attempt.status) ? (attempt.pendingApprovals ?? 0) : 0), 0);
|
|
26
|
+
const last = state?.runs.at(-1);
|
|
27
|
+
const lastEvent = state?.events
|
|
28
|
+
.filter((event) => event.text && event.type !== 'command' && event.type !== 'reasoning')
|
|
29
|
+
.at(-1);
|
|
30
|
+
// Workspace refreshes update Files/Changes without advancing recency or unread state.
|
|
31
|
+
const lastActivity = state?.events.findLast((event) => event.type !== 'workspace_changed');
|
|
32
|
+
return {
|
|
33
|
+
activeChecks: state?.checks?.filter(activeCheck).length ?? 0,
|
|
34
|
+
assignment: record.assignment,
|
|
35
|
+
parentSessionId: record.parentSessionId,
|
|
36
|
+
childSessionNumber: record.childSessionNumber,
|
|
37
|
+
mode: record.preferences?.mode,
|
|
38
|
+
archived: record.archived ?? false,
|
|
39
|
+
id: record.id,
|
|
40
|
+
title: record.title,
|
|
41
|
+
workspace: record.workspace,
|
|
42
|
+
branch: record.branch,
|
|
43
|
+
createdAt: record.createdAt,
|
|
44
|
+
activeRunStartedAt: active?.createdAt,
|
|
45
|
+
latestActivitySeq: lastActivity?.seq ?? 0,
|
|
46
|
+
latestActivityAt: [record.createdAt, last?.createdAt, lastActivity?.createdAt]
|
|
47
|
+
.filter((value) => Boolean(value))
|
|
48
|
+
.sort((a, b) => Date.parse(b) - Date.parse(a))[0],
|
|
49
|
+
status: error
|
|
50
|
+
? 'unavailable'
|
|
51
|
+
: active || state?.checks?.some(activeCheck)
|
|
52
|
+
? pending || pendingQuestions
|
|
53
|
+
? 'waiting'
|
|
54
|
+
: 'running'
|
|
55
|
+
: last?.status === 'stopped'
|
|
56
|
+
? 'completed'
|
|
57
|
+
: (last?.status ?? 'idle'),
|
|
58
|
+
runs: state?.runs.length ?? 0,
|
|
59
|
+
usage: sumTokenUsage(state?.runs ?? []),
|
|
60
|
+
pendingApprovals: pending,
|
|
61
|
+
childPendingApprovals,
|
|
62
|
+
pendingQuestions,
|
|
63
|
+
latestActivity: lastEvent?.text?.slice(0, 180) ?? last?.prompt.slice(0, 180),
|
|
64
|
+
error,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export async function createSessionManager(options) {
|
|
68
|
+
const records = options.store.get('agentSessions') ?? [];
|
|
69
|
+
const numberChildren = () => {
|
|
70
|
+
if (!options.taskNumber)
|
|
71
|
+
return numberChildSessions(records);
|
|
72
|
+
let changed = false;
|
|
73
|
+
for (const record of records)
|
|
74
|
+
if (record.parentSessionId && !record.assignment) {
|
|
75
|
+
const number = options.taskNumber(record.parentSessionId, `child:${record.id}`, record.childSessionNumber);
|
|
76
|
+
if (record.childSessionNumber !== number) {
|
|
77
|
+
record.childSessionNumber = number;
|
|
78
|
+
changed = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return changed;
|
|
82
|
+
};
|
|
83
|
+
if (numberChildren())
|
|
84
|
+
options.store.put('agentSessions', records);
|
|
85
|
+
const main = options.store.get('mainSession') ?? {
|
|
86
|
+
id: 'main',
|
|
87
|
+
title: 'Main workspace',
|
|
88
|
+
workspace: options.repository,
|
|
89
|
+
createdAt: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
options.store.put('mainSession', main);
|
|
92
|
+
const children = new Map();
|
|
93
|
+
const opening = new Map();
|
|
94
|
+
const errors = new Map();
|
|
95
|
+
const creating = new Map();
|
|
96
|
+
let publicOrigin;
|
|
97
|
+
let publicProject;
|
|
98
|
+
const publicUrl = (id) => `${publicOrigin}/s/${id}${publicProject ? '?project=' + encodeURIComponent(publicProject) : ''}`;
|
|
99
|
+
let closing = false;
|
|
100
|
+
function pendingChildApprovals(parentId) {
|
|
101
|
+
return records
|
|
102
|
+
.filter((record) => record.parentSessionId === parentId && !record.assignment)
|
|
103
|
+
.flatMap((record) => {
|
|
104
|
+
const state = children.get(record.id)?.app.getSummaryState();
|
|
105
|
+
const active = state?.runs.find((run) => run.status === 'running');
|
|
106
|
+
return (state?.approvals ?? [])
|
|
107
|
+
.filter((approval) => approval.status === 'pending' && approval.runId === active?.id)
|
|
108
|
+
.map((approval) => ({
|
|
109
|
+
sessionId: record.id,
|
|
110
|
+
label: `${record.childSessionNumber ? `T${record.childSessionNumber} · ` : ''}${record.title}`,
|
|
111
|
+
approval,
|
|
112
|
+
}));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const summaries = () => {
|
|
116
|
+
const result = [
|
|
117
|
+
summarizeSession(main, options.mainState()),
|
|
118
|
+
...records.map((record) => summarizeSession(record, children.get(record.id)?.app.getSummaryState(), errors.get(record.id))),
|
|
119
|
+
];
|
|
120
|
+
const byId = new Map(result.map((summary) => [summary.id, summary]));
|
|
121
|
+
const ownChildPending = new Map(result.map((summary) => [summary.id, summary.childPendingApprovals ?? 0]));
|
|
122
|
+
const branchPending = (id, seen = new Set()) => {
|
|
123
|
+
if (seen.has(id))
|
|
124
|
+
return 0;
|
|
125
|
+
const summary = byId.get(id);
|
|
126
|
+
if (!summary)
|
|
127
|
+
return 0;
|
|
128
|
+
const nextSeen = new Set(seen).add(id);
|
|
129
|
+
return (summary.pendingApprovals +
|
|
130
|
+
(ownChildPending.get(id) ?? 0) +
|
|
131
|
+
records
|
|
132
|
+
.filter((record) => record.parentSessionId === id && !record.assignment)
|
|
133
|
+
.reduce((count, child) => count + branchPending(child.id, nextSeen), 0));
|
|
134
|
+
};
|
|
135
|
+
for (const summary of result)
|
|
136
|
+
summary.childPendingApprovals =
|
|
137
|
+
(ownChildPending.get(summary.id) ?? 0) +
|
|
138
|
+
records
|
|
139
|
+
.filter((record) => record.parentSessionId === summary.id && !record.assignment)
|
|
140
|
+
.reduce((count, child) => count + branchPending(child.id), 0);
|
|
141
|
+
return result;
|
|
142
|
+
};
|
|
143
|
+
function coordinator(record) {
|
|
144
|
+
const parent = record.assignment?.coordinatorId === 'main'
|
|
145
|
+
? main
|
|
146
|
+
: records.find((item) => item.id === record.assignment?.coordinatorId);
|
|
147
|
+
if (!parent || parent.assignment)
|
|
148
|
+
throw new Error('Shared task workspace requires an existing parent session.');
|
|
149
|
+
return parent;
|
|
150
|
+
}
|
|
151
|
+
function ordinaryParent(id) {
|
|
152
|
+
const parent = id === 'main' ? main : records.find((r) => r.id === id);
|
|
153
|
+
if (!parent)
|
|
154
|
+
throw new Error('Child sessions require an existing parent session.');
|
|
155
|
+
return parent;
|
|
156
|
+
}
|
|
157
|
+
const open = (record, initialPrompt, initialAttachmentIds, initialSetup, initialRoutingTrace) => {
|
|
158
|
+
if (closing || children.has(record.id))
|
|
159
|
+
return Promise.resolve();
|
|
160
|
+
const pending = opening.get(record.id);
|
|
161
|
+
if (pending)
|
|
162
|
+
return pending;
|
|
163
|
+
const operation = openRecord(record, initialPrompt, initialAttachmentIds, initialSetup, initialRoutingTrace).finally(() => opening.delete(record.id));
|
|
164
|
+
opening.set(record.id, operation);
|
|
165
|
+
return operation;
|
|
166
|
+
};
|
|
167
|
+
const openRecord = async (record, initialPrompt, initialAttachmentIds, initialSetup, initialRoutingTrace) => {
|
|
168
|
+
if (closing || children.has(record.id))
|
|
169
|
+
return;
|
|
170
|
+
let app;
|
|
171
|
+
try {
|
|
172
|
+
const initialReviewer = record.reviewer
|
|
173
|
+
? options.store.get('reviewer-session:' + record.id)
|
|
174
|
+
: undefined;
|
|
175
|
+
if (record.reviewer &&
|
|
176
|
+
(!initialReviewer ||
|
|
177
|
+
JSON.stringify(initialReviewer.binding) !== JSON.stringify(record.reviewer)))
|
|
178
|
+
throw new Error('Saved reviewer snapshot is unavailable; this session cannot become a coding session.');
|
|
179
|
+
const ordinaryShared = !!record.parentSessionId && record.useWorktree === false;
|
|
180
|
+
const shared = record.assignment?.workspaceMode === 'coordinator' || ordinaryShared;
|
|
181
|
+
if (shared) {
|
|
182
|
+
const parent = ordinaryShared
|
|
183
|
+
? ordinaryParent(record.parentSessionId)
|
|
184
|
+
: coordinator(record);
|
|
185
|
+
if (record.workspace !== parent.workspace || record.branch !== parent.branch)
|
|
186
|
+
throw new Error('Saved task workspace does not match its coordinator.');
|
|
187
|
+
// The coordinator owns this checkout's lock for the lifetime of its children.
|
|
188
|
+
if (parent.id !== 'main')
|
|
189
|
+
await open(parent);
|
|
190
|
+
if (parent.id !== 'main' && !children.has(parent.id))
|
|
191
|
+
throw new Error('Coordinator workspace is unavailable. Restore it before resuming tasks.');
|
|
192
|
+
}
|
|
193
|
+
else if (record.useWorktree !== false) {
|
|
194
|
+
await validateSessionWorkspace(options.repository, record.workspace, record.branch);
|
|
195
|
+
}
|
|
196
|
+
else if (record.workspace !== options.repository) {
|
|
197
|
+
throw new Error('Saved session checkout does not match the repository.');
|
|
198
|
+
}
|
|
199
|
+
app = await options.start({
|
|
200
|
+
cwd: record.workspace,
|
|
201
|
+
dataDir: join(options.dataDir, 'sessions', record.id),
|
|
202
|
+
webDir: options.webDir,
|
|
203
|
+
port: 0,
|
|
204
|
+
isSession: true,
|
|
205
|
+
onStateChange: () => {
|
|
206
|
+
// Parent Reviews collect attributed progress without injecting a coding turn.
|
|
207
|
+
if (record.reviewer && record.parentSessionId)
|
|
208
|
+
children.get(record.parentSessionId)?.app.notifyTask?.();
|
|
209
|
+
options.changed();
|
|
210
|
+
},
|
|
211
|
+
initialPrompt,
|
|
212
|
+
initialSetup,
|
|
213
|
+
initialRoutingTrace,
|
|
214
|
+
initialRoute: initialPrompt ? record.setup?.route : undefined,
|
|
215
|
+
initialAttachmentIds,
|
|
216
|
+
...options.childOptions(),
|
|
217
|
+
sessionPreferences: record.preferences,
|
|
218
|
+
taskNumber: options.taskNumber
|
|
219
|
+
? (id, preferred) => options.taskNumber(record.id, `task:${id}`, preferred)
|
|
220
|
+
: undefined,
|
|
221
|
+
launchQueued: options.launchQueued
|
|
222
|
+
? (launch) => options.launchQueued(record.id, launch)
|
|
223
|
+
: undefined,
|
|
224
|
+
assignment: record.assignment,
|
|
225
|
+
parentSessionId: record.parentSessionId,
|
|
226
|
+
sessionRecordId: record.id,
|
|
227
|
+
initialReviewer,
|
|
228
|
+
startChild: options.startChild
|
|
229
|
+
? (key, fingerprint, launch, validate) => options.startChild(record.id, key, fingerprint, launch, validate)
|
|
230
|
+
: undefined,
|
|
231
|
+
reviewerState: options.reviewerState,
|
|
232
|
+
listChildren: () => options.listChildren?.(record.id) ?? [],
|
|
233
|
+
childApprovals: () => pendingChildApprovals(record.id),
|
|
234
|
+
acquireChildExecution: () => options.acquireChildExecution?.(record.id),
|
|
235
|
+
taskBridge: options.bridge?.(record.assignment?.coordinatorId ?? record.id),
|
|
236
|
+
}, shared);
|
|
237
|
+
const token = new URL(app.url).searchParams.get('token');
|
|
238
|
+
const response = await app.request('/api/auth', {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers: { 'Content-Type': 'application/json', Origin: app.origin },
|
|
241
|
+
body: JSON.stringify({ token }),
|
|
242
|
+
});
|
|
243
|
+
const cookie = response.headers.get('set-cookie')?.split(';')[0];
|
|
244
|
+
if (!response.ok || !cookie)
|
|
245
|
+
throw new Error('Could not establish the session connection.');
|
|
246
|
+
app.setTitle(record.title);
|
|
247
|
+
children.set(record.id, { app, cookie });
|
|
248
|
+
if (publicOrigin)
|
|
249
|
+
app.setConnectionUrl(publicUrl(record.id));
|
|
250
|
+
errors.delete(record.id);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
await app?.close();
|
|
254
|
+
errors.set(record.id, error instanceof Error ? error.message : 'Agent session unavailable.');
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
// Restore a few sessions at a time so saved history does not serialize all disk/Git work.
|
|
258
|
+
// Keep a bound: every session opens a store, watcher, and private listener.
|
|
259
|
+
let nextRecord = 0;
|
|
260
|
+
await Promise.all(Array.from({ length: Math.min(4, records.length) }, async () => {
|
|
261
|
+
while (nextRecord < records.length)
|
|
262
|
+
await open(records[nextRecord++]);
|
|
263
|
+
}));
|
|
264
|
+
const sources = historySources(() => [
|
|
265
|
+
{ id: 'main', title: main.title, store: options.store, runs: options.mainState().runs },
|
|
266
|
+
...records.flatMap((record) => {
|
|
267
|
+
const child = children.get(record.id)?.app;
|
|
268
|
+
return child
|
|
269
|
+
? [
|
|
270
|
+
{
|
|
271
|
+
id: record.id,
|
|
272
|
+
title: record.title,
|
|
273
|
+
store: child.store,
|
|
274
|
+
runs: child.getSummaryState().runs,
|
|
275
|
+
},
|
|
276
|
+
]
|
|
277
|
+
: [];
|
|
278
|
+
}),
|
|
279
|
+
]);
|
|
280
|
+
let semantic;
|
|
281
|
+
const retiring = new Set();
|
|
282
|
+
function setEmbeddings(provider) {
|
|
283
|
+
if (closing)
|
|
284
|
+
throw new Error('Server is stopping.');
|
|
285
|
+
const previous = semantic;
|
|
286
|
+
semantic = provider
|
|
287
|
+
? new SemanticHistory(join(options.dataDir, 'conversation-vectors'), provider, sources)
|
|
288
|
+
: undefined;
|
|
289
|
+
semantic?.start();
|
|
290
|
+
if (previous) {
|
|
291
|
+
const retired = previous.close().catch(() => { });
|
|
292
|
+
retiring.add(retired);
|
|
293
|
+
void retired.then(() => retiring.delete(retired));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
setEmbeddings(options.embeddings);
|
|
297
|
+
return {
|
|
298
|
+
summaries,
|
|
299
|
+
taskGraph: () => projectTaskGraph(summaries(), (id) => id === 'main' ? options.mainState() : children.get(id)?.app.getSummaryState()),
|
|
300
|
+
pendingChildApprovals,
|
|
301
|
+
navigationUrl: (id) => `/s/${id}${publicProject ? '?project=' + encodeURIComponent(publicProject) : ''}`,
|
|
302
|
+
setEmbeddings,
|
|
303
|
+
retrievalStatus: () => semantic?.status() ?? { enabled: false, indexing: false, indexedChunks: 0 },
|
|
304
|
+
async retrieve(query, limit = 5, excludeRunId) {
|
|
305
|
+
if (!semantic || closing)
|
|
306
|
+
throw new Error('Project history retrieval is unavailable.');
|
|
307
|
+
return semantic.search(query, limit, excludeRunId);
|
|
308
|
+
},
|
|
309
|
+
async search(query, mode = 'exact') {
|
|
310
|
+
if (mode === 'related') {
|
|
311
|
+
if (closing)
|
|
312
|
+
throw new Error('Server is stopping.');
|
|
313
|
+
// Global search can include projects that have not enabled semantic retrieval.
|
|
314
|
+
if (!semantic)
|
|
315
|
+
return { results: [], hasMore: false, unavailable: [] };
|
|
316
|
+
const status = semantic.status();
|
|
317
|
+
const hits = status.indexing && !status.indexedChunks ? [] : await semantic.search(query, 10);
|
|
318
|
+
const seen = new Set();
|
|
319
|
+
return {
|
|
320
|
+
results: hits
|
|
321
|
+
.filter((hit) => {
|
|
322
|
+
if (seen.has(hit.sessionId))
|
|
323
|
+
return false;
|
|
324
|
+
seen.add(hit.sessionId);
|
|
325
|
+
return true;
|
|
326
|
+
})
|
|
327
|
+
.map((hit) => ({
|
|
328
|
+
sessionId: hit.sessionId,
|
|
329
|
+
title: hit.sessionTitle,
|
|
330
|
+
runId: hit.runId,
|
|
331
|
+
kind: hit.kind,
|
|
332
|
+
archived: Boolean(records.find((r) => r.id === hit.sessionId)?.archived),
|
|
333
|
+
projectId: 'main',
|
|
334
|
+
projectName: options.repository.split('/').filter(Boolean).at(-1) ?? 'Project',
|
|
335
|
+
updatedAt: hit.createdAt,
|
|
336
|
+
score: hit.score,
|
|
337
|
+
excerpt: {
|
|
338
|
+
before: '',
|
|
339
|
+
match: '',
|
|
340
|
+
after: hit.text.slice(0, 240) + (hit.text.length > 240 ? '…' : ''),
|
|
341
|
+
},
|
|
342
|
+
})),
|
|
343
|
+
hasMore: hits.length === 10,
|
|
344
|
+
unavailable: status.error ? [status.error] : [],
|
|
345
|
+
indexing: status.indexing,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const results = [];
|
|
349
|
+
const unavailable = [];
|
|
350
|
+
const pattern = searchPattern(query);
|
|
351
|
+
const ordered = summaries().sort((a, b) => (b.latestActivityAt ?? b.createdAt).localeCompare(a.latestActivityAt ?? a.createdAt));
|
|
352
|
+
for (const session of ordered) {
|
|
353
|
+
if (closing)
|
|
354
|
+
throw new Error('Server is stopping.');
|
|
355
|
+
const app = children.get(session.id)?.app;
|
|
356
|
+
const titleMatch = searchExcerpt(session.title, pattern);
|
|
357
|
+
if (session.status === 'unavailable')
|
|
358
|
+
unavailable.push(session.title);
|
|
359
|
+
const match = titleMatch
|
|
360
|
+
? { kind: 'title', excerpt: titleMatch }
|
|
361
|
+
: session.id === 'main'
|
|
362
|
+
? searchConversation(options.store, options.mainState().runs, query)
|
|
363
|
+
: app
|
|
364
|
+
? searchConversation(app.store, app.getSummaryState().runs, query)
|
|
365
|
+
: undefined;
|
|
366
|
+
if (match) {
|
|
367
|
+
results.push({
|
|
368
|
+
...match,
|
|
369
|
+
sessionId: session.id,
|
|
370
|
+
title: session.title,
|
|
371
|
+
archived: Boolean(session.archived),
|
|
372
|
+
updatedAt: session.latestActivityAt ?? session.createdAt,
|
|
373
|
+
projectId: 'main',
|
|
374
|
+
projectName: options.repository.split('/').filter(Boolean).at(-1) ?? 'Project',
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if (results.length > 50)
|
|
378
|
+
break;
|
|
379
|
+
// Let execution, cancellation and live connections progress between sessions.
|
|
380
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
381
|
+
}
|
|
382
|
+
return { results: results.slice(0, 50), hasMore: results.length > 50, unavailable };
|
|
383
|
+
},
|
|
384
|
+
setArchived(id, archived, expectedArchived) {
|
|
385
|
+
if (closing)
|
|
386
|
+
throw new Error('Server is stopping.');
|
|
387
|
+
const record = id === 'main' ? main : records.find((item) => item.id === id);
|
|
388
|
+
if (!record)
|
|
389
|
+
throw new Error('Session not found.');
|
|
390
|
+
if (expectedArchived !== undefined && Boolean(record.archived) !== expectedArchived)
|
|
391
|
+
throw new Error('Session archive status changed; refresh before updating it.');
|
|
392
|
+
const state = id === 'main' ? options.mainState() : children.get(id)?.app.getSummaryState();
|
|
393
|
+
if (archived &&
|
|
394
|
+
(options.taskBusy?.(id) ||
|
|
395
|
+
creating.has(id) ||
|
|
396
|
+
state?.runs.some((run) => run.status === 'running')))
|
|
397
|
+
throw new Error('Stop the session before archiving it.');
|
|
398
|
+
record.archived = archived;
|
|
399
|
+
options.store.put(id === 'main' ? 'mainSession' : 'agentSessions', id === 'main' ? main : records);
|
|
400
|
+
options.changed();
|
|
401
|
+
return summaries().find((session) => session.id === id);
|
|
402
|
+
},
|
|
403
|
+
setOrigin(origin, project) {
|
|
404
|
+
publicOrigin = origin;
|
|
405
|
+
publicProject = project;
|
|
406
|
+
for (const [id, { app }] of children)
|
|
407
|
+
app.setConnectionUrl(publicUrl(id));
|
|
408
|
+
},
|
|
409
|
+
hasActiveRuns: () => creating.size > 0 || [...children.values()].some(({ app }) => app.hasActiveRuns()),
|
|
410
|
+
connection(id) {
|
|
411
|
+
const connection = children.get(id);
|
|
412
|
+
if (!connection)
|
|
413
|
+
throw new Error(errors.get(id) ?? 'Agent session not found.');
|
|
414
|
+
return connection;
|
|
415
|
+
},
|
|
416
|
+
create(id, title, prompt, useWorktree = true, attachments = [], sourceBranch, preferences, assignment, parentSessionId, reviewer) {
|
|
417
|
+
if (closing)
|
|
418
|
+
return Promise.reject(new Error('Server is stopping.'));
|
|
419
|
+
if (parentSessionId &&
|
|
420
|
+
(assignment ||
|
|
421
|
+
(parentSessionId !== 'main' && !records.some((record) => record.id === parentSessionId))))
|
|
422
|
+
return Promise.reject(new Error('Child sessions require an existing source session.'));
|
|
423
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id))
|
|
424
|
+
return Promise.reject(new Error('Invalid session ID.'));
|
|
425
|
+
if (!title.trim() || title.length > 120)
|
|
426
|
+
return Promise.reject(new Error('Enter a session title of at most 120 characters.'));
|
|
427
|
+
const promptHash = prompt
|
|
428
|
+
? createHash('sha256')
|
|
429
|
+
.update(attachments.length ? JSON.stringify({ prompt, attachments }) : prompt)
|
|
430
|
+
.digest('hex')
|
|
431
|
+
: undefined;
|
|
432
|
+
const existing = records.find((record) => record.id === id);
|
|
433
|
+
const savedReviewer = options.store.get('reviewer-session:' + id);
|
|
434
|
+
if ((reviewer &&
|
|
435
|
+
(reviewer.binding.parentSessionId !== parentSessionId ||
|
|
436
|
+
assignment ||
|
|
437
|
+
useWorktree ||
|
|
438
|
+
prompt)) ||
|
|
439
|
+
(savedReviewer && JSON.stringify(savedReviewer) !== JSON.stringify(reviewer)) ||
|
|
440
|
+
(existing && JSON.stringify(existing.reviewer) !== JSON.stringify(reviewer?.binding)))
|
|
441
|
+
return Promise.reject(new Error('Reviewer session identity cannot change.'));
|
|
442
|
+
if (reviewer)
|
|
443
|
+
options.store.put('reviewer-session:' + id, reviewer);
|
|
444
|
+
if (existing &&
|
|
445
|
+
((existing.requestTitle ?? existing.title) !== title.trim() ||
|
|
446
|
+
existing.promptHash !== promptHash ||
|
|
447
|
+
(existing.useWorktree !== false) !== useWorktree ||
|
|
448
|
+
existing.sourceBranch !== sourceBranch ||
|
|
449
|
+
existing.parentSessionId !== parentSessionId ||
|
|
450
|
+
JSON.stringify(existing.preferences) !== JSON.stringify(preferences) ||
|
|
451
|
+
JSON.stringify(existing.assignment) !== JSON.stringify(assignment)))
|
|
452
|
+
return Promise.reject(new Error('Session ID already used for a different request.'));
|
|
453
|
+
const inFlight = creating.get(id);
|
|
454
|
+
if (inFlight) {
|
|
455
|
+
if (inFlight.title !== title.trim() ||
|
|
456
|
+
inFlight.promptHash !== promptHash ||
|
|
457
|
+
inFlight.useWorktree !== useWorktree ||
|
|
458
|
+
inFlight.sourceBranch !== sourceBranch ||
|
|
459
|
+
inFlight.parentSessionId !== parentSessionId ||
|
|
460
|
+
JSON.stringify(inFlight.preferences) !== JSON.stringify(preferences) ||
|
|
461
|
+
JSON.stringify(inFlight.assignment) !== JSON.stringify(assignment))
|
|
462
|
+
return Promise.reject(new Error('Session ID already used for a different request.'));
|
|
463
|
+
return inFlight.promise;
|
|
464
|
+
}
|
|
465
|
+
const pending = (async () => {
|
|
466
|
+
let record = existing;
|
|
467
|
+
let prepared;
|
|
468
|
+
const routingTrace = [];
|
|
469
|
+
const setupPromise = !record && prompt && options.prepare && !assignment
|
|
470
|
+
? options
|
|
471
|
+
.prepare(prompt, attachments, preferences, (trace) => routingTrace.push(trace))
|
|
472
|
+
.then((setup) => {
|
|
473
|
+
prepared = setup;
|
|
474
|
+
if (!closing && record) {
|
|
475
|
+
record.title = setup.title;
|
|
476
|
+
record.setup = durableSetup(setup);
|
|
477
|
+
options.store.put('agentSessions', records);
|
|
478
|
+
children.get(record.id)?.app.setTitle(setup.title);
|
|
479
|
+
options.changed();
|
|
480
|
+
}
|
|
481
|
+
return setup;
|
|
482
|
+
})
|
|
483
|
+
: undefined;
|
|
484
|
+
void setupPromise?.catch(() => { });
|
|
485
|
+
if (!record) {
|
|
486
|
+
if (closing)
|
|
487
|
+
throw new Error('Server is stopping.');
|
|
488
|
+
const shared = assignment?.workspaceMode === 'coordinator';
|
|
489
|
+
const parent = shared
|
|
490
|
+
? coordinator({ assignment })
|
|
491
|
+
: parentSessionId && !useWorktree
|
|
492
|
+
? ordinaryParent(parentSessionId)
|
|
493
|
+
: undefined;
|
|
494
|
+
if (shared && (useWorktree || sourceBranch || assignment?.baseCommit))
|
|
495
|
+
throw new Error('Shared tasks must use their coordinator workspace without a branch or base override.');
|
|
496
|
+
if (!shared && !useWorktree && sourceBranch) {
|
|
497
|
+
await selectSessionBranch(options.repository, sourceBranch, () => options.mainState().runs.some((run) => run.status === 'running') ||
|
|
498
|
+
[...creating.keys()].some((key) => key !== id) ||
|
|
499
|
+
[...children.values()].some(({ app }) => app.getSummaryState().runs.some((run) => run.status === 'running')));
|
|
500
|
+
}
|
|
501
|
+
const created = parent
|
|
502
|
+
? { workspace: parent.workspace, branch: parent.branch }
|
|
503
|
+
: useWorktree
|
|
504
|
+
? await createSessionWorkspace({
|
|
505
|
+
repository: options.repository,
|
|
506
|
+
directory: join(options.dataDir, 'worktrees'),
|
|
507
|
+
id,
|
|
508
|
+
branch: sourceBranch,
|
|
509
|
+
baseCommit: assignment?.baseCommit,
|
|
510
|
+
})
|
|
511
|
+
: { workspace: options.repository };
|
|
512
|
+
record = {
|
|
513
|
+
id,
|
|
514
|
+
title: prepared?.title ?? title.trim(),
|
|
515
|
+
requestTitle: title.trim(),
|
|
516
|
+
setup: prepared && durableSetup(prepared),
|
|
517
|
+
preferences,
|
|
518
|
+
assignment,
|
|
519
|
+
parentSessionId,
|
|
520
|
+
reviewer: reviewer?.binding,
|
|
521
|
+
...created,
|
|
522
|
+
useWorktree,
|
|
523
|
+
sourceBranch,
|
|
524
|
+
promptHash,
|
|
525
|
+
createdAt: new Date().toISOString(),
|
|
526
|
+
};
|
|
527
|
+
records.push(record);
|
|
528
|
+
numberChildren();
|
|
529
|
+
options.store.put('agentSessions', records);
|
|
530
|
+
}
|
|
531
|
+
await open(record, prompt, attachments, setupPromise, routingTrace);
|
|
532
|
+
options.changed();
|
|
533
|
+
return summaries().find((session) => session.id === id);
|
|
534
|
+
})();
|
|
535
|
+
creating.set(id, {
|
|
536
|
+
title: title.trim(),
|
|
537
|
+
useWorktree,
|
|
538
|
+
sourceBranch,
|
|
539
|
+
preferences,
|
|
540
|
+
assignment,
|
|
541
|
+
parentSessionId,
|
|
542
|
+
promptHash,
|
|
543
|
+
promise: pending,
|
|
544
|
+
});
|
|
545
|
+
void pending.finally(() => creating.delete(id)).catch(() => { });
|
|
546
|
+
return pending;
|
|
547
|
+
},
|
|
548
|
+
async close() {
|
|
549
|
+
closing = true;
|
|
550
|
+
await semantic?.close();
|
|
551
|
+
await Promise.allSettled(retiring);
|
|
552
|
+
await Promise.allSettled([...creating.values()].map((item) => item.promise));
|
|
553
|
+
// Reap shared workers before releasing the coordinator's workspace ownership lock.
|
|
554
|
+
const sharedIds = new Set(records
|
|
555
|
+
.filter((record) => record.assignment?.workspaceMode === 'coordinator' ||
|
|
556
|
+
(!!record.parentSessionId && record.useWorktree === false))
|
|
557
|
+
.map((record) => record.id));
|
|
558
|
+
await Promise.allSettled([...children.entries()]
|
|
559
|
+
.filter(([id]) => sharedIds.has(id))
|
|
560
|
+
.map(([, { app }]) => app.close()));
|
|
561
|
+
await Promise.allSettled([...children.entries()]
|
|
562
|
+
.filter(([id]) => !sharedIds.has(id))
|
|
563
|
+
.map(([, { app }]) => app.close()));
|
|
564
|
+
children.clear();
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
/** Assign once in persisted creation order, including archived children. Never renumber. */
|
|
569
|
+
export function numberChildSessions(records) {
|
|
570
|
+
let next = records.reduce((max, record) => Math.max(max, record.childSessionNumber ?? 0), 0);
|
|
571
|
+
let changed = false;
|
|
572
|
+
for (const record of records) {
|
|
573
|
+
if (record.parentSessionId && !record.assignment && record.childSessionNumber === undefined) {
|
|
574
|
+
record.childSessionNumber = ++next;
|
|
575
|
+
changed = true;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return changed;
|
|
579
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { chmod, copyFile, mkdir, mkdtemp, readdir, readFile, rename, rm, stat, } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/** Publish a complete credential directory atomically; never overwrite a shared login. */
|
|
4
|
+
export async function sharedAuthPath(home, legacyDataDir) {
|
|
5
|
+
const directory = join(home, 'credentials');
|
|
6
|
+
const destination = join(directory, 'openai.json');
|
|
7
|
+
try {
|
|
8
|
+
await stat(directory);
|
|
9
|
+
return destination;
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (error.code !== 'ENOENT')
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
16
|
+
const candidates = [];
|
|
17
|
+
const paths = new Set();
|
|
18
|
+
if (legacyDataDir)
|
|
19
|
+
paths.add(join(legacyDataDir, 'credentials', 'openai.json'));
|
|
20
|
+
for (const entry of await readdir(home, { withFileTypes: true })) {
|
|
21
|
+
if (entry.isDirectory() && /^[a-f0-9]{20}$/.test(entry.name))
|
|
22
|
+
paths.add(join(home, entry.name, 'credentials', 'openai.json'));
|
|
23
|
+
}
|
|
24
|
+
for (const path of paths) {
|
|
25
|
+
try {
|
|
26
|
+
const info = await stat(path);
|
|
27
|
+
const value = JSON.parse(await readFile(path, 'utf8'));
|
|
28
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length)
|
|
29
|
+
candidates.push({ path, modified: info.mtimeMs });
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code !== 'ENOENT' && !(error instanceof SyntaxError))
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Prefer the most recently saved login when migrating several older repositories.
|
|
37
|
+
const source = candidates.sort((a, b) => b.modified - a.modified)[0]?.path;
|
|
38
|
+
const temp = await mkdtemp(join(home, '.credentials-'));
|
|
39
|
+
try {
|
|
40
|
+
if (source) {
|
|
41
|
+
for (const suffix of ['', '.provider', '.provider.model']) {
|
|
42
|
+
try {
|
|
43
|
+
const target = join(temp, 'openai.json' + suffix);
|
|
44
|
+
await copyFile(source + suffix, target);
|
|
45
|
+
await chmod(target, 0o600);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (suffix === '' || error.code !== 'ENOENT')
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// The marker prevents an intentional disconnect from re-importing legacy credentials.
|
|
55
|
+
const { writeFile } = await import('node:fs/promises');
|
|
56
|
+
await writeFile(join(temp, '.initialized'), '', { mode: 0o600 });
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
await rename(temp, directory);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (!['EEXIST', 'ENOTEMPTY'].includes(error.code ?? ''))
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await rm(temp, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
return destination;
|
|
70
|
+
}
|