@clinebot/core 0.0.6 → 0.0.10
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/dist/agents/hooks-config-loader.d.ts +1 -0
- package/dist/auth/cline.d.ts +2 -0
- package/dist/auth/codex.d.ts +5 -1
- package/dist/auth/oca.d.ts +7 -1
- package/dist/auth/types.d.ts +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.node.d.ts +2 -0
- package/dist/index.node.js +164 -162
- package/dist/input/mention-enricher.d.ts +1 -0
- package/dist/providers/local-provider-service.d.ts +1 -1
- package/dist/runtime/session-runtime.d.ts +1 -1
- package/dist/session/default-session-manager.d.ts +13 -17
- package/dist/session/rpc-spawn-lease.d.ts +7 -0
- package/dist/session/runtime-oauth-token-manager.d.ts +4 -2
- package/dist/session/session-agent-events.d.ts +15 -0
- package/dist/session/session-config-builder.d.ts +13 -0
- package/dist/session/session-manager.d.ts +2 -2
- package/dist/session/session-team-coordination.d.ts +12 -0
- package/dist/session/session-telemetry.d.ts +9 -0
- package/dist/session/unified-session-persistence-service.d.ts +12 -16
- package/dist/session/utils/helpers.d.ts +1 -1
- package/dist/session/utils/types.d.ts +1 -1
- package/dist/storage/provider-settings-legacy-migration.d.ts +25 -0
- package/dist/telemetry/core-events.d.ts +122 -0
- package/dist/tools/definitions.d.ts +1 -1
- package/dist/tools/executors/file-read.d.ts +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/presets.d.ts +1 -1
- package/dist/tools/schemas.d.ts +48 -11
- package/dist/tools/types.d.ts +3 -3
- package/dist/types/config.d.ts +1 -1
- package/dist/types/events.d.ts +1 -1
- package/dist/types/provider-settings.d.ts +4 -4
- package/dist/types.d.ts +1 -1
- package/package.json +4 -3
- package/src/agents/hooks-config-loader.ts +2 -0
- package/src/auth/cline.ts +35 -1
- package/src/auth/codex.ts +27 -2
- package/src/auth/oca.ts +31 -4
- package/src/auth/types.ts +3 -0
- package/src/index.node.ts +4 -0
- package/src/index.ts +27 -0
- package/src/input/file-indexer.test.ts +40 -0
- package/src/input/file-indexer.ts +21 -0
- package/src/input/mention-enricher.test.ts +3 -0
- package/src/input/mention-enricher.ts +3 -0
- package/src/providers/local-provider-service.ts +6 -7
- package/src/runtime/hook-file-hooks.test.ts +51 -1
- package/src/runtime/hook-file-hooks.ts +91 -11
- package/src/runtime/session-runtime.ts +1 -1
- package/src/session/default-session-manager.e2e.test.ts +2 -1
- package/src/session/default-session-manager.ts +367 -601
- package/src/session/rpc-spawn-lease.test.ts +49 -0
- package/src/session/rpc-spawn-lease.ts +122 -0
- package/src/session/runtime-oauth-token-manager.ts +21 -14
- package/src/session/session-agent-events.ts +159 -0
- package/src/session/session-config-builder.ts +111 -0
- package/src/session/session-graph.ts +2 -0
- package/src/session/session-host.ts +21 -0
- package/src/session/session-manager.ts +2 -2
- package/src/session/session-team-coordination.ts +198 -0
- package/src/session/session-telemetry.ts +95 -0
- package/src/session/unified-session-persistence-service.test.ts +81 -0
- package/src/session/unified-session-persistence-service.ts +470 -469
- package/src/session/utils/helpers.ts +1 -1
- package/src/session/utils/types.ts +1 -1
- package/src/storage/provider-settings-legacy-migration.test.ts +133 -1
- package/src/storage/provider-settings-legacy-migration.ts +63 -11
- package/src/telemetry/core-events.ts +344 -0
- package/src/tools/definitions.test.ts +203 -36
- package/src/tools/definitions.ts +66 -28
- package/src/tools/executors/editor.test.ts +35 -0
- package/src/tools/executors/editor.ts +33 -46
- package/src/tools/executors/file-read.test.ts +29 -5
- package/src/tools/executors/file-read.ts +17 -6
- package/src/tools/index.ts +2 -0
- package/src/tools/presets.ts +1 -1
- package/src/tools/schemas.ts +88 -38
- package/src/tools/types.ts +7 -3
- package/src/types/config.ts +1 -1
- package/src/types/events.ts +6 -1
- package/src/types/provider-settings.ts +6 -6
- package/src/types.ts +1 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { AgentResult, TeamEvent } from "@clinebot/agents";
|
|
2
|
+
import { formatUserInputBlock } from "@clinebot/shared";
|
|
3
|
+
import {
|
|
4
|
+
buildTeamProgressSummary,
|
|
5
|
+
toTeamProgressLifecycleEvent,
|
|
6
|
+
} from "../team";
|
|
7
|
+
import type { CoreSessionEvent } from "../types/events";
|
|
8
|
+
import type { ActiveSession, TeamRunUpdate } from "./utils/types";
|
|
9
|
+
|
|
10
|
+
export function trackTeamRunState(
|
|
11
|
+
session: ActiveSession,
|
|
12
|
+
event: TeamEvent,
|
|
13
|
+
): void {
|
|
14
|
+
switch (event.type) {
|
|
15
|
+
case "run_queued":
|
|
16
|
+
case "run_started":
|
|
17
|
+
session.activeTeamRunIds.add(event.run.id);
|
|
18
|
+
break;
|
|
19
|
+
case "run_completed":
|
|
20
|
+
case "run_failed":
|
|
21
|
+
case "run_cancelled":
|
|
22
|
+
case "run_interrupted": {
|
|
23
|
+
let runError: string | undefined;
|
|
24
|
+
if (event.type === "run_failed") {
|
|
25
|
+
runError = event.run.error;
|
|
26
|
+
} else if (
|
|
27
|
+
event.type === "run_cancelled" ||
|
|
28
|
+
event.type === "run_interrupted"
|
|
29
|
+
) {
|
|
30
|
+
runError = event.run.error ?? event.reason;
|
|
31
|
+
}
|
|
32
|
+
session.activeTeamRunIds.delete(event.run.id);
|
|
33
|
+
session.pendingTeamRunUpdates.push({
|
|
34
|
+
runId: event.run.id,
|
|
35
|
+
agentId: event.run.agentId,
|
|
36
|
+
taskId: event.run.taskId,
|
|
37
|
+
status: event.type.replace("run_", "") as TeamRunUpdate["status"],
|
|
38
|
+
error: runError,
|
|
39
|
+
iterations: event.run.result?.iterations,
|
|
40
|
+
});
|
|
41
|
+
notifyTeamRunWaiters(session);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
default:
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function dispatchTeamEventToBackend(
|
|
50
|
+
rootSessionId: string,
|
|
51
|
+
event: TeamEvent,
|
|
52
|
+
invokeOptional: (method: string, ...args: unknown[]) => Promise<void>,
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
switch (event.type) {
|
|
55
|
+
case "task_start":
|
|
56
|
+
await invokeOptional(
|
|
57
|
+
"onTeamTaskStart",
|
|
58
|
+
rootSessionId,
|
|
59
|
+
event.agentId,
|
|
60
|
+
event.message,
|
|
61
|
+
);
|
|
62
|
+
break;
|
|
63
|
+
case "task_end": {
|
|
64
|
+
if (event.error) {
|
|
65
|
+
await invokeOptional(
|
|
66
|
+
"onTeamTaskEnd",
|
|
67
|
+
rootSessionId,
|
|
68
|
+
event.agentId,
|
|
69
|
+
"failed",
|
|
70
|
+
`[error] ${event.error.message}`,
|
|
71
|
+
event.messages,
|
|
72
|
+
);
|
|
73
|
+
} else if (event.result?.finishReason === "aborted") {
|
|
74
|
+
await invokeOptional(
|
|
75
|
+
"onTeamTaskEnd",
|
|
76
|
+
rootSessionId,
|
|
77
|
+
event.agentId,
|
|
78
|
+
"cancelled",
|
|
79
|
+
"[done] aborted",
|
|
80
|
+
event.result.messages,
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
await invokeOptional(
|
|
84
|
+
"onTeamTaskEnd",
|
|
85
|
+
rootSessionId,
|
|
86
|
+
event.agentId,
|
|
87
|
+
"completed",
|
|
88
|
+
`[done] ${event.result?.finishReason ?? "completed"}`,
|
|
89
|
+
event.result?.messages,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
default:
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function emitTeamProgress(
|
|
100
|
+
session: ActiveSession,
|
|
101
|
+
rootSessionId: string,
|
|
102
|
+
event: TeamEvent,
|
|
103
|
+
emit: (event: CoreSessionEvent) => void,
|
|
104
|
+
): void {
|
|
105
|
+
if (!session.runtime.teamRuntime) return;
|
|
106
|
+
const teamName = session.config.teamName?.trim() || "team";
|
|
107
|
+
emit({
|
|
108
|
+
type: "team_progress",
|
|
109
|
+
payload: {
|
|
110
|
+
sessionId: rootSessionId,
|
|
111
|
+
teamName,
|
|
112
|
+
lifecycle: toTeamProgressLifecycleEvent({
|
|
113
|
+
teamName,
|
|
114
|
+
sessionId: rootSessionId,
|
|
115
|
+
event,
|
|
116
|
+
}),
|
|
117
|
+
summary: buildTeamProgressSummary(
|
|
118
|
+
teamName,
|
|
119
|
+
session.runtime.teamRuntime.exportState(),
|
|
120
|
+
),
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function hasPendingTeamRunWork(session: ActiveSession): boolean {
|
|
126
|
+
return (
|
|
127
|
+
session.activeTeamRunIds.size > 0 ||
|
|
128
|
+
session.pendingTeamRunUpdates.length > 0
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function shouldAutoContinueTeamRuns(
|
|
133
|
+
session: ActiveSession,
|
|
134
|
+
finishReason: AgentResult["finishReason"],
|
|
135
|
+
): boolean {
|
|
136
|
+
if (
|
|
137
|
+
session.aborting ||
|
|
138
|
+
finishReason === "aborted" ||
|
|
139
|
+
finishReason === "error"
|
|
140
|
+
) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return (
|
|
144
|
+
session.config.enableAgentTeams === true && hasPendingTeamRunWork(session)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function notifyTeamRunWaiters(session: ActiveSession): void {
|
|
149
|
+
const waiters = session.teamRunWaiters.splice(0);
|
|
150
|
+
for (const resolve of waiters) resolve();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function waitForTeamRunUpdates(
|
|
154
|
+
session: ActiveSession,
|
|
155
|
+
): Promise<TeamRunUpdate[]> {
|
|
156
|
+
while (true) {
|
|
157
|
+
if (session.aborting) return [];
|
|
158
|
+
if (session.pendingTeamRunUpdates.length > 0) {
|
|
159
|
+
const updates = [...session.pendingTeamRunUpdates];
|
|
160
|
+
session.pendingTeamRunUpdates.length = 0;
|
|
161
|
+
return updates;
|
|
162
|
+
}
|
|
163
|
+
if (session.activeTeamRunIds.size === 0) return [];
|
|
164
|
+
await new Promise<void>((resolve) => {
|
|
165
|
+
session.teamRunWaiters.push(resolve);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function buildTeamRunContinuationPrompt(
|
|
171
|
+
session: ActiveSession,
|
|
172
|
+
updates: TeamRunUpdate[],
|
|
173
|
+
): string {
|
|
174
|
+
const lines = updates.map((u) => {
|
|
175
|
+
const parts = [`- ${u.runId} (${u.agentId}) -> ${u.status}`];
|
|
176
|
+
if (u.taskId) parts.push(` task=${u.taskId}`);
|
|
177
|
+
if (typeof u.iterations === "number")
|
|
178
|
+
parts.push(` iterations=${u.iterations}`);
|
|
179
|
+
if (u.error) parts.push(` error=${u.error}`);
|
|
180
|
+
return parts.join("");
|
|
181
|
+
});
|
|
182
|
+
const remaining = session.activeTeamRunIds.size;
|
|
183
|
+
const instruction =
|
|
184
|
+
remaining > 0
|
|
185
|
+
? `There are still ${remaining} teammate run(s) in progress. Continue coordination and decide whether to wait for more updates.`
|
|
186
|
+
: "No teammate runs are currently in progress. Continue coordination using these updates.";
|
|
187
|
+
return formatModePrompt(
|
|
188
|
+
`System-delivered teammate async run updates:\n${lines.join("\n")}\n\n${instruction}`,
|
|
189
|
+
session.config.mode,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function formatModePrompt(
|
|
194
|
+
prompt: string,
|
|
195
|
+
mode: "act" | "plan" | undefined,
|
|
196
|
+
): string {
|
|
197
|
+
return formatUserInputBlock(prompt, mode === "plan" ? "plan" : "act");
|
|
198
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ITelemetryService } from "@clinebot/shared";
|
|
2
|
+
import {
|
|
3
|
+
listHookConfigFiles,
|
|
4
|
+
resolveDocumentsHooksDirectoryPath,
|
|
5
|
+
} from "../agents/hooks-config-loader";
|
|
6
|
+
import type { enrichPromptWithMentions } from "../input";
|
|
7
|
+
import {
|
|
8
|
+
captureHookDiscovery,
|
|
9
|
+
captureMentionFailed,
|
|
10
|
+
captureMentionSearchResults,
|
|
11
|
+
captureMentionUsed,
|
|
12
|
+
captureTaskCreated,
|
|
13
|
+
captureTaskRestarted,
|
|
14
|
+
} from "../telemetry/core-events";
|
|
15
|
+
import type { SessionSource } from "../types/common";
|
|
16
|
+
import type { CoreSessionConfig } from "../types/config";
|
|
17
|
+
|
|
18
|
+
export function emitSessionCreationTelemetry(
|
|
19
|
+
config: CoreSessionConfig,
|
|
20
|
+
sessionId: string,
|
|
21
|
+
source: SessionSource,
|
|
22
|
+
isRestart: boolean,
|
|
23
|
+
workspacePath: string,
|
|
24
|
+
): void {
|
|
25
|
+
if (isRestart) {
|
|
26
|
+
captureTaskRestarted(config.telemetry, {
|
|
27
|
+
ulid: sessionId,
|
|
28
|
+
apiProvider: config.providerId,
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
captureTaskCreated(config.telemetry, {
|
|
32
|
+
ulid: sessionId,
|
|
33
|
+
apiProvider: config.providerId,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
captureHookDiscoveryTelemetry(config.telemetry, { workspacePath });
|
|
37
|
+
config.telemetry?.capture({
|
|
38
|
+
event: "session.started",
|
|
39
|
+
properties: {
|
|
40
|
+
sessionId,
|
|
41
|
+
source,
|
|
42
|
+
providerId: config.providerId,
|
|
43
|
+
modelId: config.modelId,
|
|
44
|
+
enableTools: config.enableTools,
|
|
45
|
+
enableSpawnAgent: config.enableSpawnAgent,
|
|
46
|
+
enableAgentTeams: config.enableAgentTeams,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function captureHookDiscoveryTelemetry(
|
|
52
|
+
telemetry: ITelemetryService | undefined,
|
|
53
|
+
options: { workspacePath: string },
|
|
54
|
+
): void {
|
|
55
|
+
const globalHooksDir = resolveDocumentsHooksDirectoryPath();
|
|
56
|
+
const entries = listHookConfigFiles(options.workspacePath);
|
|
57
|
+
const counts = new Map<string, { global: number; workspace: number }>();
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
const hookName = entry.hookEventName ?? "unknown";
|
|
60
|
+
const current = counts.get(hookName) ?? { global: 0, workspace: 0 };
|
|
61
|
+
if (
|
|
62
|
+
entry.path === globalHooksDir ||
|
|
63
|
+
entry.path.startsWith(`${globalHooksDir}/`)
|
|
64
|
+
) {
|
|
65
|
+
current.global += 1;
|
|
66
|
+
} else {
|
|
67
|
+
current.workspace += 1;
|
|
68
|
+
}
|
|
69
|
+
counts.set(hookName, current);
|
|
70
|
+
}
|
|
71
|
+
for (const [hookName, count] of counts.entries()) {
|
|
72
|
+
captureHookDiscovery(telemetry, hookName, count.global, count.workspace);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function emitMentionTelemetry(
|
|
77
|
+
telemetry: ITelemetryService | undefined,
|
|
78
|
+
enriched: Awaited<ReturnType<typeof enrichPromptWithMentions>>,
|
|
79
|
+
): void {
|
|
80
|
+
for (const mention of enriched.mentions) {
|
|
81
|
+
captureMentionSearchResults(
|
|
82
|
+
telemetry,
|
|
83
|
+
mention,
|
|
84
|
+
enriched.matchedFiles.includes(mention) ? 1 : 0,
|
|
85
|
+
"file",
|
|
86
|
+
!enriched.matchedFiles.includes(mention),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
for (const matched of enriched.matchedFiles) {
|
|
90
|
+
captureMentionUsed(telemetry, "file", matched.length);
|
|
91
|
+
}
|
|
92
|
+
for (const ignored of enriched.ignoredMentions) {
|
|
93
|
+
captureMentionFailed(telemetry, "file", "not_found", ignored);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { SqliteSessionStore } from "../storage/sqlite-session-store";
|
|
6
|
+
import { SessionSource } from "../types/common";
|
|
7
|
+
import { CoreSessionService } from "./session-service";
|
|
8
|
+
|
|
9
|
+
describe("UnifiedSessionPersistenceService", () => {
|
|
10
|
+
const tempDirs: string[] = [];
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
for (const dir of tempDirs.splice(0)) {
|
|
14
|
+
rmSync(dir, { recursive: true, force: true });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("reconciles dead running sessions into failed manifests with terminal markers", async () => {
|
|
19
|
+
const sessionsDir = mkdtempSync(join(tmpdir(), "stale-session-reconcile-"));
|
|
20
|
+
tempDirs.push(sessionsDir);
|
|
21
|
+
|
|
22
|
+
const service = new CoreSessionService(
|
|
23
|
+
new SqliteSessionStore({ sessionsDir }),
|
|
24
|
+
);
|
|
25
|
+
const sessionId = "stale-root-session";
|
|
26
|
+
const artifacts = await service.createRootSessionWithArtifacts({
|
|
27
|
+
sessionId,
|
|
28
|
+
source: SessionSource.CLI,
|
|
29
|
+
pid: 999_999_999,
|
|
30
|
+
interactive: false,
|
|
31
|
+
provider: "mock-provider",
|
|
32
|
+
model: "mock-model",
|
|
33
|
+
cwd: "/tmp/project",
|
|
34
|
+
workspaceRoot: "/tmp/project",
|
|
35
|
+
enableTools: true,
|
|
36
|
+
enableSpawn: true,
|
|
37
|
+
enableTeams: false,
|
|
38
|
+
prompt: "hello",
|
|
39
|
+
startedAt: "2026-01-01T00:00:00.000Z",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const reconciled = await service.reconcileDeadSessions();
|
|
43
|
+
expect(reconciled).toBe(1);
|
|
44
|
+
|
|
45
|
+
const rows = await service.listSessions(10);
|
|
46
|
+
expect(rows).toHaveLength(1);
|
|
47
|
+
expect(rows[0]).toMatchObject({
|
|
48
|
+
session_id: sessionId,
|
|
49
|
+
status: "failed",
|
|
50
|
+
exit_code: 1,
|
|
51
|
+
});
|
|
52
|
+
expect(rows[0]?.ended_at).toBeTruthy();
|
|
53
|
+
|
|
54
|
+
const manifest = JSON.parse(
|
|
55
|
+
readFileSync(artifacts.manifestPath, "utf8"),
|
|
56
|
+
) as Record<string, unknown>;
|
|
57
|
+
expect(manifest.status).toBe("failed");
|
|
58
|
+
expect(manifest.exit_code).toBe(1);
|
|
59
|
+
expect(manifest.ended_at).toBeTruthy();
|
|
60
|
+
expect(manifest.metadata).toMatchObject({
|
|
61
|
+
terminal_marker: "failed_external_process_exit",
|
|
62
|
+
terminal_marker_pid: 999_999_999,
|
|
63
|
+
terminal_marker_source: "stale_session_reconciler",
|
|
64
|
+
});
|
|
65
|
+
expect(
|
|
66
|
+
(manifest.metadata as Record<string, unknown>).terminal_marker_at,
|
|
67
|
+
).toBeTruthy();
|
|
68
|
+
|
|
69
|
+
expect(existsSync(artifacts.hookPath)).toBe(true);
|
|
70
|
+
expect(existsSync(artifacts.transcriptPath)).toBe(true);
|
|
71
|
+
expect(readFileSync(artifacts.hookPath, "utf8")).toContain(
|
|
72
|
+
'"hookName":"session_shutdown"',
|
|
73
|
+
);
|
|
74
|
+
expect(readFileSync(artifacts.hookPath, "utf8")).toContain(
|
|
75
|
+
'"reason":"failed_external_process_exit"',
|
|
76
|
+
);
|
|
77
|
+
expect(readFileSync(artifacts.transcriptPath, "utf8")).toContain(
|
|
78
|
+
"[shutdown] failed_external_process_exit",
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
});
|