smart-context-mcp 1.16.2 → 1.16.4

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.
@@ -6,7 +6,7 @@ import { setTimeout as delay } from 'node:timers/promises';
6
6
  import { projectRoot } from '../utils/runtime-config.js';
7
7
 
8
8
  export const STATE_DB_FILENAME = 'state.sqlite';
9
- export const SQLITE_SCHEMA_VERSION = 5;
9
+ export const SQLITE_SCHEMA_VERSION = 6;
10
10
  export const ACTIVE_SESSION_SCOPE = 'project';
11
11
  export const STATE_DB_SOFT_MAX_BYTES = 32 * 1024 * 1024;
12
12
  const STATE_DB_BUSY_TIMEOUT_MS = 1000;
@@ -14,6 +14,7 @@ const STATE_DB_LOCK_RETRY_ATTEMPTS = 3;
14
14
  const STATE_DB_LOCK_RETRY_DELAY_MS = 75;
15
15
  export const EXPECTED_TABLES = [
16
16
  'active_session',
17
+ 'agent_runs',
17
18
  'context_access',
18
19
  'hook_turn_state',
19
20
  'meta',
@@ -21,6 +22,8 @@ export const EXPECTED_TABLES = [
21
22
  'session_events',
22
23
  'sessions',
23
24
  'summary_cache',
25
+ 'task_handoffs',
26
+ 'tasks',
24
27
  'workflow_metrics',
25
28
  ];
26
29
 
@@ -179,6 +182,69 @@ const MIGRATIONS = [
179
182
  ON workflow_metrics(session_id, created_at DESC)`,
180
183
  ],
181
184
  },
185
+ {
186
+ version: 6,
187
+ statements: [
188
+ 'ALTER TABLE sessions ADD COLUMN task_id TEXT',
189
+ 'ALTER TABLE sessions ADD COLUMN agent_id TEXT',
190
+ 'ALTER TABLE sessions ADD COLUMN branch_name TEXT',
191
+ 'ALTER TABLE sessions ADD COLUMN worktree_path TEXT',
192
+ 'ALTER TABLE session_events ADD COLUMN task_id TEXT',
193
+ 'ALTER TABLE session_events ADD COLUMN agent_id TEXT',
194
+ 'ALTER TABLE session_events ADD COLUMN event_kind TEXT',
195
+ 'ALTER TABLE summary_cache ADD COLUMN task_id TEXT',
196
+ 'ALTER TABLE hook_turn_state ADD COLUMN task_id TEXT',
197
+ 'ALTER TABLE hook_turn_state ADD COLUMN agent_id TEXT',
198
+ 'ALTER TABLE hook_turn_state ADD COLUMN parent_agent_id TEXT',
199
+ 'ALTER TABLE hook_turn_state ADD COLUMN conversation_id TEXT',
200
+ `CREATE TABLE IF NOT EXISTS tasks (
201
+ task_id TEXT PRIMARY KEY,
202
+ project_scope TEXT NOT NULL,
203
+ canonical_goal TEXT NOT NULL DEFAULT '',
204
+ normalized_goal TEXT NOT NULL DEFAULT '',
205
+ status TEXT NOT NULL DEFAULT 'planning',
206
+ branch_name TEXT,
207
+ worktree_path TEXT,
208
+ last_session_id TEXT,
209
+ created_at TEXT NOT NULL,
210
+ updated_at TEXT NOT NULL
211
+ )`,
212
+ `CREATE TABLE IF NOT EXISTS task_handoffs (
213
+ handoff_id INTEGER PRIMARY KEY AUTOINCREMENT,
214
+ task_id TEXT NOT NULL,
215
+ session_id TEXT,
216
+ from_agent_id TEXT,
217
+ to_agent_id TEXT,
218
+ trigger TEXT NOT NULL,
219
+ summary_json TEXT NOT NULL,
220
+ created_at TEXT NOT NULL
221
+ )`,
222
+ `CREATE TABLE IF NOT EXISTS agent_runs (
223
+ run_id TEXT PRIMARY KEY,
224
+ task_id TEXT,
225
+ agent_id TEXT NOT NULL,
226
+ parent_agent_id TEXT,
227
+ client TEXT NOT NULL,
228
+ conversation_id TEXT,
229
+ session_id TEXT,
230
+ role TEXT,
231
+ started_at TEXT NOT NULL,
232
+ updated_at TEXT NOT NULL
233
+ )`,
234
+ `CREATE INDEX IF NOT EXISTS idx_sessions_task_updated
235
+ ON sessions(task_id, updated_at DESC)`,
236
+ `CREATE INDEX IF NOT EXISTS idx_session_events_task_created
237
+ ON session_events(task_id, created_at DESC)`,
238
+ `CREATE INDEX IF NOT EXISTS idx_summary_cache_task_updated
239
+ ON summary_cache(task_id, updated_at DESC)`,
240
+ `CREATE INDEX IF NOT EXISTS idx_tasks_updated
241
+ ON tasks(updated_at DESC)`,
242
+ `CREATE INDEX IF NOT EXISTS idx_task_handoffs_task_created
243
+ ON task_handoffs(task_id, created_at DESC)`,
244
+ `CREATE INDEX IF NOT EXISTS idx_agent_runs_task_updated
245
+ ON agent_runs(task_id, updated_at DESC)`,
246
+ ],
247
+ },
182
248
  ];
183
249
 
184
250
  let sqliteModulePromise = null;
@@ -672,6 +738,60 @@ const parseJsonText = (value, fallback = {}) => {
672
738
  const hashLegacyPayload = (prefix, payload) =>
673
739
  `${prefix}:${createHash('sha1').update(payload).digest('hex')}`;
674
740
 
741
+ const normalizeTaskText = (value) =>
742
+ String(value ?? '')
743
+ .toLowerCase()
744
+ .replace(/\s+/g, ' ')
745
+ .trim();
746
+
747
+ const slugTaskText = (value) =>
748
+ normalizeTaskText(value)
749
+ .replace(/[^a-z0-9]+/g, '-')
750
+ .replace(/^-+|-+$/g, '')
751
+ .slice(0, 48) || 'task';
752
+
753
+ export const deriveTaskId = ({ goal, branchName = '', worktreePath = '' } = {}) => {
754
+ const canonicalGoal = String(goal ?? '').trim();
755
+ const slug = slugTaskText(canonicalGoal);
756
+ const identity = JSON.stringify({
757
+ goal: normalizeTaskText(canonicalGoal),
758
+ branchName: normalizeTaskText(branchName),
759
+ worktreePath: normalizeTaskText(worktreePath),
760
+ });
761
+ const digest = createHash('sha1').update(identity).digest('hex').slice(0, 10);
762
+ return `${slug}-${digest}`;
763
+ };
764
+
765
+ const deriveTaskFields = ({
766
+ taskId = null,
767
+ goal = '',
768
+ branchName = '',
769
+ worktreePath = projectRoot,
770
+ } = {}) => {
771
+ const canonicalGoal = typeof goal === 'string' ? goal.trim() : '';
772
+ const normalizedGoal = normalizeTaskText(canonicalGoal);
773
+ const resolvedBranchName = typeof branchName === 'string' && branchName.trim().length > 0
774
+ ? branchName.trim()
775
+ : null;
776
+ const resolvedWorktreePath = typeof worktreePath === 'string' && worktreePath.trim().length > 0
777
+ ? worktreePath.trim()
778
+ : projectRoot;
779
+
780
+ return {
781
+ taskId: typeof taskId === 'string' && taskId.trim().length > 0
782
+ ? taskId.trim()
783
+ : deriveTaskId({
784
+ goal: canonicalGoal,
785
+ branchName: resolvedBranchName ?? '',
786
+ worktreePath: resolvedWorktreePath,
787
+ }),
788
+ canonicalGoal,
789
+ normalizedGoal,
790
+ branchName: resolvedBranchName,
791
+ worktreePath: resolvedWorktreePath,
792
+ };
793
+ };
794
+
675
795
  const buildSessionRecord = (sessionId, data) => {
676
796
  const completed = normalizeStringArray(data.completed);
677
797
  const decisions = normalizeStringArray(data.decisions);
@@ -681,10 +801,20 @@ const buildSessionRecord = (sessionId, data) => {
681
801
  const blockers = normalizeStringArray(data.blockers);
682
802
  const updatedAt = toIsoString(data.updatedAt);
683
803
  const createdAt = toIsoString(data.createdAt, updatedAt);
804
+ const taskFields = deriveTaskFields({
805
+ taskId: data.taskId,
806
+ goal: data.goal,
807
+ branchName: data.branchName,
808
+ worktreePath: data.worktreePath,
809
+ });
684
810
 
685
811
  return {
686
812
  sessionId,
687
813
  goal: typeof data.goal === 'string' ? data.goal : '',
814
+ taskId: taskFields.taskId,
815
+ agentId: typeof data.agentId === 'string' && data.agentId.trim().length > 0 ? data.agentId : null,
816
+ branchName: taskFields.branchName,
817
+ worktreePath: taskFields.worktreePath,
688
818
  status: normalizeStatus(data.status),
689
819
  currentFocus: typeof data.currentFocus === 'string' ? data.currentFocus : '',
690
820
  whyBlocked: typeof data.whyBlocked === 'string' ? data.whyBlocked : '',
@@ -710,6 +840,10 @@ const upsertSession = (db, record) => {
710
840
  current_focus,
711
841
  why_blocked,
712
842
  next_step,
843
+ task_id,
844
+ agent_id,
845
+ branch_name,
846
+ worktree_path,
713
847
  pinned_context_json,
714
848
  unresolved_questions_json,
715
849
  blockers_json,
@@ -720,13 +854,17 @@ const upsertSession = (db, record) => {
720
854
  created_at,
721
855
  updated_at
722
856
  )
723
- VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
857
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
724
858
  ON CONFLICT(session_id) DO UPDATE SET
725
859
  goal = excluded.goal,
726
860
  status = excluded.status,
727
861
  current_focus = excluded.current_focus,
728
862
  why_blocked = excluded.why_blocked,
729
863
  next_step = excluded.next_step,
864
+ task_id = excluded.task_id,
865
+ agent_id = excluded.agent_id,
866
+ branch_name = excluded.branch_name,
867
+ worktree_path = excluded.worktree_path,
730
868
  pinned_context_json = excluded.pinned_context_json,
731
869
  unresolved_questions_json = excluded.unresolved_questions_json,
732
870
  blockers_json = excluded.blockers_json,
@@ -743,6 +881,10 @@ const upsertSession = (db, record) => {
743
881
  record.currentFocus,
744
882
  record.whyBlocked,
745
883
  record.nextStep,
884
+ record.taskId,
885
+ record.agentId,
886
+ record.branchName,
887
+ record.worktreePath,
746
888
  toJsonText(record.pinnedContext, []),
747
889
  toJsonText(record.unresolvedQuestions, []),
748
890
  toJsonText(record.blockers, []),
@@ -761,15 +903,21 @@ const insertLegacySessionEvent = (db, record, sourceFile) => {
761
903
  INSERT OR IGNORE INTO session_events(
762
904
  session_id,
763
905
  event_type,
906
+ task_id,
907
+ agent_id,
908
+ event_kind,
764
909
  payload_json,
765
910
  token_cost,
766
911
  created_at,
767
912
  legacy_key
768
913
  )
769
- VALUES(?, ?, ?, ?, ?, ?)
914
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
770
915
  `).run(
771
916
  record.sessionId,
772
917
  'legacy_import',
918
+ record.taskId,
919
+ record.agentId,
920
+ 'legacy_import',
773
921
  JSON.stringify({ source: sourceFile, updatedAt: record.updatedAt }),
774
922
  0,
775
923
  record.updatedAt,
@@ -866,17 +1014,229 @@ const upsertActiveSession = (db, sessionId, updatedAt) => {
866
1014
  `).run(ACTIVE_SESSION_SCOPE, sessionId, updatedAt);
867
1015
  };
868
1016
 
1017
+ const upsertTaskRow = (db, {
1018
+ taskId,
1019
+ canonicalGoal,
1020
+ normalizedGoal,
1021
+ status = 'planning',
1022
+ branchName = null,
1023
+ worktreePath = projectRoot,
1024
+ lastSessionId = null,
1025
+ createdAt = new Date().toISOString(),
1026
+ updatedAt = createdAt,
1027
+ } = {}) => {
1028
+ db.prepare(`
1029
+ INSERT INTO tasks(
1030
+ task_id,
1031
+ project_scope,
1032
+ canonical_goal,
1033
+ normalized_goal,
1034
+ status,
1035
+ branch_name,
1036
+ worktree_path,
1037
+ last_session_id,
1038
+ created_at,
1039
+ updated_at
1040
+ )
1041
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1042
+ ON CONFLICT(task_id) DO UPDATE SET
1043
+ canonical_goal = excluded.canonical_goal,
1044
+ normalized_goal = excluded.normalized_goal,
1045
+ status = excluded.status,
1046
+ branch_name = excluded.branch_name,
1047
+ worktree_path = excluded.worktree_path,
1048
+ last_session_id = excluded.last_session_id,
1049
+ updated_at = excluded.updated_at,
1050
+ created_at = tasks.created_at
1051
+ `).run(
1052
+ taskId,
1053
+ projectRoot,
1054
+ canonicalGoal,
1055
+ normalizedGoal,
1056
+ normalizeStatus(status, 'planning'),
1057
+ branchName,
1058
+ worktreePath,
1059
+ lastSessionId,
1060
+ createdAt,
1061
+ updatedAt,
1062
+ );
1063
+ };
1064
+
1065
+ const getTaskRow = (db, taskId) => db.prepare(`
1066
+ SELECT
1067
+ task_id,
1068
+ project_scope,
1069
+ canonical_goal,
1070
+ normalized_goal,
1071
+ status,
1072
+ branch_name,
1073
+ worktree_path,
1074
+ last_session_id,
1075
+ created_at,
1076
+ updated_at
1077
+ FROM tasks
1078
+ WHERE task_id = ?
1079
+ `).get(taskId);
1080
+
1081
+ const normalizeTaskRow = (row) => {
1082
+ if (!row) {
1083
+ return null;
1084
+ }
1085
+
1086
+ return {
1087
+ taskId: row.task_id,
1088
+ projectScope: row.project_scope,
1089
+ canonicalGoal: row.canonical_goal,
1090
+ normalizedGoal: row.normalized_goal,
1091
+ status: row.status,
1092
+ branchName: row.branch_name ?? null,
1093
+ worktreePath: row.worktree_path ?? null,
1094
+ lastSessionId: row.last_session_id ?? null,
1095
+ createdAt: row.created_at,
1096
+ updatedAt: row.updated_at,
1097
+ };
1098
+ };
1099
+
1100
+ const insertTaskHandoffRow = (db, {
1101
+ taskId,
1102
+ sessionId = null,
1103
+ fromAgentId = null,
1104
+ toAgentId = null,
1105
+ trigger,
1106
+ summary = {},
1107
+ createdAt = new Date().toISOString(),
1108
+ } = {}) => {
1109
+ const result = db.prepare(`
1110
+ INSERT INTO task_handoffs(
1111
+ task_id,
1112
+ session_id,
1113
+ from_agent_id,
1114
+ to_agent_id,
1115
+ trigger,
1116
+ summary_json,
1117
+ created_at
1118
+ )
1119
+ VALUES(?, ?, ?, ?, ?, ?, ?)
1120
+ `).run(
1121
+ taskId,
1122
+ sessionId,
1123
+ fromAgentId,
1124
+ toAgentId,
1125
+ trigger,
1126
+ JSON.stringify(summary ?? {}),
1127
+ createdAt,
1128
+ );
1129
+
1130
+ return result.lastInsertRowid;
1131
+ };
1132
+
1133
+ const getLatestTaskHandoffRow = (db, taskId) => db.prepare(`
1134
+ SELECT
1135
+ handoff_id,
1136
+ task_id,
1137
+ session_id,
1138
+ from_agent_id,
1139
+ to_agent_id,
1140
+ trigger,
1141
+ summary_json,
1142
+ created_at
1143
+ FROM task_handoffs
1144
+ WHERE task_id = ?
1145
+ ORDER BY datetime(created_at) DESC, handoff_id DESC
1146
+ LIMIT 1
1147
+ `).get(taskId);
1148
+
1149
+ const normalizeTaskHandoffRow = (row) => {
1150
+ if (!row) {
1151
+ return null;
1152
+ }
1153
+
1154
+ return {
1155
+ handoffId: Number(row.handoff_id),
1156
+ taskId: row.task_id,
1157
+ sessionId: row.session_id ?? null,
1158
+ fromAgentId: row.from_agent_id ?? null,
1159
+ toAgentId: row.to_agent_id ?? null,
1160
+ trigger: row.trigger,
1161
+ summary: parseJsonText(row.summary_json, {}),
1162
+ createdAt: row.created_at,
1163
+ };
1164
+ };
1165
+
1166
+ const upsertAgentRunRow = (db, {
1167
+ runId,
1168
+ taskId = null,
1169
+ agentId,
1170
+ parentAgentId = null,
1171
+ client,
1172
+ conversationId = null,
1173
+ sessionId = null,
1174
+ role = 'main',
1175
+ startedAt = new Date().toISOString(),
1176
+ updatedAt = startedAt,
1177
+ } = {}) => {
1178
+ db.prepare(`
1179
+ INSERT INTO agent_runs(
1180
+ run_id,
1181
+ task_id,
1182
+ agent_id,
1183
+ parent_agent_id,
1184
+ client,
1185
+ conversation_id,
1186
+ session_id,
1187
+ role,
1188
+ started_at,
1189
+ updated_at
1190
+ )
1191
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1192
+ ON CONFLICT(run_id) DO UPDATE SET
1193
+ task_id = excluded.task_id,
1194
+ agent_id = excluded.agent_id,
1195
+ parent_agent_id = excluded.parent_agent_id,
1196
+ client = excluded.client,
1197
+ conversation_id = excluded.conversation_id,
1198
+ session_id = excluded.session_id,
1199
+ role = excluded.role,
1200
+ updated_at = excluded.updated_at,
1201
+ started_at = agent_runs.started_at
1202
+ `).run(
1203
+ runId,
1204
+ taskId,
1205
+ agentId,
1206
+ parentAgentId,
1207
+ client,
1208
+ conversationId,
1209
+ sessionId,
1210
+ role,
1211
+ startedAt,
1212
+ updatedAt,
1213
+ );
1214
+ };
1215
+
869
1216
  const buildHookTurnRecord = (hookKey, state = {}) => {
870
1217
  const startedAt = toIsoString(state.startedAt);
871
1218
  const updatedAt = toIsoString(state.updatedAt, startedAt);
1219
+ const conversationId = typeof state.conversationId === 'string' && state.conversationId.trim().length > 0
1220
+ ? state.conversationId
1221
+ : typeof state.cursorConversationId === 'string' && state.cursorConversationId.trim().length > 0
1222
+ ? state.cursorConversationId
1223
+ : typeof state.claudeSessionId === 'string' && state.claudeSessionId.trim().length > 0
1224
+ ? state.claudeSessionId
1225
+ : '';
872
1226
 
873
1227
  return {
874
1228
  hookKey,
875
1229
  client: typeof state.client === 'string' && state.client.trim().length > 0 ? state.client : 'claude',
876
- claudeSessionId: typeof state.claudeSessionId === 'string' ? state.claudeSessionId : '',
1230
+ claudeSessionId: conversationId,
1231
+ conversationId,
877
1232
  projectSessionId: typeof state.projectSessionId === 'string' && state.projectSessionId.trim().length > 0
878
1233
  ? state.projectSessionId
879
1234
  : null,
1235
+ taskId: typeof state.taskId === 'string' && state.taskId.trim().length > 0 ? state.taskId : null,
1236
+ agentId: typeof state.agentId === 'string' && state.agentId.trim().length > 0 ? state.agentId : null,
1237
+ parentAgentId: typeof state.parentAgentId === 'string' && state.parentAgentId.trim().length > 0
1238
+ ? state.parentAgentId
1239
+ : null,
880
1240
  turnId: typeof state.turnId === 'string' && state.turnId.trim().length > 0
881
1241
  ? state.turnId
882
1242
  : `turn-${Date.now()}`,
@@ -906,7 +1266,11 @@ const normalizeHookTurnRow = (row) => {
906
1266
  hookKey: row.hook_key,
907
1267
  client: row.client,
908
1268
  claudeSessionId: row.claude_session_id,
1269
+ conversationId: row.conversation_id ?? row.claude_session_id,
909
1270
  projectSessionId: row.project_session_id ?? null,
1271
+ taskId: row.task_id ?? null,
1272
+ agentId: row.agent_id ?? null,
1273
+ parentAgentId: row.parent_agent_id ?? null,
910
1274
  turnId: row.turn_id,
911
1275
  promptPreview: row.prompt_preview,
912
1276
  continuityState: row.continuity_state,
@@ -927,6 +1291,10 @@ const readHookTurnRow = (db, hookKey) => db.prepare(`
927
1291
  client,
928
1292
  claude_session_id,
929
1293
  project_session_id,
1294
+ task_id,
1295
+ agent_id,
1296
+ parent_agent_id,
1297
+ conversation_id,
930
1298
  turn_id,
931
1299
  prompt_preview,
932
1300
  continuity_state,
@@ -949,6 +1317,10 @@ const upsertHookTurnRow = (db, record) => {
949
1317
  client,
950
1318
  claude_session_id,
951
1319
  project_session_id,
1320
+ task_id,
1321
+ agent_id,
1322
+ parent_agent_id,
1323
+ conversation_id,
952
1324
  turn_id,
953
1325
  prompt_preview,
954
1326
  continuity_state,
@@ -961,11 +1333,15 @@ const upsertHookTurnRow = (db, record) => {
961
1333
  started_at,
962
1334
  updated_at
963
1335
  )
964
- VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1336
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
965
1337
  ON CONFLICT(hook_key) DO UPDATE SET
966
1338
  client = excluded.client,
967
1339
  claude_session_id = excluded.claude_session_id,
968
1340
  project_session_id = excluded.project_session_id,
1341
+ task_id = excluded.task_id,
1342
+ agent_id = excluded.agent_id,
1343
+ parent_agent_id = excluded.parent_agent_id,
1344
+ conversation_id = excluded.conversation_id,
969
1345
  turn_id = excluded.turn_id,
970
1346
  prompt_preview = excluded.prompt_preview,
971
1347
  continuity_state = excluded.continuity_state,
@@ -982,6 +1358,10 @@ const upsertHookTurnRow = (db, record) => {
982
1358
  record.client,
983
1359
  record.claudeSessionId,
984
1360
  record.projectSessionId,
1361
+ record.taskId,
1362
+ record.agentId,
1363
+ record.parentAgentId,
1364
+ record.conversationId,
985
1365
  record.turnId,
986
1366
  record.promptPreview,
987
1367
  record.continuityState,
@@ -1013,6 +1393,69 @@ export const deleteHookTurnState = async ({ filePath = getStateDbPath(), hookKey
1013
1393
  return existing;
1014
1394
  }, { filePath });
1015
1395
 
1396
+ export const getTask = async ({ filePath = getStateDbPath(), taskId, readOnly = false } = {}) => {
1397
+ const reader = readOnly ? withStateDbSnapshot : withStateDb;
1398
+ return reader((db) => normalizeTaskRow(getTaskRow(db, taskId)), { filePath });
1399
+ };
1400
+
1401
+ export const persistTaskHandoff = async ({ filePath = getStateDbPath(), taskId, sessionId = null, fromAgentId = null, toAgentId = null, trigger, summary = {} } = {}) =>
1402
+ withStateDb((db) => {
1403
+ const createdAt = new Date().toISOString();
1404
+ const handoffId = insertTaskHandoffRow(db, {
1405
+ taskId,
1406
+ sessionId,
1407
+ fromAgentId,
1408
+ toAgentId,
1409
+ trigger,
1410
+ summary,
1411
+ createdAt,
1412
+ });
1413
+ return normalizeTaskHandoffRow(getLatestTaskHandoffRow(db, taskId)) ?? {
1414
+ handoffId,
1415
+ taskId,
1416
+ sessionId,
1417
+ fromAgentId,
1418
+ toAgentId,
1419
+ trigger,
1420
+ summary,
1421
+ createdAt,
1422
+ };
1423
+ }, { filePath });
1424
+
1425
+ export const getLatestTaskHandoff = async ({ filePath = getStateDbPath(), taskId, readOnly = false } = {}) => {
1426
+ const reader = readOnly ? withStateDbSnapshot : withStateDb;
1427
+ return reader((db) => normalizeTaskHandoffRow(getLatestTaskHandoffRow(db, taskId)), { filePath });
1428
+ };
1429
+
1430
+ export const upsertAgentRun = async ({ filePath = getStateDbPath(), runId, taskId = null, agentId, parentAgentId = null, client, conversationId = null, sessionId = null, role = 'main' } = {}) =>
1431
+ withStateDb((db) => {
1432
+ const now = new Date().toISOString();
1433
+ upsertAgentRunRow(db, {
1434
+ runId,
1435
+ taskId,
1436
+ agentId,
1437
+ parentAgentId,
1438
+ client,
1439
+ conversationId,
1440
+ sessionId,
1441
+ role,
1442
+ startedAt: now,
1443
+ updatedAt: now,
1444
+ });
1445
+ return {
1446
+ runId,
1447
+ taskId,
1448
+ agentId,
1449
+ parentAgentId,
1450
+ client,
1451
+ conversationId,
1452
+ sessionId,
1453
+ role,
1454
+ startedAt: now,
1455
+ updatedAt: now,
1456
+ };
1457
+ }, { filePath });
1458
+
1016
1459
  const listLegacySessionFiles = (sessionsDir) => {
1017
1460
  if (!fs.existsSync(sessionsDir)) {
1018
1461
  return [];
@@ -1051,6 +1494,17 @@ export const importLegacyState = async ({
1051
1494
  const record = buildSessionRecord(sessionId, payload);
1052
1495
 
1053
1496
  upsertSession(db, record);
1497
+ upsertTaskRow(db, {
1498
+ taskId: record.taskId,
1499
+ canonicalGoal: record.goal,
1500
+ normalizedGoal: normalizeTaskText(record.goal),
1501
+ status: record.status,
1502
+ branchName: record.branchName,
1503
+ worktreePath: record.worktreePath,
1504
+ lastSessionId: sessionId,
1505
+ createdAt: record.createdAt,
1506
+ updatedAt: record.updatedAt,
1507
+ });
1054
1508
  insertLegacySessionEvent(db, record, fileName);
1055
1509
  report.sessions[exists ? 'skipped' : 'imported'] += 1;
1056
1510
  }