pikiclaw 0.2.55 → 0.2.57

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.
@@ -1,5 +1,6 @@
1
1
  import { normalizeAgent } from './bot.js';
2
- import { getAgentsListData, getModelsListData, getSessionsPageData, getSkillsListData, modelMatchesSelection, resolveSkillPrompt, } from './bot-commands.js';
2
+ import { getSessionStatusForChat } from './session-status.js';
3
+ import { getAgentsListData, getModelsListData, getSessionsPageData, getSkillsListData, summarizeSessionRun, modelMatchesSelection, resolveSkillPrompt, } from './bot-commands.js';
3
4
  function chunkRows(items, columns) {
4
5
  const rows = [];
5
6
  const size = Math.max(1, columns);
@@ -219,13 +220,14 @@ export async function executeCommandAction(bot, chatId, action, opts = {}) {
219
220
  return { kind: 'noop', message: 'Session not found' };
220
221
  const runtime = bot.adoptExistingSessionForChat(chatId, session);
221
222
  const displayId = session.sessionId || action.sessionId;
223
+ const sessionStatus = getSessionStatusForChat(bot, chat, session);
222
224
  return {
223
225
  kind: 'notice',
224
226
  callbackText: `Switched: ${displayId.slice(0, 12)}`,
225
227
  notice: {
226
228
  title: 'Session Switched',
227
229
  value: displayId,
228
- detail: 'Switched successfully',
230
+ detail: summarizeSessionRun({ ...session, running: sessionStatus.isRunning }).noticeDetail,
229
231
  valueMode: 'code',
230
232
  },
231
233
  session: runtime,
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import path from 'node:path';
12
12
  import { fmtTokens, fmtUptime, fmtBytes } from './bot.js';
13
- import { getProjectSkillPaths } from './code-agent.js';
13
+ import { getProjectSkillPaths, normalizeClaudeModelId } from './code-agent.js';
14
14
  import { getDriver } from './agent-driver.js';
15
15
  import { buildWelcomeIntro, buildSkillCommandName, indexSkillsByCommand, SKILL_CMD_PREFIX } from './bot-menu.js';
16
16
  import { buildBotMenuState } from './bot-orchestration.js';
@@ -37,6 +37,28 @@ export function getStartData(bot, chatId) {
37
37
  commands,
38
38
  };
39
39
  }
40
+ export function summarizeSessionRun(session) {
41
+ if (session.running || session.runState === 'running') {
42
+ return {
43
+ state: 'running',
44
+ shortLabel: 'running',
45
+ noticeDetail: 'Status: running',
46
+ };
47
+ }
48
+ if (session.runState === 'incomplete') {
49
+ const detail = String(session.runDetail || '').trim();
50
+ return {
51
+ state: 'incomplete',
52
+ shortLabel: 'unfinished',
53
+ noticeDetail: detail ? `Status: unfinished · ${detail}` : 'Status: unfinished',
54
+ };
55
+ }
56
+ return {
57
+ state: 'completed',
58
+ shortLabel: 'done',
59
+ noticeDetail: 'Status: completed',
60
+ };
61
+ }
40
62
  export async function getSessionsPageData(bot, chatId, page, pageSize = 5) {
41
63
  const cs = bot.chat(chatId);
42
64
  const res = await bot.fetchSessions(cs.agent);
@@ -51,11 +73,24 @@ export async function getSessionsPageData(bot, chatId, page, pageSize = 5) {
51
73
  if (!sessionKey)
52
74
  continue;
53
75
  const status = getSessionStatusForChat(bot, cs, s);
76
+ const runSummary = summarizeSessionRun({
77
+ running: status.isRunning,
78
+ runState: status.isRunning ? 'running' : s.runState,
79
+ runDetail: s.runDetail,
80
+ });
54
81
  const title = s.title ? s.title.replace(/\n/g, ' ').slice(0, 10) : sessionKey.slice(0, 10);
55
82
  const time = s.createdAt
56
83
  ? new Date(s.createdAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
57
84
  : '?';
58
- entries.push({ key: sessionKey, title, time, isCurrent: status.isCurrent, isRunning: status.isRunning });
85
+ entries.push({
86
+ key: sessionKey,
87
+ title,
88
+ time: `${time} · ${runSummary.shortLabel}`,
89
+ isCurrent: status.isCurrent,
90
+ isRunning: status.isRunning,
91
+ runState: runSummary.state,
92
+ runDetail: s.runDetail,
93
+ });
59
94
  }
60
95
  return { agent: cs.agent, total, page: pg, totalPages, sessions: entries };
61
96
  }
@@ -121,15 +156,13 @@ export function getSkillsListData(bot, chatId) {
121
156
  return { agent: cs.agent, workdir: bot.workdir, skills };
122
157
  }
123
158
  function claudeModelSelectionKey(modelId) {
124
- const value = String(modelId || '').trim().toLowerCase();
159
+ const value = normalizeClaudeModelId(modelId).toLowerCase();
125
160
  if (!value)
126
161
  return null;
127
- if (value === 'opus' || value === 'opus-1m' || value.startsWith('claude-opus-')) {
128
- return value === 'opus-1m' || value.endsWith('[1m]') ? 'opus-1m' : 'opus';
129
- }
130
- if (value === 'sonnet' || value === 'sonnet-1m' || value.startsWith('claude-sonnet-')) {
131
- return value === 'sonnet-1m' || value.endsWith('[1m]') ? 'sonnet-1m' : 'sonnet';
132
- }
162
+ if (value === 'opus' || value.startsWith('claude-opus-'))
163
+ return 'opus';
164
+ if (value === 'sonnet' || value.startsWith('claude-sonnet-'))
165
+ return 'sonnet';
133
166
  if (value === 'haiku' || value.startsWith('claude-haiku-'))
134
167
  return 'haiku';
135
168
  return null;
package/dist/bot.js CHANGED
@@ -8,7 +8,7 @@ import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { execSync, spawn } from 'node:child_process';
10
10
  import { getActiveUserConfig, onUserConfigChange, resolveUserWorkdir, setUserWorkdir } from './user-config.js';
11
- import { doStream, getSessions, getSessionTail, getUsage, initializeProjectSkills, listAgents, listModels, listSkills, stageSessionFiles, isPendingSessionId, } from './code-agent.js';
11
+ import { doStream, getSessions, getSessionTail, getUsage, initializeProjectSkills, listAgents, listModels, listSkills, stageSessionFiles, isPendingSessionId, normalizeClaudeModelId, } from './code-agent.js';
12
12
  import { getDriver, hasDriver, allDriverIds } from './agent-driver.js';
13
13
  import { terminateProcessTree } from './process-control.js';
14
14
  import { VERSION } from './version.js';
@@ -221,7 +221,7 @@ function buildMcpDeliveryPrompt() {
221
221
  }
222
222
  function configModelValue(config, agent) {
223
223
  switch (agent) {
224
- case 'claude': return String(config.claudeModel || process.env.CLAUDE_MODEL || 'claude-opus-4-6').trim();
224
+ case 'claude': return normalizeClaudeModelId(config.claudeModel || process.env.CLAUDE_MODEL || 'claude-opus-4-6');
225
225
  case 'codex': return String(config.codexModel || process.env.CODEX_MODEL || 'gpt-5.4').trim();
226
226
  case 'gemini': return String(config.geminiModel || process.env.GEMINI_MODEL || 'gemini-3.1-pro-preview').trim();
227
227
  }
@@ -315,6 +315,9 @@ export function modelFamily(model) {
315
315
  return 'sonnet';
316
316
  return null;
317
317
  }
318
+ export function normalizeClaudeModelId(model) {
319
+ return typeof model === 'string' ? model.trim() : '';
320
+ }
318
321
  export function emptyUsage(agent, error) {
319
322
  return { ok: false, agent, source: null, capturedAt: null, status: null, windows: [], error };
320
323
  }
@@ -464,6 +467,54 @@ function nextPendingSessionId() { return `pending_${crypto.randomBytes(6).toStri
464
467
  export function isPendingSessionId(sessionId) {
465
468
  return typeof sessionId === 'string' && sessionId.startsWith('pending_');
466
469
  }
470
+ function normalizeSessionRunState(rawState) {
471
+ const state = typeof rawState === 'string' ? rawState.trim().toLowerCase() : '';
472
+ if (state === 'completed' || state === 'incomplete')
473
+ return state;
474
+ if (state === 'running')
475
+ return 'incomplete';
476
+ return 'completed';
477
+ }
478
+ function normalizeSessionRunDetail(rawState, rawDetail) {
479
+ const detail = typeof rawDetail === 'string' ? rawDetail.trim() : '';
480
+ if (detail)
481
+ return shortValue(detail, 180);
482
+ const state = typeof rawState === 'string' ? rawState.trim().toLowerCase() : '';
483
+ if (state === 'running')
484
+ return 'Last run stopped before completion.';
485
+ return null;
486
+ }
487
+ function normalizeSessionRunUpdatedAt(rawUpdatedAt, fallback) {
488
+ return typeof rawUpdatedAt === 'string' && rawUpdatedAt.trim() ? rawUpdatedAt : fallback;
489
+ }
490
+ function setSessionRunState(record, runState, runDetail, runUpdatedAt) {
491
+ record.runState = runState;
492
+ record.runDetail = runDetail ? shortValue(runDetail, 180) : null;
493
+ record.runUpdatedAt = runUpdatedAt || new Date().toISOString();
494
+ }
495
+ function incompleteRunDetail(result) {
496
+ if (result.stopReason === 'interrupted')
497
+ return 'Interrupted by user.';
498
+ if (result.stopReason === 'timeout')
499
+ return 'Timed out before completion.';
500
+ if (result.stopReason === 'max_tokens')
501
+ return 'Stopped before completion: max tokens reached.';
502
+ const error = normalizeErrorMessage(result.error);
503
+ if (error)
504
+ return shortValue(error, 180);
505
+ const stopReason = normalizeErrorMessage(result.stopReason);
506
+ if (stopReason)
507
+ return `Stopped before completion: ${shortValue(stopReason, 120)}`;
508
+ const message = firstNonEmptyLine(result.message || '');
509
+ return message ? shortValue(message, 180) : 'Last run did not complete.';
510
+ }
511
+ function applySessionRunResult(record, result) {
512
+ if (result.ok && !result.incomplete) {
513
+ setSessionRunState(record, 'completed', null);
514
+ return;
515
+ }
516
+ setSessionRunState(record, 'incomplete', incompleteRunDetail(result));
517
+ }
467
518
  function normalizeSessionRecord(raw, workdir) {
468
519
  // Support both new format (sessionId) and legacy format (localSessionId + engineSessionId)
469
520
  const sessionId = typeof raw?.sessionId === 'string' ? raw.sessionId.trim()
@@ -484,6 +535,9 @@ function normalizeSessionRecord(raw, workdir) {
484
535
  title: typeof raw?.title === 'string' && raw.title.trim() ? raw.title.trim() : null,
485
536
  model: typeof raw?.model === 'string' && raw.model.trim() ? raw.model.trim() : null,
486
537
  stagedFiles: Array.isArray(raw?.stagedFiles) ? dedupeStrings(raw.stagedFiles.filter((v) => typeof v === 'string')) : [],
538
+ runState: normalizeSessionRunState(raw?.runState),
539
+ runDetail: normalizeSessionRunDetail(raw?.runState, raw?.runDetail),
540
+ runUpdatedAt: normalizeSessionRunUpdatedAt(raw?.runUpdatedAt, typeof raw?.updatedAt === 'string' && raw.updatedAt.trim() ? raw.updatedAt : new Date().toISOString()),
487
541
  };
488
542
  }
489
543
  function loadSessionIndex(workdir) {
@@ -502,6 +556,7 @@ function writeSessionMeta(record) {
502
556
  workspacePath: record.workspacePath,
503
557
  createdAt: record.createdAt, updatedAt: record.updatedAt,
504
558
  title: record.title, model: record.model, stagedFiles: record.stagedFiles,
559
+ runState: record.runState, runDetail: record.runDetail, runUpdatedAt: record.runUpdatedAt,
505
560
  });
506
561
  }
507
562
  function copyPath(sourcePath, targetPath) {
@@ -583,8 +638,25 @@ export function promoteSessionId(workdir, agent, pendingId, nativeId) {
583
638
  record.workspacePath = sessionWorkspacePath(resolvedWorkdir, agent, nativeId);
584
639
  saveSessionRecord(resolvedWorkdir, record);
585
640
  }
641
+ function syncManagedSessionIdentity(session, workdir, nativeId) {
642
+ const resolvedId = nativeId.trim();
643
+ if (!resolvedId || session.sessionId === resolvedId)
644
+ return false;
645
+ const resolvedWorkdir = path.resolve(workdir);
646
+ const previousId = session.sessionId;
647
+ if (isPendingSessionId(previousId)) {
648
+ promoteSessionId(resolvedWorkdir, session.record.agent, previousId, resolvedId);
649
+ }
650
+ session.sessionId = resolvedId;
651
+ session.workspacePath = sessionWorkspacePath(resolvedWorkdir, session.record.agent, resolvedId);
652
+ session.record.sessionId = resolvedId;
653
+ session.record.workspacePath = session.workspacePath;
654
+ return true;
655
+ }
586
656
  function summarizePromptTitle(prompt) {
587
- const text = String(prompt || '').replace(/\s+/g, ' ').trim();
657
+ const raw = String(prompt || '').replace(/\r\n?/g, '\n');
658
+ const text = firstNonEmptyLine(raw).replace(/\s+/g, ' ').trim()
659
+ || raw.replace(/\s+/g, ' ').trim();
588
660
  if (!text)
589
661
  return null;
590
662
  return text.length <= 120 ? text : `${text.slice(0, 117).trimEnd()}...`;
@@ -635,6 +707,7 @@ function ensureSessionWorkspace(opts) {
635
707
  workspacePath: sessionWorkspacePath(workdir, opts.agent, sessionId),
636
708
  createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
637
709
  title: summarizePromptTitle(opts.title) || null, model: null, stagedFiles: [],
710
+ runState: 'completed', runDetail: null, runUpdatedAt: new Date().toISOString(),
638
711
  };
639
712
  }
640
713
  if (!record.title && opts.title)
@@ -799,6 +872,7 @@ function prepareStreamOpts(opts) {
799
872
  session.record.stagedFiles = [];
800
873
  if (!session.record.title)
801
874
  session.record.title = summarizePromptTitle(opts.prompt) || importedFiles[0] || null;
875
+ setSessionRunState(session.record, 'running', null);
802
876
  saveSessionRecord(opts.workdir, session.record);
803
877
  const attachmentPaths = attachmentRelPaths.map(relPath => path.join(session.workspacePath, relPath));
804
878
  // For pending sessions, pass null sessionId to the CLI so it creates a new session
@@ -811,20 +885,21 @@ function prepareStreamOpts(opts) {
811
885
  ...opts,
812
886
  sessionId: effectiveSessionId,
813
887
  attachments: attachmentPaths.length ? attachmentPaths : undefined,
888
+ onSessionId: (nativeSessionId) => {
889
+ if (!syncManagedSessionIdentity(session, opts.workdir, nativeSessionId))
890
+ return;
891
+ saveSessionRecord(opts.workdir, session.record);
892
+ },
814
893
  },
815
894
  };
816
895
  }
817
896
  function finalizeStreamResult(result, workdir, prompt, session) {
818
- // If the agent returned a native session ID and our session was pending, promote it
819
- const pendingId = session.sessionId;
820
- if (result.sessionId && isPendingSessionId(pendingId)) {
821
- promoteSessionId(workdir, session.record.agent, pendingId, result.sessionId);
822
- session.sessionId = result.sessionId;
823
- }
824
- session.record.sessionId = result.sessionId || session.record.sessionId;
897
+ if (result.sessionId)
898
+ syncManagedSessionIdentity(session, workdir, result.sessionId);
825
899
  session.record.model = result.model || session.record.model;
826
900
  if (!session.record.title)
827
901
  session.record.title = summarizePromptTitle(prompt);
902
+ applySessionRunResult(session.record, result);
828
903
  saveSessionRecord(workdir, session.record);
829
904
  return { ...result, sessionId: session.sessionId, workspacePath: session.workspacePath };
830
905
  }
@@ -885,6 +960,33 @@ export async function doStream(opts) {
885
960
  const result = await driver.doStream(prepared);
886
961
  return finalizeStreamResult(result, opts.workdir, opts.prompt, session);
887
962
  }
963
+ catch (error) {
964
+ const failedResult = {
965
+ ok: false,
966
+ message: normalizeErrorMessage(error) || 'Agent stream failed.',
967
+ thinking: null,
968
+ sessionId: session.sessionId,
969
+ workspacePath: session.workspacePath,
970
+ model: session.record.model,
971
+ thinkingEffort: prepared.thinkingEffort,
972
+ elapsedS: 0,
973
+ inputTokens: null,
974
+ outputTokens: null,
975
+ cachedInputTokens: null,
976
+ cacheCreationInputTokens: null,
977
+ contextWindow: null,
978
+ contextUsedTokens: null,
979
+ contextPercent: null,
980
+ codexCumulative: null,
981
+ error: normalizeErrorMessage(error) || 'Agent stream failed.',
982
+ stopReason: null,
983
+ incomplete: true,
984
+ activity: null,
985
+ };
986
+ applySessionRunResult(session.record, failedResult);
987
+ saveSessionRecord(opts.workdir, session.record);
988
+ throw error;
989
+ }
888
990
  finally {
889
991
  if (bridge) {
890
992
  await bridge.stop().catch(() => { });