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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/client-contract.js +17 -1
- package/src/orchestration/adapters/claude-adapter.js +64 -4
- package/src/orchestration/adapters/cursor-adapter.js +64 -4
- package/src/server.js +15 -4
- package/src/storage/sqlite.js +459 -5
- package/src/tools/smart-summary.js +306 -5
- package/src/tools/smart-turn.js +11 -0
|
@@ -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'
|
package/src/tools/smart-turn.js
CHANGED
|
@@ -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,
|