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.
@@ -10,6 +10,7 @@ import {
10
10
  SQLITE_SCHEMA_VERSION,
11
11
  cleanupLegacyState,
12
12
  compactState,
13
+ deriveTaskId,
13
14
  getStateStorageHealth,
14
15
  importLegacyState,
15
16
  withStateDb,
@@ -125,6 +126,28 @@ const generateSessionId = (goal) => {
125
126
  return `${date}-${slug}`;
126
127
  };
127
128
 
129
+ const normalizeTaskText = (value) => String(value ?? '').toLowerCase().replace(/\s+/g, ' ').trim();
130
+
131
+ const resolveTaskIdentity = ({ taskId, goal, branchName, worktreePath }) => {
132
+ const canonicalGoal = typeof goal === 'string' ? goal.trim() : '';
133
+ const resolvedBranchName = typeof branchName === 'string' && branchName.trim().length > 0 ? branchName.trim() : null;
134
+ const resolvedWorktreePath = typeof worktreePath === 'string' && worktreePath.trim().length > 0 ? worktreePath.trim() : null;
135
+
136
+ return {
137
+ taskId: typeof taskId === 'string' && taskId.trim().length > 0
138
+ ? taskId.trim()
139
+ : deriveTaskId({
140
+ goal: canonicalGoal,
141
+ branchName: resolvedBranchName ?? '',
142
+ worktreePath: resolvedWorktreePath ?? '',
143
+ }),
144
+ canonicalGoal,
145
+ normalizedGoal: normalizeTaskText(canonicalGoal),
146
+ branchName: resolvedBranchName,
147
+ worktreePath: resolvedWorktreePath,
148
+ };
149
+ };
150
+
128
151
  const truncateString = (str, maxLength) => {
129
152
  if (!str || str.length <= maxLength) return str;
130
153
  if (maxLength <= 3) return '';
@@ -179,8 +202,12 @@ const mergeUniqueStrings = (...lists) => {
179
202
  };
180
203
 
181
204
  const buildComparableSessionState = (data = {}) => ({
205
+ taskId: typeof data.taskId === 'string' ? data.taskId : '',
206
+ agentId: typeof data.agentId === 'string' ? data.agentId : '',
182
207
  goal: typeof data.goal === 'string' ? data.goal : '',
183
208
  status: normalizeStatus(data.status),
209
+ branchName: typeof data.branchName === 'string' ? data.branchName : '',
210
+ worktreePath: typeof data.worktreePath === 'string' ? data.worktreePath : '',
184
211
  pinnedContext: mergeUniqueStrings(data.pinnedContext),
185
212
  unresolvedQuestions: mergeUniqueStrings(data.unresolvedQuestions),
186
213
  currentFocus: typeof data.currentFocus === 'string' ? data.currentFocus : '',
@@ -198,8 +225,16 @@ const buildAppendData = (existingData, update) => {
198
225
  const touchedFiles = mergeUniqueStrings(existingData.touchedFiles, update.touchedFiles);
199
226
 
200
227
  return {
228
+ taskId: update.taskId || existingData.taskId || resolveTaskIdentity({
229
+ goal: update.goal || existingData.goal || 'Untitled session',
230
+ branchName: update.branchName || existingData.branchName,
231
+ worktreePath: update.worktreePath || existingData.worktreePath,
232
+ }).taskId,
233
+ agentId: update.agentId || existingData.agentId || null,
201
234
  goal: update.goal || existingData.goal || 'Untitled session',
202
235
  status: normalizeStatus(update.status, normalizeStatus(existingData.status)),
236
+ branchName: update.branchName || existingData.branchName || null,
237
+ worktreePath: update.worktreePath || existingData.worktreePath || null,
203
238
  pinnedContext: mergeUniqueStrings(existingData.pinnedContext, update.pinnedContext),
204
239
  unresolvedQuestions: mergeUniqueStrings(existingData.unresolvedQuestions, update.unresolvedQuestions),
205
240
  currentFocus: update.currentFocus || existingData.currentFocus || '',
@@ -221,8 +256,16 @@ const buildReplaceData = (update) => {
221
256
  const touchedFiles = mergeUniqueStrings(update.touchedFiles);
222
257
 
223
258
  return {
259
+ taskId: update.taskId || resolveTaskIdentity({
260
+ goal: update.goal || 'Untitled session',
261
+ branchName: update.branchName,
262
+ worktreePath: update.worktreePath,
263
+ }).taskId,
264
+ agentId: update.agentId || null,
224
265
  goal: update.goal || 'Untitled session',
225
266
  status: normalizeStatus(update.status),
267
+ branchName: update.branchName ?? null,
268
+ worktreePath: update.worktreePath ?? null,
226
269
  pinnedContext: mergeUniqueStrings(update.pinnedContext),
227
270
  unresolvedQuestions: mergeUniqueStrings(update.unresolvedQuestions),
228
271
  currentFocus: update.currentFocus ?? '',
@@ -252,8 +295,12 @@ const getAutoAppendChanges = (existingData, mergedData) => {
252
295
  const comparableAfter = buildComparableSessionState(mergedData);
253
296
  const changes = [];
254
297
 
298
+ if (comparableAfter.taskId !== comparableBefore.taskId) changes.push('taskId');
299
+ if (comparableAfter.agentId !== comparableBefore.agentId) changes.push('agentId');
255
300
  if (comparableAfter.goal !== comparableBefore.goal) changes.push('goal');
256
301
  if (comparableAfter.status !== comparableBefore.status) changes.push('status');
302
+ if (comparableAfter.branchName !== comparableBefore.branchName) changes.push('branchName');
303
+ if (comparableAfter.worktreePath !== comparableBefore.worktreePath) changes.push('worktreePath');
257
304
  if (comparableAfter.currentFocus !== comparableBefore.currentFocus) changes.push('currentFocus');
258
305
  if (comparableAfter.whyBlocked !== comparableBefore.whyBlocked) changes.push('whyBlocked');
259
306
  if (comparableAfter.nextStep !== comparableBefore.nextStep) changes.push('nextStep');
@@ -546,6 +593,10 @@ const getSessionRow = (db, sessionId) => db.prepare(`
546
593
  current_focus,
547
594
  why_blocked,
548
595
  next_step,
596
+ task_id,
597
+ agent_id,
598
+ branch_name,
599
+ worktree_path,
549
600
  pinned_context_json,
550
601
  unresolved_questions_json,
551
602
  blockers_json,
@@ -566,6 +617,141 @@ const getActiveSessionId = (db) =>
566
617
  WHERE scope = ?
567
618
  `).get(ACTIVE_SESSION_SCOPE)?.session_id ?? null;
568
619
 
620
+ const getTaskRow = (db, taskId) => db.prepare(`
621
+ SELECT
622
+ task_id,
623
+ project_scope,
624
+ canonical_goal,
625
+ normalized_goal,
626
+ status,
627
+ branch_name,
628
+ worktree_path,
629
+ last_session_id,
630
+ created_at,
631
+ updated_at
632
+ FROM tasks
633
+ WHERE task_id = ?
634
+ `).get(taskId);
635
+
636
+ const getTaskRowForSession = (db, sessionId) => db.prepare(`
637
+ SELECT
638
+ t.task_id,
639
+ t.project_scope,
640
+ t.canonical_goal,
641
+ t.normalized_goal,
642
+ t.status,
643
+ t.branch_name,
644
+ t.worktree_path,
645
+ t.last_session_id,
646
+ t.created_at,
647
+ t.updated_at
648
+ FROM tasks t
649
+ JOIN sessions s ON s.task_id = t.task_id
650
+ WHERE s.session_id = ?
651
+ `).get(sessionId);
652
+
653
+ const normalizeTaskRow = (row) => {
654
+ if (!row) {
655
+ return null;
656
+ }
657
+
658
+ return {
659
+ taskId: row.task_id,
660
+ projectScope: row.project_scope,
661
+ canonicalGoal: row.canonical_goal,
662
+ normalizedGoal: row.normalized_goal,
663
+ status: row.status,
664
+ branchName: row.branch_name ?? null,
665
+ worktreePath: row.worktree_path ?? null,
666
+ lastSessionId: row.last_session_id ?? null,
667
+ createdAt: row.created_at,
668
+ updatedAt: row.updated_at,
669
+ };
670
+ };
671
+
672
+ const upsertTask = (db, {
673
+ taskId,
674
+ goal,
675
+ status,
676
+ branchName = null,
677
+ worktreePath = null,
678
+ lastSessionId = null,
679
+ createdAt = new Date().toISOString(),
680
+ updatedAt = createdAt,
681
+ } = {}) => {
682
+ const task = resolveTaskIdentity({ taskId, goal, branchName, worktreePath });
683
+ db.prepare(`
684
+ INSERT INTO tasks(
685
+ task_id,
686
+ project_scope,
687
+ canonical_goal,
688
+ normalized_goal,
689
+ status,
690
+ branch_name,
691
+ worktree_path,
692
+ last_session_id,
693
+ created_at,
694
+ updated_at
695
+ )
696
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
697
+ ON CONFLICT(task_id) DO UPDATE SET
698
+ canonical_goal = excluded.canonical_goal,
699
+ normalized_goal = excluded.normalized_goal,
700
+ status = excluded.status,
701
+ branch_name = excluded.branch_name,
702
+ worktree_path = excluded.worktree_path,
703
+ last_session_id = excluded.last_session_id,
704
+ updated_at = excluded.updated_at,
705
+ created_at = tasks.created_at
706
+ `).run(
707
+ task.taskId,
708
+ 'project',
709
+ task.canonicalGoal,
710
+ task.normalizedGoal,
711
+ normalizeStatus(status, 'planning'),
712
+ task.branchName,
713
+ task.worktreePath,
714
+ lastSessionId,
715
+ createdAt,
716
+ updatedAt,
717
+ );
718
+
719
+ return normalizeTaskRow(getTaskRow(db, task.taskId));
720
+ };
721
+
722
+ const getLatestTaskHandoffRow = (db, taskId) => db.prepare(`
723
+ SELECT
724
+ handoff_id,
725
+ task_id,
726
+ session_id,
727
+ from_agent_id,
728
+ to_agent_id,
729
+ trigger,
730
+ summary_json,
731
+ created_at
732
+ FROM task_handoffs
733
+ WHERE task_id = ?
734
+ ORDER BY datetime(created_at) DESC, handoff_id DESC
735
+ LIMIT 1
736
+ `).get(taskId);
737
+
738
+ const normalizeTaskHandoff = (row) => {
739
+ if (!row) {
740
+ return null;
741
+ }
742
+
743
+ return {
744
+ handoffId: Number(row.handoff_id),
745
+ taskId: row.task_id,
746
+ sessionId: row.session_id ?? null,
747
+ fromAgentId: row.from_agent_id ?? null,
748
+ toAgentId: row.to_agent_id ?? null,
749
+ trigger: row.trigger,
750
+ summary: parseJsonText(row.summary_json, {}),
751
+ createdAt: row.created_at,
752
+ };
753
+ };
754
+
569
755
  const hydrateSession = (row) => {
570
756
  if (!row) {
571
757
  return null;
@@ -583,8 +769,12 @@ const hydrateSession = (row) => {
583
769
  ...snapshot,
584
770
  schemaVersion: Number(snapshot.schemaVersion ?? 1),
585
771
  sessionId: row.session_id,
772
+ taskId: row.task_id ?? snapshot.taskId ?? null,
773
+ agentId: row.agent_id ?? snapshot.agentId ?? null,
586
774
  goal: typeof row.goal === 'string' ? row.goal : (snapshot.goal ?? ''),
587
775
  status: normalizeStatus(row.status, normalizeStatus(snapshot.status)),
776
+ branchName: row.branch_name ?? snapshot.branchName ?? null,
777
+ worktreePath: row.worktree_path ?? snapshot.worktreePath ?? null,
588
778
  currentFocus: typeof row.current_focus === 'string' ? row.current_focus : (snapshot.currentFocus ?? ''),
589
779
  whyBlocked: typeof row.why_blocked === 'string' ? row.why_blocked : (snapshot.whyBlocked ?? ''),
590
780
  nextStep: typeof row.next_step === 'string' ? row.next_step : (snapshot.nextStep ?? ''),
@@ -765,18 +955,20 @@ const compressSummary = (data, maxTokens) => {
765
955
  return { compressed, tokens, truncated: true, omitted, compressionLevel };
766
956
  };
767
957
 
768
- const cacheSummary = (db, sessionId, { compressed, tokens, compressionLevel, omitted, updatedAt }) => {
958
+ const cacheSummary = (db, sessionId, { taskId = null, compressed, tokens, compressionLevel, omitted, updatedAt }) => {
769
959
  db.prepare(`
770
960
  INSERT INTO summary_cache(
771
961
  session_id,
962
+ task_id,
772
963
  summary_json,
773
964
  tokens,
774
965
  compression_level,
775
966
  omitted_json,
776
967
  updated_at
777
968
  )
778
- VALUES(?, ?, ?, ?, ?, ?)
969
+ VALUES(?, ?, ?, ?, ?, ?, ?)
779
970
  ON CONFLICT(session_id) DO UPDATE SET
971
+ task_id = excluded.task_id,
780
972
  summary_json = excluded.summary_json,
781
973
  tokens = excluded.tokens,
782
974
  compression_level = excluded.compression_level,
@@ -784,6 +976,7 @@ const cacheSummary = (db, sessionId, { compressed, tokens, compressionLevel, omi
784
976
  updated_at = excluded.updated_at
785
977
  `).run(
786
978
  sessionId,
979
+ taskId,
787
980
  JSON.stringify(compressed),
788
981
  tokens,
789
982
  compressionLevel,
@@ -806,6 +999,12 @@ const saveSession = (db, sessionId, data, { action, eventPayload } = {}) => {
806
999
  const existing = hydrateSession(getSessionRow(db, sessionId));
807
1000
  const createdAt = existing?.createdAt ?? new Date().toISOString();
808
1001
  const updatedAt = new Date().toISOString();
1002
+ const task = resolveTaskIdentity({
1003
+ taskId: data.taskId ?? existing?.taskId ?? null,
1004
+ goal: data.goal ?? existing?.goal ?? '',
1005
+ branchName: data.branchName ?? existing?.branchName ?? null,
1006
+ worktreePath: data.worktreePath ?? existing?.worktreePath ?? null,
1007
+ });
809
1008
  const completed = mergeUniqueStrings(data.completed);
810
1009
  const decisions = mergeUniqueStrings(data.decisions);
811
1010
  const touchedFiles = mergeUniqueStrings(data.touchedFiles);
@@ -814,8 +1013,12 @@ const saveSession = (db, sessionId, data, { action, eventPayload } = {}) => {
814
1013
  const blockers = mergeUniqueStrings(data.blockers);
815
1014
 
816
1015
  const snapshot = {
1016
+ taskId: task.taskId,
1017
+ agentId: data.agentId ?? existing?.agentId ?? null,
817
1018
  goal: typeof data.goal === 'string' ? data.goal : '',
818
1019
  status: normalizeStatus(data.status),
1020
+ branchName: task.branchName,
1021
+ worktreePath: task.worktreePath,
819
1022
  pinnedContext,
820
1023
  unresolvedQuestions,
821
1024
  currentFocus: data.currentFocus ?? '',
@@ -842,6 +1045,10 @@ const saveSession = (db, sessionId, data, { action, eventPayload } = {}) => {
842
1045
  current_focus,
843
1046
  why_blocked,
844
1047
  next_step,
1048
+ task_id,
1049
+ agent_id,
1050
+ branch_name,
1051
+ worktree_path,
845
1052
  pinned_context_json,
846
1053
  unresolved_questions_json,
847
1054
  blockers_json,
@@ -852,13 +1059,17 @@ const saveSession = (db, sessionId, data, { action, eventPayload } = {}) => {
852
1059
  created_at,
853
1060
  updated_at
854
1061
  )
855
- VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1062
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
856
1063
  ON CONFLICT(session_id) DO UPDATE SET
857
1064
  goal = excluded.goal,
858
1065
  status = excluded.status,
859
1066
  current_focus = excluded.current_focus,
860
1067
  why_blocked = excluded.why_blocked,
861
1068
  next_step = excluded.next_step,
1069
+ task_id = excluded.task_id,
1070
+ agent_id = excluded.agent_id,
1071
+ branch_name = excluded.branch_name,
1072
+ worktree_path = excluded.worktree_path,
862
1073
  pinned_context_json = excluded.pinned_context_json,
863
1074
  unresolved_questions_json = excluded.unresolved_questions_json,
864
1075
  blockers_json = excluded.blockers_json,
@@ -875,6 +1086,10 @@ const saveSession = (db, sessionId, data, { action, eventPayload } = {}) => {
875
1086
  snapshot.currentFocus,
876
1087
  snapshot.whyBlocked,
877
1088
  snapshot.nextStep,
1089
+ snapshot.taskId,
1090
+ snapshot.agentId,
1091
+ snapshot.branchName,
1092
+ snapshot.worktreePath,
878
1093
  JSON.stringify(pinnedContext),
879
1094
  JSON.stringify(unresolvedQuestions),
880
1095
  JSON.stringify(blockers),
@@ -887,20 +1102,36 @@ const saveSession = (db, sessionId, data, { action, eventPayload } = {}) => {
887
1102
  );
888
1103
 
889
1104
  writeActiveSession(db, sessionId, updatedAt);
1105
+ upsertTask(db, {
1106
+ taskId: snapshot.taskId,
1107
+ goal: snapshot.goal,
1108
+ status: snapshot.status,
1109
+ branchName: snapshot.branchName,
1110
+ worktreePath: snapshot.worktreePath,
1111
+ lastSessionId: sessionId,
1112
+ createdAt,
1113
+ updatedAt,
1114
+ });
890
1115
 
891
1116
  if (action) {
892
1117
  db.prepare(`
893
1118
  INSERT INTO session_events(
894
1119
  session_id,
895
1120
  event_type,
1121
+ task_id,
1122
+ agent_id,
1123
+ event_kind,
896
1124
  payload_json,
897
1125
  token_cost,
898
1126
  created_at
899
1127
  )
900
- VALUES(?, ?, ?, ?, ?)
1128
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?)
901
1129
  `).run(
902
1130
  sessionId,
903
1131
  action,
1132
+ snapshot.taskId,
1133
+ snapshot.agentId,
1134
+ action,
904
1135
  JSON.stringify(pruneEmptyFields(eventPayload ?? {})),
905
1136
  0,
906
1137
  updatedAt,
@@ -961,6 +1192,15 @@ const listSessions = (db, { cleanup = true } = {}) => {
961
1192
  });
962
1193
  };
963
1194
 
1195
+ const getLatestSessionIdForTask = (db, taskId) =>
1196
+ db.prepare(`
1197
+ SELECT session_id
1198
+ FROM sessions
1199
+ WHERE task_id = ?
1200
+ ORDER BY datetime(updated_at) DESC, session_id ASC
1201
+ LIMIT 1
1202
+ `).get(taskId)?.session_id ?? null;
1203
+
964
1204
  const buildResumeCandidates = (sessions) =>
965
1205
  sessions.slice(0, MAX_RESUME_CANDIDATES).map((session) => ({
966
1206
  sessionId: session.sessionId,
@@ -1098,6 +1338,7 @@ const resolveAutoResumeTarget = (db, { forceRecommended = false, cleanup = true
1098
1338
  export const smartSummary = async ({
1099
1339
  action,
1100
1340
  sessionId,
1341
+ taskId,
1101
1342
  update,
1102
1343
  maxTokens = DEFAULT_MAX_TOKENS,
1103
1344
  event,
@@ -1112,6 +1353,9 @@ export const smartSummary = async ({
1112
1353
  nextStep,
1113
1354
  currentFocus,
1114
1355
  whyBlocked,
1356
+ agentId,
1357
+ branchName,
1358
+ worktreePath,
1115
1359
  pinnedContext,
1116
1360
  unresolvedQuestions,
1117
1361
  blockers,
@@ -1121,14 +1365,19 @@ export const smartSummary = async ({
1121
1365
  } = {}) => {
1122
1366
  const startTime = Date.now();
1123
1367
 
1124
- if (!update && (goal || status || nextStep || currentFocus || whyBlocked ||
1368
+ if (!update && (goal || status || nextStep || currentFocus || whyBlocked ||
1369
+ taskId || agentId || branchName || worktreePath ||
1125
1370
  pinnedContext || unresolvedQuestions || blockers || completed || decisions || touchedFiles)) {
1126
1371
  update = {
1127
1372
  goal,
1373
+ taskId,
1128
1374
  status,
1129
1375
  nextStep,
1130
1376
  currentFocus,
1131
1377
  whyBlocked,
1378
+ agentId,
1379
+ branchName,
1380
+ worktreePath,
1132
1381
  pinnedContext,
1133
1382
  unresolvedQuestions,
1134
1383
  blockers,
@@ -1180,6 +1429,17 @@ export const smartSummary = async ({
1180
1429
  }
1181
1430
  : null;
1182
1431
 
1432
+ if (!targetSessionId && typeof taskId === 'string' && taskId.trim().length > 0) {
1433
+ targetSessionId = getLatestSessionIdForTask(db, taskId.trim());
1434
+ if (targetSessionId) {
1435
+ resumeMeta = {
1436
+ autoResumed: false,
1437
+ resumeSource: 'task_id',
1438
+ recommendedSessionId: targetSessionId,
1439
+ };
1440
+ }
1441
+ }
1442
+
1183
1443
  if (!targetSessionId && wantsAutoResume) {
1184
1444
  const resolution = resolveAutoResumeTarget(db, {
1185
1445
  forceRecommended: sessionId === AUTO_RESUME_SESSION_ID,
@@ -1225,12 +1485,20 @@ export const smartSummary = async ({
1225
1485
  writeActiveSession(db, targetSessionId, session.updatedAt);
1226
1486
  }
1227
1487
 
1488
+ const taskRecord = normalizeTaskRow(
1489
+ (session.taskId ? getTaskRow(db, session.taskId) : null) ?? getTaskRowForSession(db, targetSessionId),
1490
+ );
1491
+ const latestHandoff = taskRecord?.taskId
1492
+ ? normalizeTaskHandoff(getLatestTaskHandoffRow(db, taskRecord.taskId))
1493
+ : null;
1494
+
1228
1495
  const { compressed, tokens, truncated, omitted, compressionLevel } = compressSummary(session, maxTokens);
1229
1496
  const rawTokens = countTokens(JSON.stringify(session));
1230
1497
  const summaryMetrics = buildSummaryMetrics(rawTokens, tokens);
1231
1498
 
1232
1499
  if (allowReadSideEffects) {
1233
1500
  cacheSummary(db, targetSessionId, {
1501
+ taskId: session.taskId ?? taskRecord?.taskId ?? null,
1234
1502
  compressed,
1235
1503
  tokens,
1236
1504
  compressionLevel,
@@ -1277,6 +1545,17 @@ export const smartSummary = async ({
1277
1545
  resumeSource: resumeMeta?.resumeSource ?? 'direct',
1278
1546
  ambiguous: resumeMeta?.ambiguous ?? false,
1279
1547
  recommendedSessionId: resumeMeta?.recommendedSessionId ?? targetSessionId,
1548
+ ...(taskRecord ? {
1549
+ task: {
1550
+ taskId: taskRecord.taskId,
1551
+ status: taskRecord.status,
1552
+ canonicalGoal: taskRecord.canonicalGoal,
1553
+ branchName: taskRecord.branchName,
1554
+ worktreePath: taskRecord.worktreePath,
1555
+ resolution: resumeMeta?.resumeSource === 'task_id' ? 'exact' : (resumeMeta?.autoResumed ? 'ranked' : 'active'),
1556
+ },
1557
+ } : {}),
1558
+ ...(latestHandoff ? { handoff: latestHandoff } : {}),
1280
1559
  ...(resumeMeta?.candidates ? { candidates: resumeMeta.candidates } : {}),
1281
1560
  schemaVersion: session.schemaVersion ?? 1,
1282
1561
  updatedAt: session.updatedAt,
@@ -1305,6 +1584,7 @@ export const smartSummary = async ({
1305
1584
  }
1306
1585
 
1307
1586
  const isActiveSession = getActiveSessionId(db) === targetSessionId;
1587
+ const session = hydrateSession(getSessionRow(db, targetSessionId));
1308
1588
 
1309
1589
  db.exec('BEGIN');
1310
1590
  try {
@@ -1313,6 +1593,12 @@ export const smartSummary = async ({
1313
1593
  db.prepare('DELETE FROM active_session WHERE scope = ?').run(ACTIVE_SESSION_SCOPE);
1314
1594
  }
1315
1595
  db.prepare('DELETE FROM summary_cache WHERE session_id = ?').run(targetSessionId);
1596
+ if (session?.taskId) {
1597
+ const remaining = db.prepare('SELECT COUNT(*) AS count FROM sessions WHERE task_id = ?').get(session.taskId).count;
1598
+ if (remaining === 0) {
1599
+ db.prepare('DELETE FROM tasks WHERE task_id = ?').run(session.taskId);
1600
+ }
1601
+ }
1316
1602
  db.exec('COMMIT');
1317
1603
  } catch (error) {
1318
1604
  db.exec('ROLLBACK');
@@ -1365,6 +1651,13 @@ export const smartSummary = async ({
1365
1651
  let targetSessionId = sessionId;
1366
1652
  let existingData = {};
1367
1653
 
1654
+ if (!targetSessionId && typeof taskId === 'string' && taskId.trim().length > 0) {
1655
+ targetSessionId = getLatestSessionIdForTask(db, taskId.trim());
1656
+ if (targetSessionId) {
1657
+ existingData = hydrateSession(getSessionRow(db, targetSessionId)) ?? {};
1658
+ }
1659
+ }
1660
+
1368
1661
  if (!targetSessionId || targetSessionId === 'new') {
1369
1662
  if (action === 'append' || action === 'auto_append' || action === 'checkpoint') {
1370
1663
  const activeSessionId = getActiveSessionId(db);
@@ -1402,6 +1695,7 @@ export const smartSummary = async ({
1402
1695
 
1403
1696
  if (action === 'auto_append' && changedFields.length === 0) {
1404
1697
  const currentSession = hydrateSession(getSessionRow(db, targetSessionId)) ?? mergedData;
1698
+ const currentTask = currentSession.taskId ? normalizeTaskRow(getTaskRow(db, currentSession.taskId)) : null;
1405
1699
  const { compressed, tokens, truncated, omitted, compressionLevel } = compressSummary(currentSession, maxTokens);
1406
1700
  const rawTokens = countTokens(JSON.stringify(currentSession));
1407
1701
 
@@ -1431,6 +1725,7 @@ export const smartSummary = async ({
1431
1725
  truncated,
1432
1726
  omitted,
1433
1727
  compressionLevel,
1728
+ ...(currentTask ? { task: currentTask } : {}),
1434
1729
  schemaVersion: currentSession.schemaVersion ?? SQLITE_SCHEMA_VERSION,
1435
1730
  updatedAt: currentSession.updatedAt,
1436
1731
  message: 'Skipped auto-append because no meaningful context changed.',
@@ -1439,6 +1734,7 @@ export const smartSummary = async ({
1439
1734
 
1440
1735
  if (action === 'checkpoint' && !checkpointDecision.shouldPersist) {
1441
1736
  const currentSession = hydrateSession(getSessionRow(db, targetSessionId)) ?? mergedData;
1737
+ const currentTask = currentSession.taskId ? normalizeTaskRow(getTaskRow(db, currentSession.taskId)) : null;
1442
1738
  const { compressed, tokens, truncated, omitted, compressionLevel } = compressSummary(currentSession, maxTokens);
1443
1739
  const rawTokens = countTokens(JSON.stringify(currentSession));
1444
1740
 
@@ -1479,6 +1775,7 @@ export const smartSummary = async ({
1479
1775
  truncated,
1480
1776
  omitted,
1481
1777
  compressionLevel,
1778
+ ...(currentTask ? { task: currentTask } : {}),
1482
1779
  schemaVersion: currentSession.schemaVersion ?? SQLITE_SCHEMA_VERSION,
1483
1780
  updatedAt: currentSession.updatedAt,
1484
1781
  message: checkpointDecision.reason,
@@ -1505,6 +1802,7 @@ export const smartSummary = async ({
1505
1802
  const summaryMetrics = buildSummaryMetrics(rawTokens, tokens);
1506
1803
 
1507
1804
  cacheSummary(db, targetSessionId, {
1805
+ taskId: savedData.taskId ?? null,
1508
1806
  compressed,
1509
1807
  tokens,
1510
1808
  compressionLevel,
@@ -1532,6 +1830,9 @@ export const smartSummary = async ({
1532
1830
  return addRepoSafety({
1533
1831
  action,
1534
1832
  sessionId: targetSessionId,
1833
+ ...(savedData.taskId ? {
1834
+ task: normalizeTaskRow(getTaskRow(db, savedData.taskId)),
1835
+ } : {}),
1535
1836
  skipped: false,
1536
1837
  ...(action === 'auto_append' || action === 'checkpoint' ? { changedFields } : {}),
1537
1838
  ...(action === 'checkpoint'
@@ -409,6 +409,7 @@ const persistSmartTurnQualityMetrics = async ({
409
409
 
410
410
  const startTurn = async ({
411
411
  sessionId,
412
+ taskId,
412
413
  prompt,
413
414
  maxTokens = DEFAULT_START_MAX_TOKENS,
414
415
  ensureSession = false,
@@ -420,6 +421,7 @@ const startTurn = async ({
420
421
  let summaryResult = await smartSummary({
421
422
  action: 'get',
422
423
  sessionId,
424
+ taskId,
423
425
  maxTokens,
424
426
  });
425
427
 
@@ -437,6 +439,7 @@ const startTurn = async ({
437
439
  summaryResult = await smartSummary({
438
440
  action: 'get',
439
441
  sessionId: created.sessionId,
442
+ taskId: created.task?.taskId ?? null,
440
443
  maxTokens,
441
444
  });
442
445
  }
@@ -457,6 +460,7 @@ const startTurn = async ({
457
460
  summaryResult = await smartSummary({
458
461
  action: 'get',
459
462
  sessionId: created.sessionId,
463
+ taskId: created.task?.taskId ?? null,
460
464
  maxTokens,
461
465
  });
462
466
  continuity = classifyContinuity({ prompt, summaryResult });
@@ -565,6 +569,8 @@ const startTurn = async ({
565
569
  ...(workflow ? { workflow } : {}),
566
570
  ...(summaryResult.candidates ? { candidates: summaryResult.candidates } : {}),
567
571
  ...(summaryResult.recommendedSessionId ? { recommendedSessionId: summaryResult.recommendedSessionId } : {}),
572
+ ...(summaryResult.task ? { task: summaryResult.task } : {}),
573
+ ...(summaryResult.handoff ? { handoff: summaryResult.handoff } : {}),
568
574
  ...(metrics ? { metrics: summarizeMetrics(metrics) } : {}),
569
575
  ...(isStorageUnhealthy(summaryResult.storageHealth ?? metrics?.storageHealth) ? { storageHealth: summaryResult.storageHealth ?? metrics?.storageHealth } : {}),
570
576
  recommendedPath,
@@ -587,6 +593,7 @@ const startTurn = async ({
587
593
 
588
594
  const endTurn = async ({
589
595
  sessionId,
596
+ taskId,
590
597
  event = DEFAULT_END_EVENT,
591
598
  update = {},
592
599
  force = false,
@@ -599,6 +606,7 @@ const endTurn = async ({
599
606
  const checkpoint = await smartSummary({
600
607
  action: 'checkpoint',
601
608
  sessionId,
609
+ taskId,
602
610
  event,
603
611
  update,
604
612
  force,
@@ -697,6 +705,7 @@ const endTurn = async ({
697
705
  export const smartTurn = async ({
698
706
  phase,
699
707
  sessionId,
708
+ taskId,
700
709
  prompt,
701
710
  update,
702
711
  event,
@@ -710,6 +719,7 @@ export const smartTurn = async ({
710
719
  if (phase === 'start') {
711
720
  return startTurn({
712
721
  sessionId,
722
+ taskId,
713
723
  prompt,
714
724
  maxTokens,
715
725
  ensureSession,
@@ -722,6 +732,7 @@ export const smartTurn = async ({
722
732
  if (phase === 'end') {
723
733
  return endTurn({
724
734
  sessionId,
735
+ taskId,
725
736
  event,
726
737
  update,
727
738
  force,