Skip to content

Commit 1b1c52d

Browse files
fix: address PR review feedback and improve --all end-all-agents path
- Remove all explicit Lifecycle.emitTelemetry() calls (CLI framework handles it) - Change PreviewEndPartialFailure exit code from 4 to 6 - Use sessionType as discriminator for agent type (published vs local) - Re-set sessionId after ProductionAgent.endSession() clears it before removeCache - Clean local cache on server error for --all paths to prevent stale buildup - Show per-agent breakdown with local/published label in confirmation prompt - Use displayName in prompt breakdown, falling back to agentId Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6b5d6b7 commit 1b1c52d

4 files changed

Lines changed: 61 additions & 103 deletions

File tree

messages/agent.preview.end.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ You must have previously started a programmatic agent preview session with the "
88

99
The original "agent preview start" command outputs a session ID which you then use with the --session-id flag of this command to end the session. You don't have to specify the --session-id flag if an agent has only one active preview session. You must also use either the --authoring-bundle or --api-name flag to specify the API name of the authoring bundle or the published agent, respectively. To find either API name, navigate to your package directory in your DX project. The API name of an authoring bundle is the same as its directory name under the "aiAuthoringBundles" metadata directory. Similarly, the published agent's API name is the same as its directory name under the "Bots" metadata directory.
1010

11-
Use the --all flag together with either --api-name or --authoring-bundle to end all active preview sessions for a specific agent at once. Alternatively, use --all with only --target-org to end all active preview sessions for every agent found in the local session cache.
11+
Use the --all flag to end all active preview sessions at once. You can combine --all with --api-name or --authoring-bundle to end only sessions for a specific agent, or use --all on its own to end every session across all agents in the project.
1212

1313
# flags.session-id.summary
1414

@@ -62,17 +62,13 @@ No active preview sessions found.
6262

6363
Ended %s preview session(s).
6464

65-
# output.skippingSessionMissingDisplayName
66-
67-
Skipping session '%s' for agent '%s': no agent name found in cache.
68-
6965
# prompt.confirmAll
7066

7167
About to end %s preview session(s) for agent '%s'. Continue?
7268

7369
# prompt.confirmAllAgents
7470

75-
About to end %s preview session(s) across all agents. Continue?
71+
About to end %s preview session(s) across %s agent(s). Continue?
7672

7773
# examples
7874

src/commands/agent/preview/end.ts

Lines changed: 40 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { Flags, SfCommand, toHelpSection, prompts } from '@salesforce/sf-plugins-core';
18-
import { Messages, SfError, Lifecycle, EnvironmentVariable } from '@salesforce/core';
18+
import { Messages, SfError, EnvironmentVariable } from '@salesforce/core';
1919
import { Agent, ProductionAgent, ScriptAgent } from '@salesforce/agents';
2020
import type { Connection } from '@salesforce/core';
2121
import type { Interfaces } from '@oclif/core';
@@ -64,7 +64,7 @@ export default class AgentPreviewEnd extends SfCommand<AgentPreviewEndResult> {
6464
'Neither --api-name nor --authoring-bundle was provided (required when --all is not set).',
6565
'NotFound (2)': 'Agent not found, or no preview session exists for this agent.',
6666
'PreviewEndFailed (4)': 'Failed to end the preview session.',
67-
'PreviewEndPartialFailure (4)': 'With --all, one or more sessions failed to end while others succeeded.',
67+
'PreviewEndPartialFailure (6)': 'With --all, one or more sessions failed to end while others succeeded.',
6868
'SessionAmbiguous (5)': 'Multiple preview sessions found; specify --session-id to choose one.',
6969
});
7070

