@openchambery/web 1.19.13-beta.2 → 1.19.13-beta.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/dist/assets/AssistantView-4rOhnCbj.js +2 -0
- package/dist/assets/{DiagramView-CjNUrHsU.js → DiagramView-qyOlOyET.js} +1 -1
- package/dist/assets/{MarkdownRenderer-BHfbAwVO.js → MarkdownRenderer-DJORbaAh.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-CkBoUibc.js → MarkdownRendererImpl-Baw1_E_b.js} +4 -1
- package/dist/assets/MarkstreamRendererImpl-C3y7cRux.js +3 -0
- package/dist/assets/{MobileDetailNavigation-aY9-HwBN.js → MobileDetailNavigation-Cy-LwJ5M.js} +1 -1
- package/dist/assets/{MobileShareBridge-CRt_8DmM.js → MobileShareBridge-CTai3NEM.js} +1 -1
- package/dist/assets/{MobileSurface-CIhtF-CU.js → MobileSurface-CYg5Flgz.js} +1 -1
- package/dist/assets/{MultiRunWindow-DDq6C8t7.js → MultiRunWindow-CnzvP7OW.js} +1 -1
- package/dist/assets/{SettingsView-DFWWMvAj.js → SettingsView-Gerbi-eT.js} +1 -1
- package/dist/assets/{SettingsWindow-CnYfBplB.js → SettingsWindow-Czkt-v7v.js} +1 -1
- package/dist/assets/{TerminalView-D5CQFVVm.js → TerminalView-C3PKUnFu.js} +1 -1
- package/dist/assets/{ToolOutputDialog-VMI7TQFT.js → ToolOutputDialog-Nv1AVjeU.js} +1 -1
- package/dist/assets/{appThemeRegistry-UIrP0-5h.js → appThemeRegistry-eohB6OfO.js} +1 -1
- package/dist/assets/{gitApi-Bu4W3bgD.js → gitApi-CrP1R39C.js} +1 -1
- package/dist/assets/{insertionBoundaries-B1AqxsBD.js → insertionBoundaries-C8jXd3NT.js} +1 -1
- package/dist/assets/{main-DG-k0j8A.js → main-Bjgj-x5l.js} +2 -2
- package/dist/assets/{main-CqcB6tKc.js → main-DZg9T7tk.js} +4 -4
- package/dist/assets/{miniChat-BQtYQrsh.js → miniChat-DUFEC4jl.js} +2 -2
- package/dist/assets/{mobile-8iyqtXkr.js → mobile-BqOSxf04.js} +2 -2
- package/dist/assets/{multirun-CG_FCb_Z.js → multirun-IUfGqjJa.js} +1 -1
- package/dist/assets/{projectMeta-DMpMDWE1.js → projectMeta-B94Ha79_.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-DBzRdJl8.js → renderElectronMiniChatApp-DwWmaqPt.js} +2 -2
- package/dist/assets/{renderMobileApp-CIseqCyn.js → renderMobileApp-ByS1wjSm.js} +4 -4
- package/dist/assets/{runtimeConfig-D8nJYB-e.js → runtimeConfig-DhAd_g4W.js} +1 -1
- package/dist/assets/runtimeEndpointReset-Dk_Xpe6y.js +147 -0
- package/dist/assets/{sessionLookup-BiBD-4wC.js → sessionLookup-DZcwnQYW.js} +1 -1
- package/dist/assets/{useAppFontEffects-qHt5uaFN.js → useAppFontEffects-BO7HWYlJ.js} +5 -5
- package/dist/assets/{useDesktopWindowControlsLayout-BipyIXiK.js → useDesktopWindowControlsLayout-BAuvqFxV.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-DSNCCbCt.js → useEffectiveDirectory-B9GzC3CE.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-C1NJGAik.js → useMobileNavigationStore-Dqe-sz0u.js} +1 -1
- package/dist/assets/{useSessionAutoCleanup-Dcn6Unvd.js → useSessionAutoCleanup-DSAkV_JD.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-CzpT4M7V.js → useWorkerHighlightedLines-BW-HxZ9_.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/mini-chat.html +2 -2
- package/dist/mobile.html +2 -2
- package/package.json +1 -1
- package/server/lib/assistants/DOCUMENTATION.md +20 -11
- package/server/lib/assistants/assign.js +56 -1
- package/server/lib/assistants/assign.test.js +60 -0
- package/server/lib/assistants/contact-tools.js +212 -1
- package/server/lib/assistants/contact-tools.test.js +151 -0
- package/server/lib/assistants/harness.js +56 -5
- package/server/lib/assistants/harness.test.js +17 -9
- package/server/lib/assistants/service.js +514 -16
- package/server/lib/assistants/service.test.js +546 -26
- package/dist/assets/AssistantView-D5c557Oe.js +0 -2
- package/dist/assets/MarkstreamRendererImpl-BrXUOI9g.js +0 -3
- package/dist/assets/runtimeEndpointReset-CSub_ecV.js +0 -147
|
@@ -10,7 +10,6 @@ import { contactCardIdentity, parseContactCard, parseContactPart } from './cards
|
|
|
10
10
|
import {
|
|
11
11
|
CONTACT_PAGE_DEFAULT_LIMIT,
|
|
12
12
|
CONTACT_PAGE_MAX_LIMIT,
|
|
13
|
-
CONTACT_SETTLE_TEXT,
|
|
14
13
|
bumpContactGeneration,
|
|
15
14
|
clearContactMemory as clearContactMemoryStore,
|
|
16
15
|
contactHistoryForLlm,
|
|
@@ -66,6 +65,7 @@ import {
|
|
|
66
65
|
attachmentScopeKey,
|
|
67
66
|
hasAssignImageParts,
|
|
68
67
|
resolveAssignDirectory,
|
|
68
|
+
extractAssignSessionModel,
|
|
69
69
|
resolveAssignWorkerModel,
|
|
70
70
|
sanitizeAssignFileParts,
|
|
71
71
|
} from './assign.js';
|
|
@@ -128,6 +128,12 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
128
128
|
const promptAdmitted = (result) => !result?.error && (result?.response?.status === 204 || result?.status === 204 || result?.data !== undefined || result?.response?.ok === true);
|
|
129
129
|
/** Bound catalog / project list waits so a contact lane cannot hang forever. */
|
|
130
130
|
export const CONTACT_CATALOG_DEADLINE_MS = 8_000;
|
|
131
|
+
/** Bound last worker assistant text injected into assigned-session resume prompts. */
|
|
132
|
+
export const ASSIGNED_SESSION_RESUME_WORKER_TEXT_MAX = 2_000;
|
|
133
|
+
/** Stable resume turn/message id for assigned-session complete/error continuation. */
|
|
134
|
+
export const assignedSessionResumeMessageID = (assistantID, sessionID, status, updatedAt) => (
|
|
135
|
+
`resume_${assistantID}_${sessionID}_${status}_${updatedAt}`
|
|
136
|
+
);
|
|
131
137
|
const awaitWithDeadline = async (work, ms = CONTACT_CATALOG_DEADLINE_MS, code = 'upstream_error') => {
|
|
132
138
|
let timer = null;
|
|
133
139
|
try {
|
|
@@ -465,37 +471,38 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
465
471
|
}
|
|
466
472
|
return null;
|
|
467
473
|
};
|
|
474
|
+
// Assigned-session complete/error resumes the contact LLM (no canned settle bubble).
|
|
475
|
+
// Assigned after inContactTurnLane exists; reportAssignedSession only schedules via this ref.
|
|
476
|
+
const assignedSessionResumeRef = { schedule: null };
|
|
468
477
|
const reportAssignedSession = (sessionID, status) => {
|
|
469
478
|
const watches = listWatchesBySession(db, sessionID);
|
|
470
479
|
if (watches.length === 0) return false;
|
|
471
480
|
let changed = false;
|
|
481
|
+
const resumes = [];
|
|
472
482
|
db.exec('BEGIN IMMEDIATE');
|
|
473
483
|
try {
|
|
474
484
|
for (const watch of watches) {
|
|
475
485
|
if (watch.status === status) continue;
|
|
476
486
|
// session.idle / status idle follow session.error. Do not rewrite 失败 as 完成.
|
|
477
487
|
if (status === 'complete' && watch.status === 'error') continue;
|
|
488
|
+
const updatedAt = now();
|
|
478
489
|
updateSessionCardStatus(db, { assistantID: watch.assistantID, sessionID, status });
|
|
479
490
|
upsertContactWatch(db, {
|
|
480
491
|
assistantID: watch.assistantID,
|
|
481
492
|
sessionID,
|
|
482
493
|
directory: watch.directory,
|
|
483
494
|
status,
|
|
484
|
-
updatedAt
|
|
495
|
+
updatedAt,
|
|
485
496
|
});
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
messageID: settleID,
|
|
497
|
+
// complete/error: hand result back to contact LLM. question: card only (worker waiting).
|
|
498
|
+
// Never insert oc.settle.* canned transcript bubbles.
|
|
499
|
+
if (status === 'complete' || status === 'error') {
|
|
500
|
+
resumes.push({
|
|
491
501
|
assistantID: watch.assistantID,
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
ordinal: nextContactOrdinal(db, watch.assistantID),
|
|
497
|
-
status: 'complete',
|
|
498
|
-
parts: [{ type: 'text', text: settleText }],
|
|
502
|
+
sessionID,
|
|
503
|
+
status,
|
|
504
|
+
directory: watch.directory,
|
|
505
|
+
updatedAt,
|
|
499
506
|
});
|
|
500
507
|
}
|
|
501
508
|
changed = true;
|
|
@@ -506,6 +513,10 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
506
513
|
db.exec('ROLLBACK');
|
|
507
514
|
throw error;
|
|
508
515
|
}
|
|
516
|
+
// processEvent stays sync; resume runs on the contact turn lane asynchronously.
|
|
517
|
+
if (typeof assignedSessionResumeRef.schedule === 'function') {
|
|
518
|
+
for (const resume of resumes) assignedSessionResumeRef.schedule(resume);
|
|
519
|
+
}
|
|
509
520
|
return changed;
|
|
510
521
|
};
|
|
511
522
|
const sessionStatusType = (session) => {
|
|
@@ -1050,6 +1061,42 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1050
1061
|
return null;
|
|
1051
1062
|
}
|
|
1052
1063
|
};
|
|
1064
|
+
/**
|
|
1065
|
+
* Best-effort previous worker model for a reused sessionID.
|
|
1066
|
+
* session.get → session.model, else newest message info.model. Never throws / never fails assign.
|
|
1067
|
+
*/
|
|
1068
|
+
const lookupReusedSessionModel = async ({ sessionID, directory, signal } = {}) => {
|
|
1069
|
+
if (typeof sessionID !== 'string' || !sessionID.trim()) return null;
|
|
1070
|
+
const api = client();
|
|
1071
|
+
const options = signal ? { signal } : undefined;
|
|
1072
|
+
const dir = typeof directory === 'string' && directory.trim() ? directory.trim() : null;
|
|
1073
|
+
try {
|
|
1074
|
+
const got = await api.session.get({
|
|
1075
|
+
sessionID,
|
|
1076
|
+
...(dir ? { directory: dir } : {}),
|
|
1077
|
+
}, options);
|
|
1078
|
+
if (got?.error) return null;
|
|
1079
|
+
const sessionPayload = got?.data ?? got;
|
|
1080
|
+
let messages = null;
|
|
1081
|
+
const fromSession = extractAssignSessionModel({ session: sessionPayload });
|
|
1082
|
+
if (fromSession) return fromSession;
|
|
1083
|
+
try {
|
|
1084
|
+
if (typeof api.session.messages !== 'function') return null;
|
|
1085
|
+
const listed = await api.session.messages({
|
|
1086
|
+
sessionID,
|
|
1087
|
+
...(dir ? { directory: dir } : {}),
|
|
1088
|
+
limit: 40,
|
|
1089
|
+
}, options);
|
|
1090
|
+
if (listed?.error) return null;
|
|
1091
|
+
messages = listed?.data ?? listed;
|
|
1092
|
+
} catch {
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
return extractAssignSessionModel({ session: sessionPayload, messages });
|
|
1096
|
+
} catch {
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1053
1100
|
const assignWork = async (row, params = {}) => {
|
|
1054
1101
|
const fileParts = sanitizeAssignFileParts(params.fileParts || params.parts || []);
|
|
1055
1102
|
const signal = params.signal;
|
|
@@ -1060,16 +1107,44 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1060
1107
|
|| (typeof params.model === 'string' && params.model.trim())
|
|
1061
1108
|
|| (params.model && typeof params.model === 'object'),
|
|
1062
1109
|
);
|
|
1063
|
-
const
|
|
1110
|
+
const reuseSessionID = typeof params.sessionID === 'string' && params.sessionID.trim()
|
|
1111
|
+
? params.sessionID.trim()
|
|
1112
|
+
: '';
|
|
1113
|
+
// Reused session without explicit model may follow the session's last model (catalog-gated).
|
|
1114
|
+
const followSessionModel = Boolean(reuseSessionID && !explicitModel);
|
|
1115
|
+
const needsCatalog = explicitModel || hasAssignImageParts(fileParts) || followSessionModel;
|
|
1064
1116
|
let catalog = null;
|
|
1117
|
+
let sessionModel = null;
|
|
1065
1118
|
if (needsCatalog) {
|
|
1066
|
-
|
|
1119
|
+
if (followSessionModel && !explicitModel && !hasAssignImageParts(fileParts)) {
|
|
1120
|
+
// Session-follow degrades on catalog failure; explicit/image stay fail-closed.
|
|
1121
|
+
try {
|
|
1122
|
+
catalog = await loadAssignCatalog(signal);
|
|
1123
|
+
} catch {
|
|
1124
|
+
catalog = null;
|
|
1125
|
+
}
|
|
1126
|
+
} else {
|
|
1127
|
+
catalog = await loadAssignCatalog(signal);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if (followSessionModel && catalog) {
|
|
1131
|
+
const directoryHint = typeof params.directory === 'string' && params.directory.trim()
|
|
1132
|
+
? params.directory.trim()
|
|
1133
|
+
: (typeof params.projectPath === 'string' && params.projectPath.trim()
|
|
1134
|
+
? params.projectPath.trim()
|
|
1135
|
+
: null);
|
|
1136
|
+
sessionModel = await lookupReusedSessionModel({
|
|
1137
|
+
sessionID: reuseSessionID,
|
|
1138
|
+
directory: directoryHint,
|
|
1139
|
+
signal,
|
|
1140
|
+
});
|
|
1067
1141
|
}
|
|
1068
1142
|
const workerModel = resolveAssignWorkerModel({
|
|
1069
1143
|
providerID: params.providerID,
|
|
1070
1144
|
modelID: params.modelID,
|
|
1071
1145
|
model: params.model,
|
|
1072
1146
|
fallback: { providerID: row.provider_id, modelID: row.model_id },
|
|
1147
|
+
sessionModel,
|
|
1073
1148
|
catalog,
|
|
1074
1149
|
});
|
|
1075
1150
|
let acceptsImages = workerModel.acceptsImages;
|
|
@@ -1338,6 +1413,377 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1338
1413
|
}),
|
|
1339
1414
|
};
|
|
1340
1415
|
};
|
|
1416
|
+
const findAssignedSessionCardTitle = (assistantID, sessionID) => {
|
|
1417
|
+
const rows = db.prepare(
|
|
1418
|
+
'SELECT p.part_json FROM assistant_contact_part p JOIN assistant_contact_message m ON m.message_id=p.message_id WHERE m.assistant_id=?',
|
|
1419
|
+
).all(assistantID);
|
|
1420
|
+
for (const row of rows) {
|
|
1421
|
+
let part;
|
|
1422
|
+
try { part = parseContactPart(parse(row.part_json)); } catch { continue; }
|
|
1423
|
+
if (part?.type === 'card' && part.cardType === 'session' && part.sessionID === sessionID) {
|
|
1424
|
+
return typeof part.title === 'string' && part.title.trim() ? part.title.trim() : null;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
return null;
|
|
1428
|
+
};
|
|
1429
|
+
const extractLastWorkerAssistantText = (messages, maxChars = ASSIGNED_SESSION_RESUME_WORKER_TEXT_MAX) => {
|
|
1430
|
+
if (!Array.isArray(messages)) return '';
|
|
1431
|
+
const limit = Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : ASSIGNED_SESSION_RESUME_WORKER_TEXT_MAX;
|
|
1432
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
1433
|
+
const entry = messages[index];
|
|
1434
|
+
const info = entry?.info ?? entry;
|
|
1435
|
+
if (info?.role !== 'assistant') continue;
|
|
1436
|
+
const parts = Array.isArray(entry?.parts)
|
|
1437
|
+
? entry.parts
|
|
1438
|
+
: (Array.isArray(info?.parts) ? info.parts : []);
|
|
1439
|
+
const chunks = [];
|
|
1440
|
+
for (const part of parts) {
|
|
1441
|
+
if (part?.type === 'text' && typeof part.text === 'string' && part.text.trim()) {
|
|
1442
|
+
chunks.push(part.text.trim());
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
if (chunks.length === 0 && typeof info?.content === 'string' && info.content.trim()) {
|
|
1446
|
+
chunks.push(info.content.trim());
|
|
1447
|
+
}
|
|
1448
|
+
if (chunks.length === 0) continue;
|
|
1449
|
+
const joined = chunks.join('\n');
|
|
1450
|
+
return joined.length > limit ? joined.slice(0, limit) : joined;
|
|
1451
|
+
}
|
|
1452
|
+
return '';
|
|
1453
|
+
};
|
|
1454
|
+
const buildAssignedSessionResumeUserText = ({ sessionID, status, title, workerText }) => {
|
|
1455
|
+
const lines = [
|
|
1456
|
+
'[Internal assigned-session resume — not a user message. Do not quote this block.]',
|
|
1457
|
+
`sessionID: ${sessionID}`,
|
|
1458
|
+
`status: ${status}`,
|
|
1459
|
+
];
|
|
1460
|
+
if (title) lines.push(`cardTitle: ${title}`);
|
|
1461
|
+
if (workerText) {
|
|
1462
|
+
lines.push('lastWorkerAssistantText (bounded):');
|
|
1463
|
+
lines.push(workerText);
|
|
1464
|
+
} else {
|
|
1465
|
+
lines.push('lastWorkerAssistantText: (unavailable)');
|
|
1466
|
+
}
|
|
1467
|
+
lines.push(
|
|
1468
|
+
'Instructions:',
|
|
1469
|
+
'- Never tell the user canned phrases like "会话已完成" / "Session finished" / "会话失败" / "Session failed".',
|
|
1470
|
+
'- If more coding work is needed, call assign_session with the SAME sessionID and omit model args so the prior worker model is reused.',
|
|
1471
|
+
'- If no further work is needed, reply in one or two short sentences in the user\'s language summarizing the result or failure reason.',
|
|
1472
|
+
);
|
|
1473
|
+
return lines.join('\n');
|
|
1474
|
+
};
|
|
1475
|
+
/**
|
|
1476
|
+
* After assigned worker complete/error: async contact-lane continuation.
|
|
1477
|
+
* No user transcript row — only internal runContactTurn userText + assistant bubbles/cards.
|
|
1478
|
+
*/
|
|
1479
|
+
const scheduleAssignedSessionResume = ({ assistantID, sessionID, status, directory, updatedAt }) => {
|
|
1480
|
+
if (closed) return;
|
|
1481
|
+
if (status !== 'complete' && status !== 'error') return;
|
|
1482
|
+
const row = assistant(assistantID);
|
|
1483
|
+
if (!row || row.tombstone_at) return;
|
|
1484
|
+
const messageID = assignedSessionResumeMessageID(assistantID, sessionID, status, updatedAt);
|
|
1485
|
+
const turnID = messageID;
|
|
1486
|
+
// Idempotent: same watch status already resumed (bubbles or durable error).
|
|
1487
|
+
if (
|
|
1488
|
+
getContactMessage(db, `${messageID}:bubble:1`)
|
|
1489
|
+
|| getContactMessage(db, `${messageID}:error`)
|
|
1490
|
+
) return;
|
|
1491
|
+
|
|
1492
|
+
rememberActiveContactTurn(assistantID, {
|
|
1493
|
+
turnID,
|
|
1494
|
+
messageID,
|
|
1495
|
+
status: 'queued',
|
|
1496
|
+
admittedAt: now(),
|
|
1497
|
+
});
|
|
1498
|
+
// Tip snapshot so list green dots / 3-dot row rehydrate without waiting for lane start.
|
|
1499
|
+
bump();
|
|
1500
|
+
emitContactTurnEvent('openchamber:contact-turn-start', {
|
|
1501
|
+
assistantID,
|
|
1502
|
+
turnID,
|
|
1503
|
+
messageID,
|
|
1504
|
+
});
|
|
1505
|
+
contactTurnSettlement(messageID);
|
|
1506
|
+
|
|
1507
|
+
void inContactTurnLane(assistantID, async () => {
|
|
1508
|
+
if (closed) {
|
|
1509
|
+
settleActiveContactTurn(assistantID, turnID, { bumpRevision: true });
|
|
1510
|
+
resolveContactTurnSettlement(messageID, { status: 'error', error: 'closed' });
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
const live = assistant(assistantID);
|
|
1514
|
+
if (!live || live.tombstone_at) {
|
|
1515
|
+
settleActiveContactTurn(assistantID, turnID, { bumpRevision: true });
|
|
1516
|
+
resolveContactTurnSettlement(messageID, { status: 'error', error: 'not_found' });
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
if (
|
|
1520
|
+
getContactMessage(db, `${messageID}:bubble:1`)
|
|
1521
|
+
|| getContactMessage(db, `${messageID}:error`)
|
|
1522
|
+
) {
|
|
1523
|
+
settleActiveContactTurn(assistantID, turnID, { bumpRevision: true });
|
|
1524
|
+
resolveContactTurnSettlement(messageID, { status: 'complete', replayed: true });
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
markActiveContactTurnRunning(assistantID, turnID);
|
|
1528
|
+
const assistantSnapshot = output(live);
|
|
1529
|
+
let registeredProjects = [];
|
|
1530
|
+
try {
|
|
1531
|
+
registeredProjects = filterRegisteredProjects(await loadRegisteredProjects());
|
|
1532
|
+
} catch {
|
|
1533
|
+
registeredProjects = [];
|
|
1534
|
+
}
|
|
1535
|
+
let connectedModels = [];
|
|
1536
|
+
try {
|
|
1537
|
+
const catalog = await loadAssignCatalog(undefined);
|
|
1538
|
+
connectedModels = normalizeConnectedModels(catalog?.models);
|
|
1539
|
+
} catch {
|
|
1540
|
+
connectedModels = [];
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
let workerText = '';
|
|
1544
|
+
try {
|
|
1545
|
+
const watchDirectory = nonEmptyString(directory)
|
|
1546
|
+
? (() => {
|
|
1547
|
+
try { return workspace(directory, assistantID); } catch { return null; }
|
|
1548
|
+
})()
|
|
1549
|
+
: null;
|
|
1550
|
+
const resolvedDirectory = watchDirectory || (() => {
|
|
1551
|
+
try { return effectiveWorkspace(live); } catch { return null; }
|
|
1552
|
+
})();
|
|
1553
|
+
if (resolvedDirectory) {
|
|
1554
|
+
const messagesResult = await client().session.messages({
|
|
1555
|
+
sessionID,
|
|
1556
|
+
directory: resolvedDirectory,
|
|
1557
|
+
limit: 100,
|
|
1558
|
+
});
|
|
1559
|
+
if (!messagesResult?.error) {
|
|
1560
|
+
const rows = Array.isArray(messagesResult?.data)
|
|
1561
|
+
? messagesResult.data
|
|
1562
|
+
: (Array.isArray(messagesResult) ? messagesResult : []);
|
|
1563
|
+
workerText = extractLastWorkerAssistantText(rows);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
} catch {
|
|
1567
|
+
workerText = '';
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
const title = findAssignedSessionCardTitle(assistantID, sessionID);
|
|
1571
|
+
const userText = buildAssignedSessionResumeUserText({
|
|
1572
|
+
sessionID,
|
|
1573
|
+
status,
|
|
1574
|
+
title,
|
|
1575
|
+
workerText,
|
|
1576
|
+
});
|
|
1577
|
+
const history = contactHistoryForLlm(db, assistantID, {});
|
|
1578
|
+
const assignedCards = [];
|
|
1579
|
+
let contactResetThisTurn = false;
|
|
1580
|
+
let contactHistoryClearedThisTurn = false;
|
|
1581
|
+
|
|
1582
|
+
try {
|
|
1583
|
+
const executionHistory = await materializeContactHistory(
|
|
1584
|
+
db,
|
|
1585
|
+
dataDir,
|
|
1586
|
+
assistantID,
|
|
1587
|
+
history,
|
|
1588
|
+
{ budgetBytes: CONTACT_ATTACHMENT_TURN_BUDGET_BYTES },
|
|
1589
|
+
);
|
|
1590
|
+
const tools = createContactTools({
|
|
1591
|
+
assignWork: (params) => assignWork(live, params),
|
|
1592
|
+
createAssistant: (toolInput) => createAssistant(toolInput),
|
|
1593
|
+
scheduleTask: (params) => scheduleWork(live, params),
|
|
1594
|
+
deliverPeerMessage: (toolInput) => deliverPeerMessage(assistantID, toolInput),
|
|
1595
|
+
clearContactMemory: () => {
|
|
1596
|
+
const result = clearContactMemory(assistantID, {});
|
|
1597
|
+
contactResetThisTurn = true;
|
|
1598
|
+
return result;
|
|
1599
|
+
},
|
|
1600
|
+
resetContact: () => {
|
|
1601
|
+
const result = resetContact(assistantID, {});
|
|
1602
|
+
contactResetThisTurn = true;
|
|
1603
|
+
contactHistoryClearedThisTurn = true;
|
|
1604
|
+
return result;
|
|
1605
|
+
},
|
|
1606
|
+
listAssistants: () => db.prepare('SELECT * FROM assistant_v2 WHERE tombstone_at IS NULL ORDER BY created_at').all().map(output),
|
|
1607
|
+
listProjects: async () => loadRegisteredProjects(),
|
|
1608
|
+
listSessions: (params) => listSessionsWork(params),
|
|
1609
|
+
readAssistantSettings: (input = {}) => {
|
|
1610
|
+
const targetID = typeof input?.assistantID === 'string' && input.assistantID.trim()
|
|
1611
|
+
? input.assistantID.trim()
|
|
1612
|
+
: assistantID;
|
|
1613
|
+
return output(editable(targetID));
|
|
1614
|
+
},
|
|
1615
|
+
updateAssistantSettings: async (patch = {}) => {
|
|
1616
|
+
const normalizePrompt = (value) => {
|
|
1617
|
+
if (typeof value !== 'string') fail('validation_error');
|
|
1618
|
+
if (value.length > 200_000) fail('validation_error');
|
|
1619
|
+
return value.trim();
|
|
1620
|
+
};
|
|
1621
|
+
const targetID = typeof patch?.assistantID === 'string' && patch.assistantID.trim()
|
|
1622
|
+
? patch.assistantID.trim()
|
|
1623
|
+
: assistantID;
|
|
1624
|
+
let lastError = null;
|
|
1625
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1626
|
+
const current = output(editable(targetID));
|
|
1627
|
+
if (!Object.prototype.hasOwnProperty.call(patch, 'defaultPrompt')) {
|
|
1628
|
+
fail('validation_error');
|
|
1629
|
+
}
|
|
1630
|
+
const nextPrompt = normalizePrompt(patch.defaultPrompt);
|
|
1631
|
+
if (current.defaultPrompt === nextPrompt) {
|
|
1632
|
+
return {
|
|
1633
|
+
updated: false,
|
|
1634
|
+
unchanged: true,
|
|
1635
|
+
defaultPrompt: current.defaultPrompt,
|
|
1636
|
+
assistant: current,
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
await Promise.resolve();
|
|
1640
|
+
try {
|
|
1641
|
+
const updated = await updateAssistant(targetID, {
|
|
1642
|
+
expectedRevision: current.revision,
|
|
1643
|
+
defaultPrompt: nextPrompt,
|
|
1644
|
+
});
|
|
1645
|
+
return {
|
|
1646
|
+
updated: true,
|
|
1647
|
+
defaultPrompt: updated.defaultPrompt,
|
|
1648
|
+
assistant: updated,
|
|
1649
|
+
};
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
lastError = error;
|
|
1652
|
+
if (error?.code !== 'revision_conflict') throw error;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
throw lastError;
|
|
1656
|
+
},
|
|
1657
|
+
currentAssistant: assistantSnapshot,
|
|
1658
|
+
onCard: (card) => assignedCards.push(card),
|
|
1659
|
+
turnFileParts: [],
|
|
1660
|
+
turnAttachmentScope: attachmentScopeKey([]),
|
|
1661
|
+
});
|
|
1662
|
+
const generated = await runContactTurn({
|
|
1663
|
+
assistant: assistantSnapshot,
|
|
1664
|
+
history: executionHistory,
|
|
1665
|
+
userText,
|
|
1666
|
+
userParts: [{ type: 'text', text: userText }],
|
|
1667
|
+
createChatCompletion,
|
|
1668
|
+
tools,
|
|
1669
|
+
projects: registeredProjects,
|
|
1670
|
+
connectedModels,
|
|
1671
|
+
globalEventHub,
|
|
1672
|
+
onBubbleDelta: (bubbleIndex, delta, done) => {
|
|
1673
|
+
emitContactTurnEvent('openchamber:contact-bubble-delta', {
|
|
1674
|
+
assistantID,
|
|
1675
|
+
turnID,
|
|
1676
|
+
bubbleIndex,
|
|
1677
|
+
delta: typeof delta === 'string' ? delta : '',
|
|
1678
|
+
done: Boolean(done),
|
|
1679
|
+
});
|
|
1680
|
+
},
|
|
1681
|
+
});
|
|
1682
|
+
if (closed) {
|
|
1683
|
+
settleActiveContactTurn(assistantID, turnID, { bumpRevision: true });
|
|
1684
|
+
resolveContactTurnSettlement(messageID, { status: 'error', error: 'closed' });
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
const resetThisTurn = contactResetThisTurn || generated?.reset === true;
|
|
1688
|
+
const historyClearedThisTurn = contactHistoryClearedThisTurn
|
|
1689
|
+
|| contactTurnClearedChatHistory(generated?.messages)
|
|
1690
|
+
|| generated?.historyCleared === true;
|
|
1691
|
+
const preferredConfirm = historyClearedThisTurn
|
|
1692
|
+
? CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE
|
|
1693
|
+
: NEW_CONVERSATION_CONFIRM_BUBBLE;
|
|
1694
|
+
const bubbles = resetThisTurn
|
|
1695
|
+
? confirmBubbleAfterContactReset(generated?.bubbles, preferredConfirm)
|
|
1696
|
+
: (Array.isArray(generated?.bubbles) ? generated.bubbles.filter((item) => typeof item === 'string' && item.trim()) : []);
|
|
1697
|
+
const cards = resetThisTurn
|
|
1698
|
+
? []
|
|
1699
|
+
: [
|
|
1700
|
+
...assignedCards,
|
|
1701
|
+
...(Array.isArray(generated?.cards) ? generated.cards : []),
|
|
1702
|
+
].filter((card, index, list) => list.findIndex((item) => contactCardIdentity(item) === contactCardIdentity(card)) === index);
|
|
1703
|
+
if (bubbles.length === 0 && cards.length === 0) {
|
|
1704
|
+
const detail = 'Assistant returned no text';
|
|
1705
|
+
try {
|
|
1706
|
+
persistContactTurnFailure(assistantID, {
|
|
1707
|
+
userMessageID: messageID,
|
|
1708
|
+
turnID,
|
|
1709
|
+
error: detail,
|
|
1710
|
+
});
|
|
1711
|
+
settleActiveContactTurn(assistantID, turnID);
|
|
1712
|
+
} catch {
|
|
1713
|
+
settleActiveContactTurn(assistantID, turnID, { bumpRevision: true });
|
|
1714
|
+
}
|
|
1715
|
+
emitContactTurnEvent('openchamber:contact-turn-end', {
|
|
1716
|
+
assistantID,
|
|
1717
|
+
turnID,
|
|
1718
|
+
status: 'error',
|
|
1719
|
+
error: detail,
|
|
1720
|
+
});
|
|
1721
|
+
notifyContactTurn({
|
|
1722
|
+
assistantID,
|
|
1723
|
+
name: assistantSnapshot.name,
|
|
1724
|
+
turnID,
|
|
1725
|
+
status: 'error',
|
|
1726
|
+
body: detail,
|
|
1727
|
+
});
|
|
1728
|
+
resolveContactTurnSettlement(messageID, { status: 'error', error: detail });
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
// No user row for resume — only assistant bubbles/cards (userMessageID is id prefix).
|
|
1732
|
+
persistContactAssistantReply(assistantID, {
|
|
1733
|
+
userMessageID: messageID,
|
|
1734
|
+
bubbles: bubbles.length > 0 ? bubbles : [ASSIGNED_SESSION_FALLBACK_BUBBLE],
|
|
1735
|
+
cards,
|
|
1736
|
+
turnID,
|
|
1737
|
+
});
|
|
1738
|
+
settleActiveContactTurn(assistantID, turnID);
|
|
1739
|
+
emitContactTurnEvent('openchamber:contact-turn-end', {
|
|
1740
|
+
assistantID,
|
|
1741
|
+
turnID,
|
|
1742
|
+
status: 'complete',
|
|
1743
|
+
});
|
|
1744
|
+
const spoken = bubbles.filter((text) => typeof text === 'string' && text.trim() && !text.startsWith('oc.settle.'));
|
|
1745
|
+
notifyContactTurn({
|
|
1746
|
+
assistantID,
|
|
1747
|
+
name: assistantSnapshot.name,
|
|
1748
|
+
turnID,
|
|
1749
|
+
status: 'complete',
|
|
1750
|
+
body: spoken.join('\n') || cards[0]?.title || '',
|
|
1751
|
+
});
|
|
1752
|
+
resolveContactTurnSettlement(messageID, { status: 'complete' });
|
|
1753
|
+
} catch (error) {
|
|
1754
|
+
const detail = typeof error?.message === 'string' && error.message.trim()
|
|
1755
|
+
? error.message.trim()
|
|
1756
|
+
: (error?.code || 'upstream_error');
|
|
1757
|
+
const statusCode = error?.code === 'no_provider' ? 'no_provider' : (error?.code || 'upstream_error');
|
|
1758
|
+
try {
|
|
1759
|
+
persistContactTurnFailure(assistantID, {
|
|
1760
|
+
userMessageID: messageID,
|
|
1761
|
+
turnID,
|
|
1762
|
+
error: detail,
|
|
1763
|
+
});
|
|
1764
|
+
settleActiveContactTurn(assistantID, turnID);
|
|
1765
|
+
} catch {
|
|
1766
|
+
settleActiveContactTurn(assistantID, turnID, { bumpRevision: true });
|
|
1767
|
+
}
|
|
1768
|
+
emitContactTurnEvent('openchamber:contact-turn-end', {
|
|
1769
|
+
assistantID,
|
|
1770
|
+
turnID,
|
|
1771
|
+
status: 'error',
|
|
1772
|
+
error: detail,
|
|
1773
|
+
...(statusCode !== detail ? { code: statusCode } : {}),
|
|
1774
|
+
});
|
|
1775
|
+
notifyContactTurn({
|
|
1776
|
+
assistantID,
|
|
1777
|
+
name: assistantSnapshot.name,
|
|
1778
|
+
turnID,
|
|
1779
|
+
status: 'error',
|
|
1780
|
+
body: detail,
|
|
1781
|
+
});
|
|
1782
|
+
resolveContactTurnSettlement(messageID, { status: 'error', error: detail, code: statusCode });
|
|
1783
|
+
}
|
|
1784
|
+
});
|
|
1785
|
+
};
|
|
1786
|
+
assignedSessionResumeRef.schedule = scheduleAssignedSessionResume;
|
|
1341
1787
|
/**
|
|
1342
1788
|
* Contact composer send: validate → admit user (202) → async turn.
|
|
1343
1789
|
* Does not await the LLM. Live bubble tokens use openchamber:contact-bubble-delta
|
|
@@ -1479,6 +1925,58 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1479
1925
|
listAssistants: () => db.prepare('SELECT * FROM assistant_v2 WHERE tombstone_at IS NULL ORDER BY created_at').all().map(output),
|
|
1480
1926
|
listProjects: async () => loadRegisteredProjects(),
|
|
1481
1927
|
listSessions: (params) => listSessionsWork(params),
|
|
1928
|
+
// Live DB read — not the turn-start assistantSnapshot.
|
|
1929
|
+
// Optional assistantID targets another live row; omitted → this contact.
|
|
1930
|
+
readAssistantSettings: (input = {}) => {
|
|
1931
|
+
const assistantID = typeof input?.assistantID === 'string' && input.assistantID.trim()
|
|
1932
|
+
? input.assistantID.trim()
|
|
1933
|
+
: row.assistant_id;
|
|
1934
|
+
return output(editable(assistantID));
|
|
1935
|
+
},
|
|
1936
|
+
// Persist defaultPrompt via updateAssistant CAS; one revision_conflict retry.
|
|
1937
|
+
updateAssistantSettings: async (patch = {}) => {
|
|
1938
|
+
const normalizePrompt = (value) => {
|
|
1939
|
+
if (typeof value !== 'string') fail('validation_error');
|
|
1940
|
+
if (value.length > 200_000) fail('validation_error');
|
|
1941
|
+
return value.trim();
|
|
1942
|
+
};
|
|
1943
|
+
const assistantID = typeof patch?.assistantID === 'string' && patch.assistantID.trim()
|
|
1944
|
+
? patch.assistantID.trim()
|
|
1945
|
+
: row.assistant_id;
|
|
1946
|
+
let lastError = null;
|
|
1947
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1948
|
+
const current = output(editable(assistantID));
|
|
1949
|
+
if (!Object.prototype.hasOwnProperty.call(patch, 'defaultPrompt')) {
|
|
1950
|
+
fail('validation_error');
|
|
1951
|
+
}
|
|
1952
|
+
const nextPrompt = normalizePrompt(patch.defaultPrompt);
|
|
1953
|
+
if (current.defaultPrompt === nextPrompt) {
|
|
1954
|
+
return {
|
|
1955
|
+
updated: false,
|
|
1956
|
+
unchanged: true,
|
|
1957
|
+
defaultPrompt: current.defaultPrompt,
|
|
1958
|
+
assistant: current,
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
// Yield so a concurrent UI PATCH can land before CAS (and tests can inject).
|
|
1962
|
+
await Promise.resolve();
|
|
1963
|
+
try {
|
|
1964
|
+
const updated = await updateAssistant(assistantID, {
|
|
1965
|
+
expectedRevision: current.revision,
|
|
1966
|
+
defaultPrompt: nextPrompt,
|
|
1967
|
+
});
|
|
1968
|
+
return {
|
|
1969
|
+
updated: true,
|
|
1970
|
+
defaultPrompt: updated.defaultPrompt,
|
|
1971
|
+
assistant: updated,
|
|
1972
|
+
};
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
lastError = error;
|
|
1975
|
+
if (error?.code !== 'revision_conflict') throw error;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
throw lastError;
|
|
1979
|
+
},
|
|
1482
1980
|
currentAssistant: assistantSnapshot,
|
|
1483
1981
|
onCard: (card) => assignedCards.push(card),
|
|
1484
1982
|
turnFileParts,
|