Skip to content

Commit 19ce26d

Browse files
committed
improving and getting ready for Paid plans
1 parent 4ffce1b commit 19ce26d

8 files changed

Lines changed: 135 additions & 15 deletions

File tree

src/core/agent/AgentDependencyComposer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ export function initializeAgentDependencies(
358358
apiBaseUrl: runtime.config.telemetry?.apiBaseUrl || 'https://api.autohand.ai',
359359
enableSessionSync: runtime.config.telemetry?.enableSessionSync !== false,
360360
companySecret: runtime.config.telemetry?.companySecret || runtime.config.api?.companySecret || '',
361+
authToken: runtime.config.auth?.token,
361362
clientVersion: packageJson.version
362363
});
363364
host.featureFlagManager = new RemoteFeatureFlagManager(runtime.config);

src/core/agent/AgentSessionAccounting.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,21 @@ function toSyncMessages(messages: SessionMessage[]): Array<{ role: string; conte
106106
}));
107107
}
108108

109-
function buildSessionSyncMetadata(host: AgentSessionAccountingHost, endTimeMs: number) {
109+
function buildSessionSyncMetadata(
110+
host: AgentSessionAccountingHost,
111+
endTimeMs: number,
112+
options: { final?: boolean } = {}
113+
) {
110114
const sessionDuration = Math.max(0, endTimeMs - host.sessionStartedAt);
111-
return {
115+
const metadata = {
112116
workspaceRoot: host.runtime.workspaceRoot,
113117
startTime: new Date(host.sessionStartedAt).toISOString(),
114-
endTime: new Date(endTimeMs).toISOString(),
115118
durationSeconds: Math.round(sessionDuration / 1000),
116119
totalTokens: sessionTotalTokens(host),
117120
};
121+
return options.final
122+
? { ...metadata, endTime: new Date(endTimeMs).toISOString() }
123+
: metadata;
118124
}
119125

120126
export async function syncAgentSessionSnapshot(
@@ -131,7 +137,7 @@ export async function syncAgentSessionSnapshot(
131137
try {
132138
await host.telemetryManager.syncSession({
133139
messages: toSyncMessages(session.getMessages()),
134-
metadata: buildSessionSyncMetadata(host, endTimeMs),
140+
metadata: buildSessionSyncMetadata(host, endTimeMs, { final: options.force }),
135141
});
136142
} finally {
137143
host.sessionSyncInFlight = false;

src/telemetry/TelemetryClient.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -266,10 +266,14 @@ export class TelemetryClient {
266266
workspaceRoot?: string;
267267
};
268268
}): Promise<{ success: boolean; id?: string; error?: string }> {
269-
if (!this.config.enabled || !this.config.enableSessionSync) {
269+
if (!this.config.enableSessionSync) {
270270
return { success: false, error: 'Session sync disabled' };
271271
}
272272

273+
if (!this.config.authToken) {
274+
return { success: false, error: 'Login required for session sync' };
275+
}
276+
273277
const online = await this.isOnline();
274278
if (!online) {
275279
// Queue for later - store in a separate file
@@ -292,14 +296,12 @@ export class TelemetryClient {
292296
}
293297

294298
try {
295-
// Build auth token: {device_id}.{company_secret}
296-
const authToken = `${this.deviceId}.${this.config.companySecret}`;
297-
298299
const response = await fetch(`${this.config.apiBaseUrl}/v1/history`, {
299300
method: 'POST',
300301
headers: {
301302
'Content-Type': 'application/json',
302-
'Authorization': `Bearer ${authToken}`
303+
'Authorization': `Bearer ${this.config.authToken}`,
304+
'X-CLI-Version': this.config.clientVersion || 'unknown'
303305
},
304306
body: JSON.stringify({
305307
deviceId: this.deviceId,

src/telemetry/TelemetryManager.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,6 @@ export class TelemetryManager {
261261
}
262262

263263
const endTimeMs = Date.now();
264-
const endTime = data.metadata?.endTime ?? new Date(endTimeMs).toISOString();
265264
const startTime = data.metadata?.startTime ?? this.sessionStartTime?.toISOString();
266265
const durationSeconds = data.metadata?.durationSeconds ?? this.getSessionDurationSeconds(endTimeMs);
267266

@@ -273,7 +272,7 @@ export class TelemetryManager {
273272
provider: this.currentProvider || undefined,
274273
totalTokens: data.metadata?.totalTokens,
275274
startTime,
276-
endTime,
275+
...(data.metadata?.endTime ? { endTime: data.metadata.endTime } : {}),
277276
durationSeconds,
278277
workspaceRoot: data.metadata?.workspaceRoot
279278
}

src/telemetry/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export interface TelemetryConfig {
5858
enableSessionSync: boolean;
5959
/** Company secret for API authentication */
6060
companySecret: string;
61+
/** Authenticated Autohand session token for user-scoped features */
62+
authToken?: string;
6163
/** Client type (cli, vscode, zed) */
6264
clientType: ClientType;
6365
/** Client/extension version (for non-CLI clients) */

tests/core/agentSessionSync.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,11 @@ describe('agent near-real-time session sync', () => {
6767
metadata: expect.objectContaining({
6868
workspaceRoot: '/workspace/project',
6969
startTime: '2026-05-13T10:00:00.000Z',
70-
endTime: '2026-05-13T10:00:13.000Z',
71-
durationSeconds: 13,
70+
durationSeconds: 18,
7271
totalTokens: 42,
7372
}),
7473
});
74+
expect(syncSession.mock.calls[0][0].metadata).not.toHaveProperty('endTime');
7575
});
7676

7777
it('can force a final snapshot with canonical timing metadata', async () => {
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import fs from 'fs-extra';
3+
import { TelemetryClient } from '../../src/telemetry/TelemetryClient.js';
4+
5+
const { tempRoot } = vi.hoisted(() => ({
6+
tempRoot: `/tmp/autohand-telemetry-client-${process.pid}`,
7+
}));
8+
9+
vi.mock('../../src/constants.js', () => ({
10+
AUTOHAND_PATHS: {
11+
telemetry: `${tempRoot}/telemetry`,
12+
},
13+
AUTOHAND_FILES: {
14+
telemetryQueue: `${tempRoot}/telemetry/queue.json`,
15+
sessionSyncQueue: `${tempRoot}/telemetry/session-sync-queue.json`,
16+
deviceId: `${tempRoot}/device-id`,
17+
},
18+
}));
19+
20+
describe('TelemetryClient session sync', () => {
21+
beforeEach(async () => {
22+
await fs.remove(tempRoot);
23+
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => {
24+
const url = String(input);
25+
if (url.endsWith('/health')) {
26+
return new Response('ok', { status: 200 });
27+
}
28+
return new Response(JSON.stringify({ id: 'history-1' }), { status: 200 });
29+
}));
30+
});
31+
32+
afterEach(async () => {
33+
vi.unstubAllGlobals();
34+
await fs.remove(tempRoot);
35+
});
36+
37+
it('does not upload session snapshots without a logged-in auth token', async () => {
38+
const client = new TelemetryClient({
39+
enabled: false,
40+
enableSessionSync: true,
41+
apiBaseUrl: 'https://api.example.test',
42+
});
43+
44+
const result = await client.uploadSession({
45+
sessionId: 'session-1',
46+
messages: [{ role: 'user', content: 'hello' }],
47+
});
48+
49+
expect(result).toEqual({ success: false, error: 'Login required for session sync' });
50+
expect(fetch).not.toHaveBeenCalledWith(
51+
'https://api.example.test/v1/history',
52+
expect.anything()
53+
);
54+
});
55+
56+
it('uploads session snapshots with the user auth token even when telemetry events are disabled', async () => {
57+
const client = new TelemetryClient({
58+
enabled: false,
59+
enableSessionSync: true,
60+
apiBaseUrl: 'https://api.example.test',
61+
authToken: 'auth-token-123',
62+
clientVersion: '0.8.2',
63+
});
64+
65+
const result = await client.uploadSession({
66+
sessionId: 'session-1',
67+
messages: [{ role: 'user', content: 'hello' }],
68+
});
69+
70+
expect(result).toEqual({ success: true, id: 'history-1' });
71+
expect(fetch).toHaveBeenCalledWith(
72+
'https://api.example.test/v1/history',
73+
expect.objectContaining({
74+
method: 'POST',
75+
headers: expect.objectContaining({
76+
Authorization: 'Bearer auth-token-123',
77+
'X-CLI-Version': '0.8.2',
78+
}),
79+
})
80+
);
81+
});
82+
});

tests/telemetry/TelemetryManager.test.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ describe('TelemetryManager', () => {
105105
expect(trackSpy).not.toHaveBeenCalled();
106106
});
107107

108-
it('includes canonical durationSeconds in synced session metadata', async () => {
108+
it('includes canonical durationSeconds in synced active-session metadata without ending the session', async () => {
109109
const manager = new TelemetryManager({ enabled: true, enableSessionSync: true });
110110

111111
await manager.startSession(
@@ -127,11 +127,39 @@ describe('TelemetryManager', () => {
127127
model: 'gpt-5',
128128
provider: 'openai',
129129
startTime: '2026-05-13T10:00:00.000Z',
130-
endTime: '2026-05-13T10:07:30.000Z',
131130
durationSeconds: 450,
132131
workspaceRoot: '/workspace/project',
133132
totalTokens: 123,
134133
}),
135134
}));
135+
expect(uploadSessionSpy.mock.calls[0][0].metadata).not.toHaveProperty('endTime');
136+
});
137+
138+
it('preserves explicit endTime for final synced session metadata', async () => {
139+
const manager = new TelemetryManager({ enabled: true, enableSessionSync: true });
140+
141+
await manager.startSession(
142+
'session-1',
143+
'gpt-5',
144+
'openai',
145+
new Date('2026-05-13T10:00:00.000Z')
146+
);
147+
148+
await manager.syncSession({
149+
messages: [{ role: 'user', content: 'done', timestamp: '2026-05-13T10:00:10.000Z' }],
150+
metadata: {
151+
workspaceRoot: '/workspace/project',
152+
endTime: '2026-05-13T10:08:00.000Z',
153+
durationSeconds: 480,
154+
},
155+
});
156+
157+
expect(uploadSessionSpy).toHaveBeenCalledWith(expect.objectContaining({
158+
sessionId: 'session-1',
159+
metadata: expect.objectContaining({
160+
endTime: '2026-05-13T10:08:00.000Z',
161+
durationSeconds: 480,
162+
}),
163+
}));
136164
});
137165
});

0 commit comments

Comments
 (0)