@@ -123,11 +123,9 @@ export default class AgentPreviewEnd extends SfCommand<AgentPreviewEndResult> {
123123
if (sessionId === undefined) {
124124
const cached = await getCachedSessionIds(this.project!, agent);
125125
if (cached.length === 0) {
126-
await Lifecycle.getInstance().emitTelemetry({ eventName: 'agent_preview_end_no_session' });
127126
throw new SfError(messages.getMessage('error.noSession'), 'PreviewSessionNotFound', [], 2);
128127
}
129128
if (cached.length > 1) {
130-
await Lifecycle.getInstance().emitTelemetry({ eventName: 'agent_preview_end_multiple_sessions' });
131129
throw new SfError(
132130
messages.getMessage('error.multipleSessions', [cached.join(', ')]),
133131
'PreviewSessionAmbiguous',
@@ -144,7 +142,6 @@ export default class AgentPreviewEnd extends SfCommand<AgentPreviewEndResult> {
144142
await validatePreviewSession(agent);
145143
} catch (error) {
146144
const wrapped = SfError.wrap(error);
147-
await Lifecycle.getInstance().emitTelemetry({ eventName: 'agent_preview_end_session_invalid' });
148145
throw new SfError(
149146
messages.getMessage('error.sessionInvalid', [sessionId]),
150147
'PreviewSessionInvalid',
@@ -161,7 +158,6 @@ export default class AgentPreviewEnd extends SfCommand<AgentPreviewEndResult> {
161158
await callPreviewEnd(agent);
162159
} catch (error) {
163160
const wrapped = SfError.wrap(error);
164-
await Lifecycle.getInstance().emitTelemetry({ eventName: 'agent_preview_end_failed' });
165161
throw new SfError(
166162
messages.getMessage('error.endFailed', [wrapped.message]),
167163
'PreviewEndFailed',
@@ -171,7 +167,6 @@ export default class AgentPreviewEnd extends SfCommand<AgentPreviewEndResult> {
171167
);
172168
}
173169

174-
await Lifecycle.getInstance().emitTelemetry({ eventName: 'agent_preview_end_success' });
175170
const result: EndedSession = { sessionId, tracesPath };
176171
this.log(messages.getMessage('output.tracesPath', [tracesPath]));
177172
return result;
@@ -195,7 +190,6 @@ export default class AgentPreviewEnd extends SfCommand<AgentPreviewEndResult> {
195190
: await Agent.init({ connection: conn as Connection, project: this.project!, apiNameOrId: flags['api-name']! });
196191
} catch (error) {
197192
const wrapped = SfError.wrap(error);
198-
await Lifecycle.getInstance().emitTelemetry({ eventName: 'agent_preview_end_agent_not_found' });
199193
throw new SfError(messages.getMessage('error.agentNotFound', [agentIdentifier]), 'AgentNotFound', [], 2, wrapped);
200194
}
201195
}
@@ -241,30 +235,39 @@ export default class AgentPreviewEnd extends SfCommand<AgentPreviewEndResult> {
241235
}
242236
}
243237

244-
const { ended, failed } = await endSessionsForAgent(agent, sessionsToEnd);
238+
const { ended, failed } = await endSessionsForAgent(agent, sessionsToEnd, true);
245239
return this.finishEndAll(ended, failed);
246240
}
247241

248242
/**
249243
* Path 2: --all alone (no agent identifier).
250244
* Reads all agents from the local cache via listCachedSessions and ends every session.
251-
* Missing displayName: warns and skips the entry (no sessionId context for failed list).
252-
* Missing sessionType: defaults to ScriptAgent (safest — local-only cleanup).
245+
* sessionType 'published' → ProductionAgent (server-side DELETE). 'simulated'/'live' → ScriptAgent (local only).
246+
* session-meta.json is always present for entries returned by listCachedSessions, so sessionType is always defined.
253247
*/
254248
private async endAllAgents(conn: Connection, noPrompt: boolean | undefined): Promise<{ ended: EndedSession[] }> {
255249
const entries: CachedPreviewSessionEntry[] = await listCachedSessions(this.project!);
256250

257-
// Only count sessions for entries that have a displayName; entries without one will be skipped.
258-
const totalSessions = entries.reduce((sum, e) => (e.displayName ? sum + e.sessions.length : sum), 0);
251+
const totalSessions = entries.reduce((sum, e) => sum + e.sessions.length, 0);
259252

260253
if (totalSessions === 0) {
261254
this.log(messages.getMessage('output.noSessionsFound'));
262255
return { ended: [] };
263256
}
264257

265258
if (!noPrompt) {
259+
const agentBreakdown = entries
260+
.map((e) => {
261+
const label = e.displayName ?? e.agentId;
262+
const type = e.sessions[0]?.sessionType === 'published' ? 'published' : 'local';
263+
return ` - ${label} (${type}): ${e.sessions.length} session(s)`;
264+
})
265+
.join('\n');
266266
const confirmed = await prompts.confirm({
267-
message: messages.getMessage('prompt.confirmAllAgents', [totalSessions]),
267+
message: `${messages.getMessage('prompt.confirmAllAgents', [
268+
totalSessions,
269+
entries.length,
270+
])}\n${agentBreakdown}`,
268271
});
269272
if (!confirmed) {
270273
return { ended: [] };
@@ -275,29 +278,17 @@ export default class AgentPreviewEnd extends SfCommand<AgentPreviewEndResult> {
275278
const failed: Array<{ task: SessionTask; error: string }> = [];
276279

277280
for (const entry of entries) {
278-
const { agentId, displayName, sessions } = entry;
279-
280-
if (!displayName) {
281-
// Cannot init an agent without a name. Log a warning per session and skip — we have no
282-
// sessionId context to surface in the failed list, so we skip rather than fail.
283-
for (const s of sessions) {
284-
this.warn(messages.getMessage('output.skippingSessionMissingDisplayName', [s.sessionId, agentId]));
285-
}
286-
continue;
287-
}
281+
const { agentId, sessions } = entry;
288282

289283
let agent: ScriptAgent | ProductionAgent;
290284
try {
291-
// Determine agent type from any session's sessionType (all sessions for an agent share the same type).
292-
// Default to ScriptAgent when sessionType is missing — local-only cleanup is safest.
293-
const sessionType = sessions[0]?.sessionType ?? 'simulated';
294-
const isProduction = sessionType === 'published';
285+
const isProduction = sessions[0]?.sessionType === 'published';
295286
if (isProduction) {
296287
// eslint-disable-next-line no-await-in-loop
297-
agent = await Agent.init({ connection: conn, project: this.project!, apiNameOrId: displayName });
288+
agent = await Agent.init({ connection: conn, project: this.project!, apiNameOrId: agentId });
298289
} else {
299290
// eslint-disable-next-line no-await-in-loop
300-
agent = await Agent.init({ connection: conn, project: this.project!, aabName: displayName });
291+
agent = await Agent.init({ connection: conn, project: this.project!, aabName: agentId });
301292
}
302293
} catch (error) {
303294
// If we can't init the agent, mark all its sessions as failed.
@@ -311,7 +302,8 @@ export default class AgentPreviewEnd extends SfCommand<AgentPreviewEndResult> {
311302
// eslint-disable-next-line no-await-in-loop
312303
const { ended: agentEnded, failed: agentFailed } = await endSessionsForAgent(
313304
agent,
314-
sessions.map((s) => ({ sessionId: s.sessionId }))
305+
sessions.map((s) => ({ sessionId: s.sessionId })),
306+
true
315307
);
316308
ended.push(...agentEnded);
317309
failed.push(...agentFailed);
@@ -324,28 +316,19 @@ export default class AgentPreviewEnd extends SfCommand<AgentPreviewEndResult> {
324316
* Called by endAllForAgent when we have a single agent and already aggregated ended/failed.
325317
* Throws a partial-failure error if needed; logs success otherwise.
326318
*/
327-
private async finishEndAll(
319+
private finishEndAll(
328320
ended: EndedSession[],
329321
failed: Array<{ task: SessionTask; error: string }>
330-
): Promise<{ ended: EndedSession[] }> {
322+
): { ended: EndedSession[] } {
331323
if (failed.length > 0) {
332324
const failedList = failed.map((f) => `${f.task.sessionId}: ${f.error}`).join(', ');
333325
const endedIds = ended.map((e) => e.sessionId).join(', ');
334326
const msg = `Failed to end ${failed.length} session(s): [${failedList}]. Successfully ended ${
335327
ended.length
336328
} session(s)${ended.length > 0 ? `: [${endedIds}]` : ''}.`;
337-
await Lifecycle.getInstance().emitTelemetry({
338-
eventName: 'agent_preview_end_all_partial_failure',
339-
failedCount: failed.length,
340-
succeededCount: ended.length,
341-
});
342-
throw new SfError(msg, 'PreviewEndPartialFailure', [], 4);
329+
throw new SfError(msg, 'PreviewEndPartialFailure', [], 6);
343330
}
344331

345-
await Lifecycle.getInstance().emitTelemetry({
346-
eventName: 'agent_preview_end_all_success',
347-
sessionCount: ended.length,
348-
});
349332
this.log(messages.getMessage('output.endedAll', [ended.length]));
350333
return { ended };
351334
}
@@ -357,10 +340,13 @@ type CommandFlags = Interfaces.InferredFlags<typeof AgentPreviewEnd.flags>;
357340
* Ends a list of sessions on the given agent object serially.
358341
* Returns { ended, failed } so callers can aggregate results.
359342
* Does NOT throw on partial failure — callers decide what to do.
343+
* When cleanOnServerError is true, a server-side failure still removes the local cache entry
344+
* so stale sessions don't accumulate (used by --all paths).
360345
*/
361346
async function endSessionsForAgent(
362347
agent: ScriptAgent | ProductionAgent,
363-
sessionsToEnd: SessionTask[]
348+
sessionsToEnd: SessionTask[],
349+
cleanOnServerError = false
364350
): Promise<{ ended: EndedSession[]; failed: Array<{ task: SessionTask; error: string }> }> {
365351
const ended: EndedSession[] = [];
366352
const failed: Array<{ task: SessionTask; error: string }> = [];
@@ -376,10 +362,20 @@ async function endSessionsForAgent(
376362
// ScriptAgent flushes traces to disk; ProductionAgent issues the server-side end request.
377363
// eslint-disable-next-line no-await-in-loop
378364
await callPreviewEnd(agent);
365+
// ProductionAgent.endSession() clears this.sessionId after the server call; re-set it so
366+
// removeCache can call getHistoryDir() without throwing "No sessionId set on agent".
367+
agent.setSessionId(sessionId);
379368
// eslint-disable-next-line no-await-in-loop
380369
await removeCache(agent);
381370
ended.push({ sessionId, tracesPath });
382371
} catch (error) {
372+
if (cleanOnServerError) {
373+
// Server doesn't know about this session (e.g. already expired). Remove local cache so
374+
// it doesn't show up in future listings.
375+
agent.setSessionId(sessionId);
376+
// eslint-disable-next-line no-await-in-loop
377+
await removeCache(agent).catch(() => {});
378+
}
383379
failed.push({ task, error: SfError.wrap(error).message });
384380
}
385381
}

test/commands/agent/preview/end.test.ts

Lines changed: 18 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,8 @@ describe('agent preview end', () => {
283283
expect(err.message).to.include('network timeout');
284284
// Also mentions the ones that succeeded
285285
expect(err.message).to.include('Successfully ended 2 session(s)');
286-
// Only 2 successful removes happened (session-1 and session-3)
287-
expect(removeCacheStub.callCount).to.equal(2);
286+
// 3 removes: session-1 (success), session-2 (catch cleanup), session-3 (success)
287+
expect(removeCacheStub.callCount).to.equal(3);
288288
expect(err.name).to.equal('PreviewEndPartialFailure');
289289
}
290290
});
@@ -308,7 +308,8 @@ describe('agent preview end', () => {
308308
expect(err.message).to.include('session-2');
309309
expect(err.message).to.include('stale session');
310310
expect(err.message).to.include('Successfully ended 1 session(s)');
311-
expect(removeCacheStub.callCount).to.equal(1);
311+
// 2 removes: session-1 (success) + session-2 (catch cleanup)
312+
expect(removeCacheStub.callCount).to.equal(2);
312313
expect(agentPreviewEndStub.callCount).to.equal(1);
313314
expect(err.name).to.equal('PreviewEndPartialFailure');
314315
}
@@ -363,16 +364,14 @@ describe('agent preview end', () => {
363364
it('ends sessions for all agents from listCachedSessions when only --target-org is provided', async () => {
364365
listCachedSessionsStub.resolves([
365366
{
366-
agentId: 'agent-1-id',
367-
displayName: 'My_Script_Agent',
367+
agentId: 'My_Script_Agent',
368368
sessions: [
369369
{ sessionId: 'sess-1', sessionType: 'simulated' },
370370
{ sessionId: 'sess-2', sessionType: 'live' },
371371
],
372372
},
373373
{
374-
agentId: 'agent-2-id',
375-
displayName: 'My_Published_Agent',
374+
agentId: '0Xxg8000000NBNlCAO',
376375
sessions: [{ sessionId: 'sess-3', sessionType: 'published' }],
377376
},
378377
]);
@@ -404,70 +403,39 @@ describe('agent preview end', () => {
404403
}
405404
});
406405

407-
it('skips agents with missing displayName and logs a warning', async () => {
406+
it('uses aabName (ScriptAgent) for live sessionType', async () => {
408407
listCachedSessionsStub.resolves([
409408
{
410-
agentId: 'agent-no-name',
411-
displayName: undefined,
412-
sessions: [{ sessionId: 'orphan-sess-1', sessionType: 'simulated' }],
413-
},
414-
{
415-
agentId: 'agent-with-name',
416-
displayName: 'My_Script_Agent',
417-
sessions: [{ sessionId: 'known-sess-1', sessionType: 'simulated' }],
409+
agentId: 'Local_Info_Agent',
410+
sessions: [{ sessionId: 'aab-sess-1', sessionType: 'live' }],
418411
},
419412
]);
420413

421-
const warnStub = $$.SANDBOX.stub(AgentPreviewEnd.prototype, 'warn');
422-
423414
const result = await AgentPreviewEnd.run(['--all', '--target-org', 'test@org.com', '--no-prompt']);
424415

425-
// Only the named agent's session should have been ended
426416
expect((result as { ended: unknown[] }).ended).to.have.length(1);
427-
expect(initStub.callCount).to.equal(1);
428-
expect(removeCacheStub.callCount).to.equal(1);
429-
// A warning should have been emitted for the session with the missing displayName
430-
expect(warnStub.calledOnce).to.be.true;
417+
expect(initStub.calledOnce).to.be.true;
418+
expect(initStub.firstCall.args[0]).to.have.property('aabName', 'Local_Info_Agent');
419+
expect(removeCacheStub.calledOnce).to.be.true;
431420
});
432421

433-
it('uses aabName init path for simulated/live sessions and apiNameOrId for published', async () => {
422+
it('uses aabName for simulated/live sessions and apiNameOrId for published', async () => {
434423
listCachedSessionsStub.resolves([
435424
{
436-
agentId: 'script-agent-id',
437-
displayName: 'My_Script_Agent',
425+
agentId: 'My_Script_Agent',
438426
sessions: [{ sessionId: 'sess-sim', sessionType: 'simulated' }],
439427
},
440428
{
441-
agentId: 'prod-agent-id',
442-
displayName: 'My_Published_Agent',
429+
agentId: 'Weather_Agent',
443430
sessions: [{ sessionId: 'sess-pub', sessionType: 'published' }],
444431
},
445432
]);
446433

447434
await AgentPreviewEnd.run(['--all', '--target-org', 'test@org.com', '--no-prompt']);
448435

449436
expect(initStub.callCount).to.equal(2);
450-
// First call should use aabName for simulated agent
451437
expect(initStub.firstCall.args[0]).to.have.property('aabName', 'My_Script_Agent');
452-
// Second call should use apiNameOrId for published agent
453-
expect(initStub.secondCall.args[0]).to.have.property('apiNameOrId', 'My_Published_Agent');
454-
});
455-
456-
it('defaults to aabName (ScriptAgent) when sessionType is undefined', async () => {
457-
listCachedSessionsStub.resolves([
458-
{
459-
agentId: 'agent-no-type',
460-
displayName: 'My_Unknown_Agent',
461-
sessions: [{ sessionId: 'sess-no-type', sessionType: undefined }],
462-
},
463-
]);
464-
465-
await AgentPreviewEnd.run(['--all', '--target-org', 'test@org.com', '--no-prompt']);
466-
467-
expect(initStub.calledOnce).to.be.true;
468-
// sessionType undefined → defaults to 'simulated' → should use aabName path
469-
expect(initStub.firstCall.args[0]).to.have.property('aabName', 'My_Unknown_Agent');
470-
expect(initStub.firstCall.args[0]).to.not.have.property('apiNameOrId');
438+
expect(initStub.secondCall.args[0]).to.have.property('apiNameOrId', 'Weather_Agent');
471439
});
472440

473441
it('throws PreviewEndPartialFailure when one agent succeeds and another throws (no agent identifier)', async () => {
@@ -477,13 +445,11 @@ describe('agent preview end', () => {
477445

478446
listCachedSessionsStub.resolves([
479447
{
480-
agentId: 'agent-a-id',
481-
displayName: 'Agent_A',
448+
agentId: 'Agent_A',
482449
sessions: [{ sessionId: 'sess-a-1', sessionType: 'simulated' }],
483450
},
484451
{
485-
agentId: 'agent-b-id',
486-
displayName: 'Agent_B',
452+
agentId: 'Agent_B',
487453
sessions: [{ sessionId: 'sess-b-1', sessionType: 'simulated' }],
488454
},
489455
]);

test/nuts/z3.agent.preview.nut.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ describe('agent preview', function () {
154154
`agent preview end --session-id ${sessionId} --api-name ${
155155
publishedAgent!.DeveloperName
156156
} --target-org ${targetOrg} --json`
157-
).jsonOutput?.result as import('../../src/commands/agent/preview/end.js').EndedSession | undefined;
157+
).jsonOutput?.result as { sessionId?: string; tracesPath?: string } | undefined;
158158
expect(endResult?.sessionId).to.equal(sessionId);
159159
expect(endResult?.tracesPath).to.be.a('string');
160160
});

0 commit comments

Comments
 (0)