@openchambery/web 1.19.14 → 1.19.15-beta.2
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/dist/assets/AssistantView-Bp4j2j2j.js +2 -0
- package/dist/assets/{DiagramView-CovEl1BY.js → DiagramView-OdVPLt6s.js} +1 -1
- package/dist/assets/MarkdownRenderer-4weeFDNg.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-CCfHY-86.js → MarkdownRendererImpl-D9O0gsEc.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-y6QgIUZA.js → MarkstreamRendererImpl-ovXFLt9J.js} +1 -1
- package/dist/assets/MobileDetailNavigation-BYzib11p.js +1 -0
- package/dist/assets/{MobileShareBridge-B4S7BaUj.js → MobileShareBridge-QUhimU7K.js} +1 -1
- package/dist/assets/MobileSurface-zxMVX0-y.js +2 -0
- package/dist/assets/{MultiRunWindow-BeHa-CG6.js → MultiRunWindow-CzG_tdV0.js} +1 -1
- package/dist/assets/{SettingsView-Dn7odtQz.js → SettingsView-Dr-d6ryG.js} +3 -3
- package/dist/assets/{SettingsWindow-C0tWPS5f.js → SettingsWindow-BI4lg5NY.js} +1 -1
- package/dist/assets/{TerminalView-BnJgC7Qo.js → TerminalView-C6Wqo-mQ.js} +1 -1
- package/dist/assets/{ToolOutputDialog-D9S-X8hM.js → ToolOutputDialog-DEIsCH48.js} +1 -1
- package/dist/assets/{appThemeRegistry-DBrvaJnq.js → appThemeRegistry-nDZxiGjG.js} +1 -1
- package/dist/assets/{fileMentionTouchSelection-Dc004xOh.js → fileMentionTouchSelection-C4twfQKY.js} +1 -1
- package/dist/assets/{gitApi-Cm3JLQqh.js → gitApi-BRZeUzHF.js} +1 -1
- package/dist/assets/{insertionBoundaries-CTWYFsbH.js → insertionBoundaries-CK0Nv-Vf.js} +1 -1
- package/dist/assets/{main-DwMprEyr.js → main-BRIfvcsR.js} +2 -2
- package/dist/assets/{main-CSqu3MB4.js → main-w3PEAPHR.js} +4 -4
- package/dist/assets/{miniChat-C8bzY_Ye.js → miniChat-DSuRbtqh.js} +2 -2
- package/dist/assets/{mobile-Oy-SRKkr.js → mobile-B9Ek0XAw.js} +2 -2
- package/dist/assets/{multirun-ClcuaiKU.js → multirun-Bh4P5g3q.js} +1 -1
- package/dist/assets/{projectMeta-F93wE2Ye.js → projectMeta-H0Ghy4GZ.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-rOOhMVlQ.js → renderElectronMiniChatApp-DJQvuapb.js} +2 -2
- package/dist/assets/renderMobileApp-BP2iltJl.js +28 -0
- package/dist/assets/{runtimeConfig-iJ2mTBVN.js → runtimeConfig-AZn89Pi3.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-ISnhAS3q.js → runtimeEndpointReset-B86-1ID-.js} +1 -1
- package/dist/assets/sessionChangeSummary-BG7l2heP.js +2 -0
- package/dist/assets/{sessionLookup-DQQ9-DuP.js → sessionLookup-DqMZlsfX.js} +1 -1
- package/dist/assets/src-CnLJ1J1M.css +2 -0
- package/dist/assets/{useAppFontEffects--IcU7jyv.js → useAppFontEffects-DgMhWd3K.js} +5 -5
- package/dist/assets/{useDesktopWindowControlsLayout-BqJ5R_T2.js → useDesktopWindowControlsLayout-CEs-_qs8.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-ChWXV8xM.js → useEffectiveDirectory-LPMarhQP.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-DQkgxuzg.js → useMobileNavigationStore-C02t_wmj.js} +2 -2
- package/dist/assets/{useSessionAutoCleanup-DWWAvQQ0.js → useSessionAutoCleanup-DdXvH0A6.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-D2oQszsk.js → useWorkerHighlightedLines-4HwUoeTn.js} +1 -1
- package/dist/index.html +3 -3
- package/dist/mini-chat.html +3 -3
- package/dist/mobile.html +3 -3
- package/package.json +1 -1
- package/server/lib/assistants/DOCUMENTATION.md +33 -12
- package/server/lib/assistants/contact-store.js +25 -1
- package/server/lib/assistants/contact-store.test.js +39 -5
- package/server/lib/assistants/contact-tools.js +3 -3
- package/server/lib/assistants/harness.js +136 -101
- package/server/lib/assistants/harness.test.js +235 -8
- package/server/lib/assistants/pi-tools.js +2 -0
- package/server/lib/assistants/pi-tools.test.js +8 -0
- package/server/lib/assistants/service.js +313 -74
- package/server/lib/assistants/service.test.js +555 -18
- package/server/lib/llm/DOCUMENTATION.md +9 -4
- package/server/lib/llm/generate.js +45 -6
- package/server/lib/llm/generate.test.js +52 -0
- package/dist/assets/AssistantView-Dncbu_Z2.js +0 -2
- package/dist/assets/MarkdownRenderer-CZqok-Cf.js +0 -2
- package/dist/assets/MobileDetailNavigation-Ca-d5eZ9.js +0 -1
- package/dist/assets/MobileSurface-xL5Jby4U.js +0 -2
- package/dist/assets/renderMobileApp-BP_hGwHP.js +0 -28
- package/dist/assets/sessionChangeSummary-BAzU2J1H.js +0 -2
- package/dist/assets/src-DYeppLfP.css +0 -2
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
createActiveContactTurn,
|
|
18
18
|
deleteContactMessages,
|
|
19
19
|
deleteContactReadState,
|
|
20
|
+
deleteContactTurnAssistantOutputs,
|
|
20
21
|
ensureContactSchema,
|
|
21
22
|
contactPartsFingerprint,
|
|
22
23
|
getContactMessage,
|
|
@@ -140,10 +141,11 @@ const promptAdmitted = (result) => !result?.error && (result?.response?.status =
|
|
|
140
141
|
export const CONTACT_CATALOG_DEADLINE_MS = 8_000;
|
|
141
142
|
/** Bound last worker assistant text injected into assigned-session resume prompts. */
|
|
142
143
|
export const ASSIGNED_SESSION_RESUME_WORKER_TEXT_MAX = 2_000;
|
|
143
|
-
/** Stable resume turn/message id for assigned-session complete/error continuation. */
|
|
144
|
+
/** Stable resume turn/message id for assigned-session complete/error/cancelled continuation. */
|
|
144
145
|
export const assignedSessionResumeMessageID = (assistantID, sessionID, status, updatedAt) => (
|
|
145
146
|
`resume_${assistantID}_${sessionID}_${status}_${updatedAt}`
|
|
146
147
|
);
|
|
148
|
+
const assignedSessionResumeStatus = (status) => status === 'complete' || status === 'error' || status === 'cancelled';
|
|
147
149
|
const awaitWithDeadline = async (work, ms = CONTACT_CATALOG_DEADLINE_MS, code = 'upstream_error') => {
|
|
148
150
|
let timer = null;
|
|
149
151
|
try {
|
|
@@ -492,13 +494,35 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
492
494
|
// Last UI locale seen from a client per assistant. Background notifications
|
|
493
495
|
// (assigned-session settle) have no request locale, so they recall this value.
|
|
494
496
|
const contactTurnLanguages = new Map();
|
|
495
|
-
const cancelAssignedResumes = ({ sessionID, assistantID }) => {
|
|
497
|
+
const cancelAssignedResumes = ({ sessionID, assistantID, statuses } = {}) => {
|
|
496
498
|
for (const resume of assignedResumes.values()) {
|
|
497
499
|
if (sessionID && resume.sessionID !== sessionID) continue;
|
|
498
500
|
if (assistantID && resume.assistantID !== assistantID) continue;
|
|
501
|
+
if (statuses && !statuses.includes(resume.status)) continue;
|
|
499
502
|
resume.controller.abort();
|
|
500
503
|
}
|
|
501
504
|
};
|
|
505
|
+
// Abort-request lifecycle only (stop_session). Concurrent same-session aborts share one promise.
|
|
506
|
+
// Observed SSE terminals are stashed until abort settles so resume=true cannot beat resume=false.
|
|
507
|
+
const sessionAbortInflight = new Map();
|
|
508
|
+
const abortTerminalRank = (status) => {
|
|
509
|
+
if (status === 'cancelled') return 3;
|
|
510
|
+
if (status === 'error') return 2;
|
|
511
|
+
if (status === 'complete') return 1;
|
|
512
|
+
return 0;
|
|
513
|
+
};
|
|
514
|
+
const stashSessionAbortTerminal = (entry, status, resume) => {
|
|
515
|
+
if (!entry || typeof status !== 'string' || !status) return;
|
|
516
|
+
const nextRank = abortTerminalRank(status);
|
|
517
|
+
if (nextRank <= 0) return;
|
|
518
|
+
const prev = entry.observed;
|
|
519
|
+
const prevRank = prev ? abortTerminalRank(prev.status) : -1;
|
|
520
|
+
// cancelled wins over complete/error; error wins over complete; equal rank keeps latest.
|
|
521
|
+
if (nextRank > prevRank || (nextRank === prevRank && (status === 'cancelled' || nextRank > 0))) {
|
|
522
|
+
entry.observed = { status, resume: resume !== false };
|
|
523
|
+
if (status === 'cancelled') entry.observed.resume = resume !== false;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
502
526
|
const settleStatusFromEvent = (payload, properties) => {
|
|
503
527
|
if (payload.type === 'session.error') return isUserAbort(properties.error) ? 'cancelled' : 'error';
|
|
504
528
|
if (payload.type === 'question.asked' || payload.type === 'permission.asked') return 'question';
|
|
@@ -510,11 +534,20 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
510
534
|
}
|
|
511
535
|
return null;
|
|
512
536
|
};
|
|
513
|
-
// Assigned-session complete/error resumes the contact LLM (no canned settle bubble).
|
|
537
|
+
// Assigned-session complete/error/cancelled resumes the contact LLM (no canned settle bubble).
|
|
514
538
|
// Assigned after inContactTurnLane exists; reportAssignedSession only schedules via this ref.
|
|
515
539
|
const assignedSessionResumeRef = { schedule: null };
|
|
516
|
-
const reportAssignedSession = (sessionID, status) => {
|
|
517
|
-
|
|
540
|
+
const reportAssignedSession = (sessionID, status, resume = true) => {
|
|
541
|
+
const inflight = sessionAbortInflight.get(sessionID);
|
|
542
|
+
if (inflight) {
|
|
543
|
+
// Defer watch terminal + continuation until stop_session abort settles.
|
|
544
|
+
stashSessionAbortTerminal(inflight, status, resume);
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
// Drop complete/error notifies for this worker. Do not abort a cancelled
|
|
548
|
+
// interrupt notify — duplicate cancelled reports (boot+timer, error+message.updated)
|
|
549
|
+
// must still reach the contact LLM.
|
|
550
|
+
if (status === 'cancelled') cancelAssignedResumes({ sessionID, statuses: ['complete', 'error'] });
|
|
518
551
|
const watches = listWatchesBySession(db, sessionID);
|
|
519
552
|
if (watches.length === 0) return false;
|
|
520
553
|
let changed = false;
|
|
@@ -535,9 +568,10 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
535
568
|
updatedAt,
|
|
536
569
|
resumeAllowed: watch.resumeAllowed,
|
|
537
570
|
});
|
|
538
|
-
// complete/error: hand result back to contact LLM.
|
|
571
|
+
// complete/error/user-interrupt cancelled: hand result back to contact LLM.
|
|
572
|
+
// question: card only (worker waiting). stop_session passes resume=false.
|
|
539
573
|
// Never insert oc.settle.* canned transcript bubbles.
|
|
540
|
-
if (watch.resumeAllowed && (status
|
|
574
|
+
if (resume && watch.resumeAllowed && assignedSessionResumeStatus(status)) {
|
|
541
575
|
resumes.push({
|
|
542
576
|
assistantID: watch.assistantID,
|
|
543
577
|
sessionID,
|
|
@@ -878,7 +912,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
878
912
|
if (parts.length === 0 && userText) parts.push({ type: 'text', text: userText });
|
|
879
913
|
return parts;
|
|
880
914
|
};
|
|
881
|
-
/** Admit the user row only — HTTP 202 returns here; assistant bubbles persist
|
|
915
|
+
/** Admit the user row only — HTTP 202 returns here; assistant bubbles persist as they publish. */
|
|
882
916
|
const admitContactUser = (assistantID, { userMessageID, userText, userParts, turnID }) => {
|
|
883
917
|
db.exec('BEGIN IMMEDIATE');
|
|
884
918
|
try {
|
|
@@ -925,13 +959,135 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
925
959
|
throw writeError;
|
|
926
960
|
}
|
|
927
961
|
};
|
|
928
|
-
|
|
929
|
-
const
|
|
962
|
+
const contactBubbleMessageID = (userMessageID, bubbleIndex) => `${userMessageID}:bubble:${bubbleIndex + 1}`;
|
|
963
|
+
const contactCardMessageID = (userMessageID, index) => `${userMessageID}:card:${index + 1}`;
|
|
964
|
+
|
|
965
|
+
const persistPublishedContactBubble = (assistantID, { userMessageID, turnID, bubbleIndex, text }) => {
|
|
966
|
+
const trimmed = typeof text === 'string' ? text.trim() : '';
|
|
967
|
+
if (!trimmed || trimmed.startsWith('oc.settle.')) return false;
|
|
968
|
+
const bubbleID = contactBubbleMessageID(userMessageID, bubbleIndex);
|
|
969
|
+
if (getContactMessage(db, bubbleID)) return false;
|
|
970
|
+
db.exec('BEGIN IMMEDIATE');
|
|
971
|
+
try {
|
|
972
|
+
if (getContactMessage(db, bubbleID)) {
|
|
973
|
+
db.exec('COMMIT');
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
insertContactMessage(db, {
|
|
977
|
+
messageID: bubbleID,
|
|
978
|
+
assistantID,
|
|
979
|
+
role: 'assistant',
|
|
980
|
+
turnID,
|
|
981
|
+
bubbleIndex,
|
|
982
|
+
createdAt: now(),
|
|
983
|
+
ordinal: nextContactOrdinal(db, assistantID),
|
|
984
|
+
status: 'complete',
|
|
985
|
+
parts: [{ type: 'text', text: trimmed }],
|
|
986
|
+
});
|
|
987
|
+
bump();
|
|
988
|
+
db.exec('COMMIT');
|
|
989
|
+
return true;
|
|
990
|
+
} catch (error) {
|
|
991
|
+
db.exec('ROLLBACK');
|
|
992
|
+
throw error;
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
const persistPublishedContactCard = (assistantID, { userMessageID, turnID, index, card: cardInput }) => {
|
|
997
|
+
const card = parseContactCard({ type: 'card', cardType: cardInput?.cardType || 'session', ...cardInput });
|
|
998
|
+
if (!card) return false;
|
|
999
|
+
const cardID = contactCardMessageID(userMessageID, index);
|
|
1000
|
+
if (getContactMessage(db, cardID)) return false;
|
|
1001
|
+
db.exec('BEGIN IMMEDIATE');
|
|
1002
|
+
try {
|
|
1003
|
+
if (getContactMessage(db, cardID)) {
|
|
1004
|
+
db.exec('COMMIT');
|
|
1005
|
+
return false;
|
|
1006
|
+
}
|
|
1007
|
+
insertContactMessage(db, {
|
|
1008
|
+
messageID: cardID,
|
|
1009
|
+
assistantID,
|
|
1010
|
+
role: 'assistant',
|
|
1011
|
+
turnID,
|
|
1012
|
+
bubbleIndex: 0,
|
|
1013
|
+
createdAt: now(),
|
|
1014
|
+
ordinal: nextContactOrdinal(db, assistantID),
|
|
1015
|
+
status: 'complete',
|
|
1016
|
+
parts: [card],
|
|
1017
|
+
});
|
|
1018
|
+
if (card.cardType === 'session' && card.sessionID) {
|
|
1019
|
+
upsertContactWatch(db, {
|
|
1020
|
+
assistantID,
|
|
1021
|
+
sessionID: card.sessionID,
|
|
1022
|
+
directory: card.directory,
|
|
1023
|
+
status: card.status === 'error' || card.status === 'question' || card.status === 'complete' ? card.status : 'busy',
|
|
1024
|
+
updatedAt: now(),
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
bump();
|
|
1028
|
+
db.exec('COMMIT');
|
|
1029
|
+
return true;
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
db.exec('ROLLBACK');
|
|
1032
|
+
throw error;
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
const publishContactBubbleDelta = ({
|
|
1037
|
+
assistantID, turnID, userMessageID, spokenByIndex, bubbleIndex, delta, done, signal,
|
|
1038
|
+
}) => {
|
|
1039
|
+
if (signal?.aborted) return;
|
|
1040
|
+
const chunk = typeof delta === 'string' ? delta : '';
|
|
1041
|
+
if (chunk) spokenByIndex.set(bubbleIndex, `${spokenByIndex.get(bubbleIndex) || ''}${chunk}`);
|
|
1042
|
+
emitContactTurnEvent('openchamber:contact-bubble-delta', {
|
|
1043
|
+
assistantID,
|
|
1044
|
+
turnID,
|
|
1045
|
+
bubbleIndex,
|
|
1046
|
+
delta: chunk,
|
|
1047
|
+
done: Boolean(done),
|
|
1048
|
+
});
|
|
1049
|
+
if (!done) return;
|
|
1050
|
+
try {
|
|
1051
|
+
persistPublishedContactBubble(assistantID, {
|
|
1052
|
+
userMessageID,
|
|
1053
|
+
turnID,
|
|
1054
|
+
bubbleIndex,
|
|
1055
|
+
text: spokenByIndex.get(bubbleIndex) || '',
|
|
1056
|
+
});
|
|
1057
|
+
} catch {
|
|
1058
|
+
// Publishing a spoken bubble must not abort the live turn.
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
const rememberPublishedContactCard = (assistantID, { userMessageID, turnID, assignedCards, card }) => {
|
|
1063
|
+
assignedCards.push(card);
|
|
1064
|
+
try {
|
|
1065
|
+
persistPublishedContactCard(assistantID, {
|
|
1066
|
+
userMessageID,
|
|
1067
|
+
turnID,
|
|
1068
|
+
index: assignedCards.length - 1,
|
|
1069
|
+
card,
|
|
1070
|
+
});
|
|
1071
|
+
} catch {
|
|
1072
|
+
// Publishing a card must not abort the live turn.
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
/** Persist remaining assistant bubbles/cards for an already-admitted user turn (no user row). */
|
|
1077
|
+
const persistContactAssistantReply = (assistantID, {
|
|
1078
|
+
userMessageID, bubbles, cards = [], turnID, reset = false,
|
|
1079
|
+
}) => {
|
|
930
1080
|
db.exec('BEGIN IMMEDIATE');
|
|
931
1081
|
try {
|
|
1082
|
+
// Successful new_conversation / clear_chat_history: drop this turn's pre-reset
|
|
1083
|
+
// spoken/card rows so the unique confirm can own bubble:1 (same id is not skipped).
|
|
1084
|
+
if (reset) {
|
|
1085
|
+
deleteContactTurnAssistantOutputs(db, assistantID, turnID);
|
|
1086
|
+
}
|
|
932
1087
|
let ordinal = nextContactOrdinal(db, assistantID);
|
|
933
1088
|
bubbles.forEach((text, index) => {
|
|
934
|
-
const bubbleID =
|
|
1089
|
+
const bubbleID = contactBubbleMessageID(userMessageID, index);
|
|
1090
|
+
if (getContactMessage(db, bubbleID)) return;
|
|
935
1091
|
insertContactMessage(db, {
|
|
936
1092
|
messageID: bubbleID,
|
|
937
1093
|
assistantID,
|
|
@@ -948,8 +1104,10 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
948
1104
|
cards.forEach((cardInput, index) => {
|
|
949
1105
|
const card = parseContactCard({ type: 'card', cardType: cardInput?.cardType || 'session', ...cardInput });
|
|
950
1106
|
if (!card) return;
|
|
1107
|
+
const cardID = contactCardMessageID(userMessageID, index);
|
|
1108
|
+
if (getContactMessage(db, cardID)) return;
|
|
951
1109
|
insertContactMessage(db, {
|
|
952
|
-
messageID:
|
|
1110
|
+
messageID: cardID,
|
|
953
1111
|
assistantID,
|
|
954
1112
|
role: 'assistant',
|
|
955
1113
|
turnID,
|
|
@@ -1536,42 +1694,90 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1536
1694
|
});
|
|
1537
1695
|
const signal = params.signal;
|
|
1538
1696
|
signal?.throwIfAborted();
|
|
1539
|
-
const sdkOptions = signal ? { signal } : undefined;
|
|
1540
1697
|
if (typeof client().session.abort !== 'function') {
|
|
1541
1698
|
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Session abort is unavailable.');
|
|
1542
1699
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
if (isMissing(result)) {
|
|
1558
|
-
throw new AssignError(ASSIGN_CODES.NOT_FOUND, `No coding session ${resolved.sessionID} was found.`);
|
|
1700
|
+
const targetID = resolved.sessionID;
|
|
1701
|
+
// Share one abort lifecycle per worker session (register before await abort).
|
|
1702
|
+
const existing = sessionAbortInflight.get(targetID);
|
|
1703
|
+
if (existing?.promise) {
|
|
1704
|
+
const shared = await existing.promise;
|
|
1705
|
+
if (shared?.ok) {
|
|
1706
|
+
return {
|
|
1707
|
+
sessionID: targetID,
|
|
1708
|
+
directory: resolved.directory,
|
|
1709
|
+
title: resolved.title,
|
|
1710
|
+
aborted: true,
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
throw shared?.error || new AssignError(ASSIGN_CODES.UPSTREAM, 'Failed to abort that coding session.');
|
|
1559
1714
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1715
|
+
const entry = { sessionID: targetID, observed: null, promise: null };
|
|
1716
|
+
sessionAbortInflight.set(targetID, entry);
|
|
1717
|
+
const failAbort = (code, message) => new AssignError(
|
|
1718
|
+
code,
|
|
1719
|
+
typeof message === 'string' && message.trim() ? message.trim() : 'Failed to abort that coding session.',
|
|
1720
|
+
);
|
|
1721
|
+
entry.promise = (async () => {
|
|
1722
|
+
const sdkOptions = signal ? { signal } : undefined;
|
|
1723
|
+
let result;
|
|
1724
|
+
try {
|
|
1725
|
+
signal?.throwIfAborted();
|
|
1726
|
+
result = await client().session.abort({
|
|
1727
|
+
sessionID: targetID,
|
|
1728
|
+
directory: resolved.directory,
|
|
1729
|
+
}, sdkOptions);
|
|
1730
|
+
} catch (error) {
|
|
1731
|
+
const observed = entry.observed;
|
|
1732
|
+
if (sessionAbortInflight.get(targetID) === entry) sessionAbortInflight.delete(targetID);
|
|
1733
|
+
if (observed) reportAssignedSession(targetID, observed.status, observed.resume !== false);
|
|
1734
|
+
const wrapped = error?.name === 'AbortError' || signal?.aborted
|
|
1735
|
+
? error
|
|
1736
|
+
: failAbort(
|
|
1737
|
+
ASSIGN_CODES.UPSTREAM,
|
|
1738
|
+
typeof error?.message === 'string' ? error.message : '',
|
|
1739
|
+
);
|
|
1740
|
+
return { ok: false, error: wrapped };
|
|
1741
|
+
}
|
|
1742
|
+
if (isMissing(result)) {
|
|
1743
|
+
const observed = entry.observed;
|
|
1744
|
+
if (sessionAbortInflight.get(targetID) === entry) sessionAbortInflight.delete(targetID);
|
|
1745
|
+
if (observed) reportAssignedSession(targetID, observed.status, observed.resume !== false);
|
|
1746
|
+
return { ok: false, error: failAbort(ASSIGN_CODES.NOT_FOUND, `No coding session ${targetID} was found.`) };
|
|
1747
|
+
}
|
|
1748
|
+
if (result?.error || result?.data === false) {
|
|
1749
|
+
const observed = entry.observed;
|
|
1750
|
+
if (sessionAbortInflight.get(targetID) === entry) sessionAbortInflight.delete(targetID);
|
|
1751
|
+
if (observed) reportAssignedSession(targetID, observed.status, observed.resume !== false);
|
|
1752
|
+
return {
|
|
1753
|
+
ok: false,
|
|
1754
|
+
error: failAbort(
|
|
1755
|
+
ASSIGN_CODES.UPSTREAM,
|
|
1756
|
+
typeof result.error?.message === 'string' ? result.error.message : '',
|
|
1757
|
+
),
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
// Success (including silent abort with no SSE): release map first so reporter applies.
|
|
1761
|
+
if (sessionAbortInflight.get(targetID) === entry) sessionAbortInflight.delete(targetID);
|
|
1762
|
+
cancelAssignedResumes({ sessionID: targetID });
|
|
1763
|
+
// Assistant already owns this user turn; do not schedule a second cancelled notify.
|
|
1764
|
+
reportAssignedSession(targetID, 'cancelled', false);
|
|
1765
|
+
return { ok: true };
|
|
1766
|
+
})();
|
|
1767
|
+
try {
|
|
1768
|
+
const outcome = await entry.promise;
|
|
1769
|
+
if (outcome?.ok) {
|
|
1770
|
+
return {
|
|
1771
|
+
sessionID: targetID,
|
|
1772
|
+
directory: resolved.directory,
|
|
1773
|
+
title: resolved.title,
|
|
1774
|
+
aborted: true,
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
throw outcome?.error || failAbort(ASSIGN_CODES.UPSTREAM, '');
|
|
1778
|
+
} finally {
|
|
1779
|
+
if (sessionAbortInflight.get(targetID) === entry) sessionAbortInflight.delete(targetID);
|
|
1567
1780
|
}
|
|
1568
|
-
reportAssignedSession(resolved.sessionID, 'cancelled');
|
|
1569
|
-
return {
|
|
1570
|
-
sessionID: resolved.sessionID,
|
|
1571
|
-
directory: resolved.directory,
|
|
1572
|
-
title: resolved.title,
|
|
1573
|
-
aborted: true,
|
|
1574
|
-
};
|
|
1575
1781
|
};
|
|
1576
1782
|
// Resolve scope from the authoritative session, never model-supplied directories.
|
|
1577
1783
|
const mutateSessionWork = async (operation, params = {}) => {
|
|
@@ -1877,6 +2083,12 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1877
2083
|
`sessionID: ${sessionID}`,
|
|
1878
2084
|
`status: ${status}`,
|
|
1879
2085
|
];
|
|
2086
|
+
if (status === 'cancelled') {
|
|
2087
|
+
lines.push(
|
|
2088
|
+
'reason: user_interrupted',
|
|
2089
|
+
'The user manually interrupted this assigned worker session (OpenCode session.abort / MessageAbortedError). This is not a model failure and not an assistant-initiated stop_session.',
|
|
2090
|
+
);
|
|
2091
|
+
}
|
|
1880
2092
|
if (title) lines.push(`cardTitle: ${title}`);
|
|
1881
2093
|
if (workerText) {
|
|
1882
2094
|
lines.push('lastWorkerAssistantText (bounded):');
|
|
@@ -1886,20 +2098,28 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1886
2098
|
}
|
|
1887
2099
|
lines.push(
|
|
1888
2100
|
'Instructions:',
|
|
1889
|
-
'- Never tell the user canned phrases like "会话已完成" / "Session finished" / "会话失败" / "Session failed".',
|
|
2101
|
+
'- Never tell the user canned phrases like "会话已完成" / "Session finished" / "会话失败" / "Session failed" / "会话已取消" / "Session cancelled".',
|
|
1890
2102
|
'- This is a read-only result notification, never authorization to create, restart, continue, steer, or otherwise mutate any session or file.',
|
|
1891
2103
|
'- Respect the latest user instructions in history, including stop/cancel. If work remains, report it and wait for an explicit user request.',
|
|
1892
|
-
'- If no further work is needed, reply in one or two short sentences in the user\'s language summarizing the result or failure reason.',
|
|
1893
2104
|
);
|
|
2105
|
+
if (status === 'cancelled') {
|
|
2106
|
+
lines.push(
|
|
2107
|
+
'- The user interrupted this worker themselves. In the user\'s language, acknowledge that interruption and decide the next spoken step (usually stop here and wait). Do not restart the worker unless history already asked you to.',
|
|
2108
|
+
);
|
|
2109
|
+
} else {
|
|
2110
|
+
lines.push(
|
|
2111
|
+
'- If no further work is needed, reply in one or two short sentences in the user\'s language summarizing the result or failure reason.',
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
1894
2114
|
return lines.join('\n');
|
|
1895
2115
|
};
|
|
1896
2116
|
/**
|
|
1897
|
-
* After assigned worker complete/error: async contact-lane continuation.
|
|
2117
|
+
* After assigned worker complete/error/cancelled: async contact-lane continuation.
|
|
1898
2118
|
* No user transcript row — only internal runContactTurn userText + assistant bubbles/cards.
|
|
1899
2119
|
*/
|
|
1900
2120
|
const scheduleAssignedSessionResume = ({ assistantID, sessionID, status, directory, updatedAt }) => {
|
|
1901
2121
|
if (closed) return;
|
|
1902
|
-
if (status
|
|
2122
|
+
if (!assignedSessionResumeStatus(status)) return;
|
|
1903
2123
|
const row = assistant(assistantID);
|
|
1904
2124
|
if (!row || row.tombstone_at) return;
|
|
1905
2125
|
const messageID = assignedSessionResumeMessageID(assistantID, sessionID, status, updatedAt);
|
|
@@ -1912,7 +2132,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1912
2132
|
|
|
1913
2133
|
if (assignedResumes.has(messageID)) return;
|
|
1914
2134
|
const controller = new AbortController();
|
|
1915
|
-
assignedResumes.set(messageID, { assistantID, sessionID, controller });
|
|
2135
|
+
assignedResumes.set(messageID, { assistantID, sessionID, status, controller });
|
|
1916
2136
|
const finishCancelled = () => {
|
|
1917
2137
|
settleActiveContactTurn(assistantID, turnID, { bumpRevision: true });
|
|
1918
2138
|
emitContactTurnEvent('openchamber:contact-turn-end', { assistantID, turnID, status: 'cancelled' });
|
|
@@ -1985,7 +2205,9 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1985
2205
|
const rows = Array.isArray(messagesResult?.data)
|
|
1986
2206
|
? messagesResult.data
|
|
1987
2207
|
: (Array.isArray(messagesResult) ? messagesResult : []);
|
|
1988
|
-
|
|
2208
|
+
// complete/error that discovers a user abort becomes cancelled + notify.
|
|
2209
|
+
// A cancelled resume must still run with reason: user_interrupted.
|
|
2210
|
+
if (status !== 'cancelled' && isUserAbort(lastAssistantInfo(rows)?.error)) {
|
|
1989
2211
|
reportAssignedSession(sessionID, 'cancelled');
|
|
1990
2212
|
finishCancelled();
|
|
1991
2213
|
return;
|
|
@@ -2006,9 +2228,16 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2006
2228
|
workerText,
|
|
2007
2229
|
});
|
|
2008
2230
|
const history = contactHistoryForLlm(db, assistantID, {});
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2231
|
+
const assignedCards = [];
|
|
2232
|
+
const spokenByIndex = new Map();
|
|
2233
|
+
let contactResetThisTurn = false;
|
|
2234
|
+
let contactHistoryClearedThisTurn = false;
|
|
2235
|
+
const markContactReset = (historyCleared = false) => {
|
|
2236
|
+
contactResetThisTurn = true;
|
|
2237
|
+
if (historyCleared) contactHistoryClearedThisTurn = true;
|
|
2238
|
+
spokenByIndex.clear();
|
|
2239
|
+
assignedCards.length = 0;
|
|
2240
|
+
};
|
|
2012
2241
|
|
|
2013
2242
|
try {
|
|
2014
2243
|
const executionHistory = await materializeContactHistory(
|
|
@@ -2030,13 +2259,12 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2030
2259
|
deliverPeerMessage: (toolInput) => deliverPeerMessage(assistantID, toolInput),
|
|
2031
2260
|
clearContactMemory: () => {
|
|
2032
2261
|
const result = clearContactMemory(assistantID, {});
|
|
2033
|
-
|
|
2262
|
+
markContactReset(false);
|
|
2034
2263
|
return result;
|
|
2035
2264
|
},
|
|
2036
2265
|
resetContact: () => {
|
|
2037
2266
|
const result = resetContact(assistantID, {});
|
|
2038
|
-
|
|
2039
|
-
contactHistoryClearedThisTurn = true;
|
|
2267
|
+
markContactReset(true);
|
|
2040
2268
|
return result;
|
|
2041
2269
|
},
|
|
2042
2270
|
listAssistants: () => db.prepare('SELECT * FROM assistant_v2 WHERE tombstone_at IS NULL ORDER BY created_at').all().map(output),
|
|
@@ -2092,7 +2320,12 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2092
2320
|
throw lastError;
|
|
2093
2321
|
},
|
|
2094
2322
|
currentAssistant: assistantSnapshot,
|
|
2095
|
-
onCard: (card) =>
|
|
2323
|
+
onCard: (card) => {
|
|
2324
|
+
if (contactResetThisTurn) return;
|
|
2325
|
+
rememberPublishedContactCard(assistantID, {
|
|
2326
|
+
userMessageID: messageID, turnID, assignedCards, card,
|
|
2327
|
+
});
|
|
2328
|
+
},
|
|
2096
2329
|
turnFileParts: [],
|
|
2097
2330
|
turnAttachmentScope: attachmentScopeKey([]),
|
|
2098
2331
|
});
|
|
@@ -2111,13 +2344,9 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2111
2344
|
...modelContext,
|
|
2112
2345
|
globalEventHub,
|
|
2113
2346
|
onBubbleDelta: (bubbleIndex, delta, done) => {
|
|
2114
|
-
if (
|
|
2115
|
-
|
|
2116
|
-
assistantID,
|
|
2117
|
-
turnID,
|
|
2118
|
-
bubbleIndex,
|
|
2119
|
-
delta: typeof delta === 'string' ? delta : '',
|
|
2120
|
-
done: Boolean(done),
|
|
2347
|
+
if (contactResetThisTurn) return;
|
|
2348
|
+
publishContactBubbleDelta({
|
|
2349
|
+
assistantID, turnID, userMessageID: messageID, spokenByIndex, bubbleIndex, delta, done, signal: controller.signal,
|
|
2121
2350
|
});
|
|
2122
2351
|
},
|
|
2123
2352
|
});
|
|
@@ -2177,6 +2406,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2177
2406
|
bubbles: bubbles.length > 0 ? bubbles : [ASSIGNED_SESSION_FALLBACK_BUBBLE],
|
|
2178
2407
|
cards,
|
|
2179
2408
|
turnID,
|
|
2409
|
+
reset: resetThisTurn,
|
|
2180
2410
|
});
|
|
2181
2411
|
settleActiveContactTurn(assistantID, turnID);
|
|
2182
2412
|
emitContactTurnEvent('openchamber:contact-turn-end', {
|
|
@@ -2324,8 +2554,15 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2324
2554
|
beforeOrdinal,
|
|
2325
2555
|
});
|
|
2326
2556
|
const assignedCards = [];
|
|
2557
|
+
const spokenByIndex = new Map();
|
|
2327
2558
|
let contactResetThisTurn = false;
|
|
2328
2559
|
let contactHistoryClearedThisTurn = false;
|
|
2560
|
+
const markContactReset = (historyCleared = false) => {
|
|
2561
|
+
contactResetThisTurn = true;
|
|
2562
|
+
if (historyCleared) contactHistoryClearedThisTurn = true;
|
|
2563
|
+
spokenByIndex.clear();
|
|
2564
|
+
assignedCards.length = 0;
|
|
2565
|
+
};
|
|
2329
2566
|
const modelContext = await loadContactModelContext(controller.signal);
|
|
2330
2567
|
try {
|
|
2331
2568
|
// Materialize inside turn try so failures durable-error + settle working.
|
|
@@ -2365,7 +2602,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2365
2602
|
const result = clearContactMemory(row.assistant_id, {
|
|
2366
2603
|
upToOrdinal: beforeOrdinal,
|
|
2367
2604
|
});
|
|
2368
|
-
|
|
2605
|
+
markContactReset(false);
|
|
2369
2606
|
return result;
|
|
2370
2607
|
},
|
|
2371
2608
|
resetContact: () => {
|
|
@@ -2374,8 +2611,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2374
2611
|
const result = resetContact(row.assistant_id, {
|
|
2375
2612
|
upToOrdinal: beforeOrdinal,
|
|
2376
2613
|
});
|
|
2377
|
-
|
|
2378
|
-
contactHistoryClearedThisTurn = true;
|
|
2614
|
+
markContactReset(true);
|
|
2379
2615
|
return result;
|
|
2380
2616
|
},
|
|
2381
2617
|
listAssistants: () => db.prepare('SELECT * FROM assistant_v2 WHERE tombstone_at IS NULL ORDER BY created_at').all().map(output),
|
|
@@ -2435,7 +2671,12 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2435
2671
|
throw lastError;
|
|
2436
2672
|
},
|
|
2437
2673
|
currentAssistant: assistantSnapshot,
|
|
2438
|
-
onCard: (card) =>
|
|
2674
|
+
onCard: (card) => {
|
|
2675
|
+
if (contactResetThisTurn) return;
|
|
2676
|
+
rememberPublishedContactCard(row.assistant_id, {
|
|
2677
|
+
userMessageID: messageID, turnID, assignedCards, card,
|
|
2678
|
+
});
|
|
2679
|
+
},
|
|
2439
2680
|
turnFileParts,
|
|
2440
2681
|
turnAttachmentScope,
|
|
2441
2682
|
});
|
|
@@ -2453,13 +2694,9 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2453
2694
|
...modelContext,
|
|
2454
2695
|
globalEventHub,
|
|
2455
2696
|
onBubbleDelta: (bubbleIndex, delta, done) => {
|
|
2456
|
-
if (
|
|
2457
|
-
|
|
2458
|
-
assistantID: row.assistant_id,
|
|
2459
|
-
turnID,
|
|
2460
|
-
bubbleIndex,
|
|
2461
|
-
delta: typeof delta === 'string' ? delta : '',
|
|
2462
|
-
done: Boolean(done),
|
|
2697
|
+
if (contactResetThisTurn) return;
|
|
2698
|
+
publishContactBubbleDelta({
|
|
2699
|
+
assistantID: row.assistant_id, turnID, userMessageID: messageID, spokenByIndex, bubbleIndex, delta, done, signal: controller.signal,
|
|
2463
2700
|
});
|
|
2464
2701
|
},
|
|
2465
2702
|
});
|
|
@@ -2532,6 +2769,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2532
2769
|
bubbles: bubbles.length > 0 ? bubbles : [ASSIGNED_SESSION_FALLBACK_BUBBLE],
|
|
2533
2770
|
cards,
|
|
2534
2771
|
turnID,
|
|
2772
|
+
reset: resetThisTurn,
|
|
2535
2773
|
});
|
|
2536
2774
|
// persistContactAssistantReply already bumps; clear activity without a second tip.
|
|
2537
2775
|
settleActiveContactTurn(row.assistant_id, turnID);
|
|
@@ -2953,6 +3191,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2953
3191
|
}
|
|
2954
3192
|
}, ensure, createNew, compact, send, whenContactTurnSettled, abort, captureQueueDeliveryTarget, sendWithCapturedConfig, share, shareOperation, historicalMessages, contactMessages, clearContactMemory, resetContact, markContactRead, appendContactCard, deliverPeerMessage, listAssistantScheduledTasks, putAssistantContactAttachment, getAssistantContactAttachment, migrateContactAttachments, processEvent, reportAssignedSessionSettle: reportAssignedSession, reconcile, close: () => {
|
|
2955
3193
|
cancelAssignedResumes({});
|
|
3194
|
+
sessionAbortInflight.clear();
|
|
2956
3195
|
for (const turn of contactControllers.values()) turn.controller.abort();
|
|
2957
3196
|
if (!closed) {
|
|
2958
3197
|
closed = true;
|