instar 1.3.1309 → 1.3.1310

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.
Files changed (52) hide show
  1. package/dashboard/index.html +471 -5
  2. package/dist/commands/server.d.ts +21 -1
  3. package/dist/commands/server.d.ts.map +1 -1
  4. package/dist/commands/server.js +82 -13
  5. package/dist/commands/server.js.map +1 -1
  6. package/dist/core/FileClassifier.d.ts.map +1 -1
  7. package/dist/core/FileClassifier.js +4 -0
  8. package/dist/core/FileClassifier.js.map +1 -1
  9. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  10. package/dist/core/PostUpdateMigrator.js +25 -0
  11. package/dist/core/PostUpdateMigrator.js.map +1 -1
  12. package/dist/core/TopicProfileResolver.d.ts +20 -1
  13. package/dist/core/TopicProfileResolver.d.ts.map +1 -1
  14. package/dist/core/TopicProfileResolver.js +34 -11
  15. package/dist/core/TopicProfileResolver.js.map +1 -1
  16. package/dist/core/TopicProfileStore.d.ts +13 -0
  17. package/dist/core/TopicProfileStore.d.ts.map +1 -1
  18. package/dist/core/TopicProfileStore.js +25 -0
  19. package/dist/core/TopicProfileStore.js.map +1 -1
  20. package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
  21. package/dist/core/WriteDomainRegistry.js +16 -0
  22. package/dist/core/WriteDomainRegistry.js.map +1 -1
  23. package/dist/core/dashboardTopicProfile.d.ts +254 -0
  24. package/dist/core/dashboardTopicProfile.d.ts.map +1 -0
  25. package/dist/core/dashboardTopicProfile.js +314 -0
  26. package/dist/core/dashboardTopicProfile.js.map +1 -0
  27. package/dist/core/topicProfileWriteSurface.d.ts +2 -0
  28. package/dist/core/topicProfileWriteSurface.d.ts.map +1 -1
  29. package/dist/core/topicProfileWriteSurface.js +4 -0
  30. package/dist/core/topicProfileWriteSurface.js.map +1 -1
  31. package/dist/data/standards-guard-index.json +1 -1
  32. package/dist/data/standards-guard-index.meta.json +2 -2
  33. package/dist/data/standards-registry.meta.json +1 -1
  34. package/dist/messaging/telegram-origin/OriginDeterministicSend.d.ts +1 -1
  35. package/dist/messaging/telegram-origin/OriginDeterministicSend.d.ts.map +1 -1
  36. package/dist/scaffold/templates.d.ts.map +1 -1
  37. package/dist/scaffold/templates.js +1 -0
  38. package/dist/scaffold/templates.js.map +1 -1
  39. package/dist/server/routes.d.ts +15 -0
  40. package/dist/server/routes.d.ts.map +1 -1
  41. package/dist/server/routes.js +289 -26
  42. package/dist/server/routes.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/data/builtin-manifest.json +63 -63
  45. package/src/data/standards-guard-index.json +1 -1
  46. package/src/data/standards-guard-index.meta.json +2 -2
  47. package/src/data/standards-registry.meta.json +1 -1
  48. package/src/data/state-coherence-registry.json +17 -0
  49. package/src/scaffold/templates.ts +1 -0
  50. package/upgrades/1.3.1310.md +70 -0
  51. package/upgrades/side-effects/dashboard-door-model-controls.md +227 -0
  52. package/upgrades/side-effects/window-run-liveness-register-lock-retry.md +65 -0
@@ -1731,6 +1731,50 @@
1731
1731
  color: var(--text);
1732
1732
  }
1733
1733
 
1734
+ /* Door + model controls (dashboard-door-model-controls §3.6) — native
1735
+ selects, full width at phone width, no horizontal scroll. */
1736
+ .modal-field select {
1737
+ width: 100%;
1738
+ max-width: 100%;
1739
+ box-sizing: border-box;
1740
+ padding: 8px 12px;
1741
+ background: var(--bg-panel);
1742
+ color: var(--text-bright);
1743
+ border: 1px solid var(--border);
1744
+ border-radius: 6px;
1745
+ font-size: 14px;
1746
+ }
1747
+ .modal-field select:disabled { opacity: 0.5; }
1748
+ .modal-note {
1749
+ font-size: 12px;
1750
+ color: var(--text);
1751
+ margin: 6px 0 0;
1752
+ overflow-wrap: anywhere;
1753
+ }
1754
+ .modal-note.warn { color: var(--yellow, #e5c07b); }
1755
+ .modal-note.err { color: var(--red, #e06c75); }
1756
+ .ntd-row {
1757
+ margin: 0 12px 8px;
1758
+ padding: 6px 10px;
1759
+ border: 1px solid var(--border);
1760
+ border-radius: 6px;
1761
+ font-size: 12px;
1762
+ color: var(--text);
1763
+ cursor: pointer;
1764
+ overflow-wrap: anywhere;
1765
+ }
1766
+ .ntd-row:hover { background: var(--bg-hover); }
1767
+ .ntd-row .ntd-note { color: var(--text-dim); font-size: 11px; }
1768
+ .door-model-btn {
1769
+ font-size: 11px;
1770
+ padding: 2px 8px;
1771
+ border-radius: 10px;
1772
+ border: 1px solid var(--border);
1773
+ background: var(--bg);
1774
+ color: var(--text);
1775
+ cursor: pointer;
1776
+ }
1777
+
1734
1778
  .modal-hint {
1735
1779
  font-size: 11px;
1736
1780
  color: var(--text-dim);
@@ -3027,6 +3071,7 @@
3027
3071
  </div>
3028
3072
  </div>
3029
3073
  <div class="tab-purpose" style="padding:0 12px 8px">Your agent's live sessions — pick one to watch its terminal in real time.</div>
3074
+ <div class="ntd-row" id="newTopicDefaultRow" style="display:none" onclick="openNewTopicDefaultSheet()" role="button" tabindex="0"></div>
3030
3075
  <div class="session-list" id="sessionList">
3031
3076
  <div class="empty-state" id="emptyState">
3032
3077
  <div class="icon">&#x1F4AD;</div>
@@ -3048,6 +3093,7 @@
3048
3093
  <h3 id="termSessionName">—</h3>
3049
3094
  <span class="type-badge" id="termTypeBadge" style="display:none"></span>
3050
3095
  <span class="model-badge" id="termModelBadge" style="display:none"></span>
3096
+ <button class="door-model-btn" id="termDoorModelBtn" style="display:none" onclick="openDoorModelSheet()" title="Switch this topic's door and model">Door + model</button>
3051
3097
  </div>
3052
3098
  <div class="terminal-actions">
3053
3099
  <button class="action-btn" onclick="sendKey('Enter')" title="Send Enter">Enter</button>
@@ -3126,7 +3172,7 @@
3126
3172
  </div>
3127
3173
  <div class="modal-field">
3128
3174
  <label for="newSessionPlatform">Platform</label>
3129
- <select id="newSessionPlatform" style="width:100%;padding:8px 12px;background:var(--bg-panel);color:var(--text-bright);border:1px solid var(--border);border-radius:6px;font-size:14px;">
3175
+ <select id="newSessionPlatform" onchange="onNewSessionPlatformChange()" style="width:100%;padding:8px 12px;background:var(--bg-panel);color:var(--text-bright);border:1px solid var(--border);border-radius:6px;font-size:14px;">
3130
3176
  <option value="auto">Auto (best available)</option>
3131
3177
  <option value="telegram">✈️ Telegram</option>
3132
3178
  <option value="slack">💬 Slack</option>
@@ -3134,6 +3180,17 @@
3134
3180
  </select>
3135
3181
  <div class="modal-hint">Choose which messaging platform to link this session to.</div>
3136
3182
  </div>
3183
+ <div class="modal-field">
3184
+ <label for="newSessionDoor">Door</label>
3185
+ <select id="newSessionDoor" onchange="onNewSessionDoorChange()"></select>
3186
+ </div>
3187
+ <div class="modal-field">
3188
+ <label for="newSessionModel">Model</label>
3189
+ <select id="newSessionModel" onchange="updateCreateButtonLabel()"></select>
3190
+ <div class="modal-hint" id="newSessionDoorHint">You can change this again after the topic's first message.</div>
3191
+ <div class="modal-note warn" id="newSessionRegimeNote" style="display:none"></div>
3192
+ <div class="modal-note err" id="newSessionError" style="display:none"></div>
3193
+ </div>
3137
3194
  <!-- Keep hidden checkbox for backward compat -->
3138
3195
  <input type="hidden" id="newSessionHeadless">
3139
3196
  <div class="modal-actions">
@@ -3143,6 +3200,54 @@
3143
3200
  </div>
3144
3201
  </div>
3145
3202
 
3203
+ <!-- Default for dashboard-created topics (dashboard-door-model-controls §3.2/§3.6) -->
3204
+ <div class="modal-backdrop" id="newTopicDefaultSheet" style="display:none" onclick="closeSheet(event, 'newTopicDefaultSheet')">
3205
+ <div class="modal-dialog" onclick="event.stopPropagation()">
3206
+ <h3>Default for dashboard-created topics</h3>
3207
+ <div class="modal-note">Applies to topics you create from this dashboard. Topics you start in Telegram and topics that already exist are not changed.</div>
3208
+ <div class="modal-field" style="margin-top:12px">
3209
+ <label for="ntdDoor">Door</label>
3210
+ <select id="ntdDoor" onchange="onSheetDoorChange('ntdDoor','ntdModel')"></select>
3211
+ </div>
3212
+ <div class="modal-field">
3213
+ <label for="ntdModel">Model</label>
3214
+ <select id="ntdModel"></select>
3215
+ </div>
3216
+ <div class="modal-note" id="ntdCount"></div>
3217
+ <div class="modal-note warn" id="ntdRegimeNote" style="display:none"></div>
3218
+ <div class="modal-note" id="ntdCaveat"></div>
3219
+ <div class="modal-note" id="ntdStatus" style="display:none"></div>
3220
+ <div class="modal-actions">
3221
+ <button class="modal-btn-cancel" onclick="clearNewTopicDefault()">Clear</button>
3222
+ <button class="modal-btn-cancel" onclick="closeSheet(null, 'newTopicDefaultSheet')">Close</button>
3223
+ <button class="modal-btn-create" id="ntdSaveBtn" onclick="saveNewTopicDefault()">Save</button>
3224
+ </div>
3225
+ </div>
3226
+ </div>
3227
+
3228
+ <!-- Switch an existing topic's door + model (dashboard-door-model-controls §3.4/§3.6) -->
3229
+ <div class="modal-backdrop" id="doorModelSheet" style="display:none" onclick="closeSheet(event, 'doorModelSheet')">
3230
+ <div class="modal-dialog" onclick="event.stopPropagation()">
3231
+ <h3>Door + model</h3>
3232
+ <div class="modal-note" id="dmCurrent">Loading…</div>
3233
+ <div class="modal-field" style="margin-top:12px">
3234
+ <label for="dmDoor">Door</label>
3235
+ <select id="dmDoor" onchange="onSheetDoorChange('dmDoor','dmModel')"></select>
3236
+ </div>
3237
+ <div class="modal-field">
3238
+ <label for="dmModel">Model</label>
3239
+ <select id="dmModel"></select>
3240
+ </div>
3241
+ <div class="modal-note">Switching the door restarts this topic's session; the conversation is kept.</div>
3242
+ <div class="modal-note warn" id="dmRegimeNote" style="display:none"></div>
3243
+ <div class="modal-note" id="dmStatus" style="display:none"></div>
3244
+ <div class="modal-actions">
3245
+ <button class="modal-btn-cancel" onclick="closeSheet(null, 'doorModelSheet')">Close</button>
3246
+ <button class="modal-btn-create" id="dmSwitchBtn" onclick="switchDoorModel()">Switch</button>
3247
+ </div>
3248
+ </div>
3249
+ </div>
3250
+
3146
3251
  <!-- Send Content tab (was Drop Zone) -->
3147
3252
  <div class="dropzone-container" id="dropzoneTab" style="display:none">
3148
3253
  <div class="dropzone-panel">
@@ -4200,6 +4305,11 @@
4200
4305
  // API token is intentionally insufficient for operator-only actions.
4201
4306
  let operatorSessionToken = '';
4202
4307
  let sessions = [];
4308
+ // Door + model controls (dashboard-door-model-controls §3.6).
4309
+ let topicProfileOptions = null;
4310
+ const switchingModel = new Set(); // tmux sessions whose topic is mid-switch
4311
+ let pendingDoorSwitch = null; // { topicId, model, since, tmuxSession }
4312
+ let doorModelSheetTopic = null; // { topicId, tmuxSession }
4203
4313
  // Sessions running on OTHER machines in the pool (GET /sessions?scope=pool).
4204
4314
  // Kept separate from `sessions` because the WebSocket replaces that array
4205
4315
  // with LOCAL sessions on every broadcast — merging would be wiped each tick.
@@ -4265,6 +4375,7 @@
4265
4375
  startWaPolling();
4266
4376
  startVitalSigns();
4267
4377
  startPoolSessionsPolling();
4378
+ loadTopicProfileOptions();
4268
4379
  } else {
4269
4380
  showAuthError('Incorrect PIN');
4270
4381
  }
@@ -4291,6 +4402,7 @@
4291
4402
  startWaPolling();
4292
4403
  startVitalSigns();
4293
4404
  startPoolSessionsPolling();
4405
+ loadTopicProfileOptions();
4294
4406
  }
4295
4407
  })
4296
4408
  .catch(() => {});
@@ -4567,7 +4679,7 @@
4567
4679
  </div>
4568
4680
  <div class="session-meta">
4569
4681
  <span class="type-badge ${sType}">${sType}</span>
4570
- <span class="model-badge ${model}">${model}</span>
4682
+ <span class="model-badge ${escapeHtml(model)}">${switchingModel.has(session.tmuxSession) && !session.remote ? 'switching…' : escapeHtml(model)}</span>
4571
4683
  <span class="platform-badge ${platform}" title="${platformLabel}">${platformIcon}</span>
4572
4684
  ${session.machineNickname ? `<span class="machine-badge" title="Machine this session runs on">${escapeHtml(session.machineNickname)}</span>` : ''}
4573
4685
  ${(session.duplicateTopic === true || poolDuplicateSessionNames.has(session.name)) ? `<span class="duplicate-badge" title="Genuine cross-machine duplicate: this conversation has a live session on more than one machine at once">⚠ duplicate</span>` : ''}
@@ -4653,6 +4765,9 @@
4653
4765
  badge.style.display = 'none';
4654
4766
  }
4655
4767
 
4768
+ if (switchingModel.has(tmuxSession) && !session.remote) badge.textContent = 'switching…';
4769
+ refreshDoorModelButton(session);
4770
+
4656
4771
  // Show session type badge
4657
4772
  const sType = session.type || (session.jobSlug ? 'job' : 'interactive');
4658
4773
  const typeEl = document.getElementById('termTypeBadge');
@@ -4948,7 +5063,17 @@
4948
5063
  const nameInput = document.getElementById('newSessionName');
4949
5064
  nameInput.value = '';
4950
5065
  document.getElementById('newSessionHeadless').checked = false;
5066
+ showNote('newSessionError', '');
4951
5067
  setTimeout(() => nameInput.focus(), 50);
5068
+ // Door/Model preselect from the default (§3.6) — refreshed on every open.
5069
+ loadTopicProfileOptions().then(() => {
5070
+ const def = topicProfileOptions && topicProfileOptions.newTopicDefault;
5071
+ fillDoorSelect('newSessionDoor', def && def.framework ? def.framework : '', true);
5072
+ fillModelSelect('newSessionModel', document.getElementById('newSessionDoor').value,
5073
+ def && def.framework ? def.model : null);
5074
+ showRegimeNote('newSessionRegimeNote');
5075
+ onNewSessionPlatformChange();
5076
+ });
4952
5077
  }
4953
5078
 
4954
5079
  function closeNewSessionModal(e) {
@@ -4973,6 +5098,16 @@
4973
5098
 
4974
5099
  btn.disabled = true;
4975
5100
  btn.textContent = 'Creating...';
5101
+ showNote('newSessionError', '');
5102
+
5103
+ // A pick that differs from the dashboard default is sent explicitly; an
5104
+ // unchanged pick sends nothing and the server seeds the default (§3.2).
5105
+ const pick = newSessionPick();
5106
+ const payload = { name, headless, platform: platform === 'auto' ? undefined : platform };
5107
+ if (pick && pickDiffersFromDefault(pick)) {
5108
+ payload.framework = pick.framework;
5109
+ payload.model = pick.model;
5110
+ }
4976
5111
 
4977
5112
  try {
4978
5113
  const resp = await fetch('/sessions/create', {
@@ -4980,23 +5115,354 @@
4980
5115
  headers: {
4981
5116
  'Authorization': `Bearer ${token}`,
4982
5117
  'Content-Type': 'application/json',
5118
+ // Required for a door/model pick (403 without it) — §3.6.
5119
+ 'X-Instar-Request': '1',
4983
5120
  },
4984
- body: JSON.stringify({ name, headless, platform: platform === 'auto' ? undefined : platform }),
5121
+ body: JSON.stringify(payload),
4985
5122
  });
4986
5123
  const data = await resp.json();
4987
5124
 
4988
5125
  if (!resp.ok) {
5126
+ // Server copy verbatim, rendered as text (§3.6).
5127
+ showNote('newSessionError', data.error || data.message || 'Failed to create session');
4989
5128
  showToast(data.error || 'Failed to create session', 'error');
4990
5129
  return;
4991
5130
  }
4992
5131
 
4993
- showToast(`Session "${name}" created${data.topicId ? ' with Telegram topic' : ' (headless)'}`, 'success');
5132
+ const where = data.profile ? ` on ${data.profile.label}${data.profile.model ? ' · ' + data.profile.model : ''}` : '';
5133
+ showToast(`Session "${name}" created${data.topicId ? ' with Telegram topic' + where : ' (headless)'}`, 'success');
4994
5134
  document.getElementById('newSessionModal').style.display = 'none';
5135
+ if (data.seed && data.seed.outcome === 'seeded') loadTopicProfileOptions();
4995
5136
  } catch (err) {
4996
5137
  showToast('Network error: ' + (err.message || 'Cannot reach server'), 'error');
4997
5138
  } finally {
4998
5139
  btn.disabled = false;
4999
- btn.textContent = 'Create Session';
5140
+ updateCreateButtonLabel();
5141
+ }
5142
+ }
5143
+
5144
+ // ── Door + model controls (docs/specs/dashboard-door-model-controls.md) ──
5145
+ // Every server-supplied string is rendered with textContent / option.text.
5146
+
5147
+ function showNote(id, text) {
5148
+ const el = document.getElementById(id);
5149
+ if (!el) return;
5150
+ el.textContent = text || '';
5151
+ el.style.display = text ? 'block' : 'none';
5152
+ }
5153
+
5154
+ async function loadTopicProfileOptions() {
5155
+ try {
5156
+ const r = await fetch('/topic-profile/options', { headers: { 'Authorization': `Bearer ${token}` } });
5157
+ topicProfileOptions = r.ok ? await r.json() : null;
5158
+ } catch {
5159
+ topicProfileOptions = null;
5160
+ }
5161
+ renderNewTopicDefaultRow();
5162
+ return topicProfileOptions;
5163
+ }
5164
+
5165
+ function doorOf(fw) {
5166
+ return ((topicProfileOptions && topicProfileOptions.doors) || []).find(d => d.framework === fw) || null;
5167
+ }
5168
+
5169
+ function doorModelLabel(fw, model) {
5170
+ const d = doorOf(fw);
5171
+ return (d ? d.label : fw) + ' · ' + (model || 'Account default');
5172
+ }
5173
+
5174
+ // withDefaultOption: the modal offers "Agent default" (no pick) when no
5175
+ // dashboard default is set, so an untouched create stays exactly as today.
5176
+ function fillDoorSelect(id, selected, withDefaultOption) {
5177
+ const sel = document.getElementById(id);
5178
+ sel.textContent = '';
5179
+ const hasDefault = topicProfileOptions && topicProfileOptions.newTopicDefault && topicProfileOptions.newTopicDefault.framework;
5180
+ if (withDefaultOption && !hasDefault) {
5181
+ const o = document.createElement('option');
5182
+ o.value = '';
5183
+ o.text = 'Agent default';
5184
+ sel.appendChild(o);
5185
+ }
5186
+ for (const d of ((topicProfileOptions && topicProfileOptions.doors) || [])) {
5187
+ const o = document.createElement('option');
5188
+ o.value = d.framework;
5189
+ let text = d.label;
5190
+ if (d.availability === 'assumed') text += ' (not verified)';
5191
+ if (!d.selectable) {
5192
+ text += ' — ' + (d.reason || 'unavailable');
5193
+ if (d.chatOnly) text += ' (you can still pin this in chat)';
5194
+ o.disabled = true;
5195
+ }
5196
+ o.text = text;
5197
+ sel.appendChild(o);
5198
+ }
5199
+ const canSelect = [...sel.options].some(o => o.value === selected && !o.disabled);
5200
+ sel.value = canSelect ? selected : (sel.options[0] && !sel.options[0].disabled ? sel.options[0].value : '');
5201
+ }
5202
+
5203
+ function fillModelSelect(id, fw, selected) {
5204
+ const sel = document.getElementById(id);
5205
+ sel.textContent = '';
5206
+ const d = doorOf(fw);
5207
+ if (!d) {
5208
+ const o = document.createElement('option');
5209
+ o.value = '';
5210
+ o.text = '—';
5211
+ sel.appendChild(o);
5212
+ sel.disabled = true;
5213
+ return;
5214
+ }
5215
+ sel.disabled = false;
5216
+ const acct = document.createElement('option');
5217
+ acct.value = '';
5218
+ acct.text = 'Account default';
5219
+ sel.appendChild(acct);
5220
+ for (const m of d.models) {
5221
+ const o = document.createElement('option');
5222
+ o.value = m;
5223
+ o.text = m;
5224
+ sel.appendChild(o);
5225
+ }
5226
+ const want = selected !== undefined && selected !== null ? selected : (d.defaultModel || '');
5227
+ sel.value = [...sel.options].some(o => o.value === want) ? want : '';
5228
+ }
5229
+
5230
+ function showRegimeNote(id) {
5231
+ const regime = topicProfileOptions && topicProfileOptions.regime;
5232
+ showNote(id, regime === 'dry-run'
5233
+ ? 'On this agent a chosen model is recorded as intent, not applied — the door still switches.'
5234
+ : regime === 'disabled'
5235
+ ? 'On this agent the door switch works, but model choice is turned off — the model will be refused.'
5236
+ : '');
5237
+ }
5238
+
5239
+ function onSheetDoorChange(doorId, modelId) {
5240
+ fillModelSelect(modelId, document.getElementById(doorId).value, null);
5241
+ }
5242
+
5243
+ function newSessionPick() {
5244
+ const fw = document.getElementById('newSessionDoor').value;
5245
+ if (!fw) return null;
5246
+ const m = document.getElementById('newSessionModel').value;
5247
+ return { framework: fw, model: m || null };
5248
+ }
5249
+
5250
+ function pickDiffersFromDefault(pick) {
5251
+ const def = topicProfileOptions && topicProfileOptions.newTopicDefault;
5252
+ if (!def || !def.framework) return true;
5253
+ return def.framework !== pick.framework || (def.model || null) !== pick.model;
5254
+ }
5255
+
5256
+ function updateCreateButtonLabel() {
5257
+ const btn = document.getElementById('createSessionBtn');
5258
+ const doorSel = document.getElementById('newSessionDoor');
5259
+ const pick = doorSel && !doorSel.disabled ? newSessionPick() : null;
5260
+ btn.textContent = pick && pickDiffersFromDefault(pick)
5261
+ ? 'Create on ' + doorModelLabel(pick.framework, pick.model)
5262
+ : 'Create Session';
5263
+ }
5264
+
5265
+ function onNewSessionDoorChange() {
5266
+ fillModelSelect('newSessionModel', document.getElementById('newSessionDoor').value, null);
5267
+ updateCreateButtonLabel();
5268
+ }
5269
+
5270
+ // Door/Model apply only to Telegram topics in v1 — disabled for Headless/Slack.
5271
+ function onNewSessionPlatformChange() {
5272
+ const p = document.getElementById('newSessionPlatform').value;
5273
+ const off = p === 'headless' || p === 'slack' || !topicProfileOptions;
5274
+ document.getElementById('newSessionDoor').disabled = off;
5275
+ document.getElementById('newSessionModel').disabled = off || !document.getElementById('newSessionDoor').value;
5276
+ document.getElementById('newSessionDoorHint').textContent = off && topicProfileOptions
5277
+ ? 'Door and model apply to Telegram topics only.'
5278
+ : 'You can change this again after the topic\'s first message.';
5279
+ updateCreateButtonLabel();
5280
+ }
5281
+
5282
+ function closeSheet(e, id) {
5283
+ if (e && e.target !== e.currentTarget) return;
5284
+ document.getElementById(id).style.display = 'none';
5285
+ }
5286
+
5287
+ // Sessions list header row — "Default for dashboard-created topics".
5288
+ function renderNewTopicDefaultRow() {
5289
+ const row = document.getElementById('newTopicDefaultRow');
5290
+ if (!row) return;
5291
+ if (!topicProfileOptions) { row.style.display = 'none'; return; }
5292
+ const def = topicProfileOptions.newTopicDefault || {};
5293
+ row.textContent = '';
5294
+ const main = document.createElement('div');
5295
+ main.textContent = 'Default for dashboard-created topics: '
5296
+ + (def.framework ? doorModelLabel(def.framework, def.model) : 'not set') + ' ▸';
5297
+ const note = document.createElement('div');
5298
+ note.className = 'ntd-note';
5299
+ note.textContent = def.replication === 'local-only' ? 'this machine only' : (def.replication || '');
5300
+ row.appendChild(main);
5301
+ row.appendChild(note);
5302
+ row.style.display = 'block';
5303
+ }
5304
+
5305
+ function openNewTopicDefaultSheet() {
5306
+ document.getElementById('newTopicDefaultSheet').style.display = 'flex';
5307
+ showNote('ntdStatus', '');
5308
+ loadTopicProfileOptions().then(() => {
5309
+ const def = (topicProfileOptions && topicProfileOptions.newTopicDefault) || {};
5310
+ fillDoorSelect('ntdDoor', def.framework || '', false);
5311
+ fillModelSelect('ntdModel', document.getElementById('ntdDoor').value, def.framework ? def.model : null);
5312
+ showRegimeNote('ntdRegimeNote');
5313
+ const n = def.seedsSinceBoot || 0;
5314
+ document.getElementById('ntdCount').textContent =
5315
+ `${n} dashboard topic${n === 1 ? '' : 's'} started on this default since the server started.`;
5316
+ document.getElementById('ntdCaveat').textContent =
5317
+ `Applies to new topics created on ${selfMachineNickname || 'this machine'}. Your other machines keep their own default until replication lands. `
5318
+ + 'Topics you start in Telegram are not covered yet (coming with the cross-machine work).';
5319
+ });
5320
+ }
5321
+
5322
+ async function postNewTopicDefault(body) {
5323
+ const headers = {
5324
+ 'Authorization': `Bearer ${token}`,
5325
+ 'Content-Type': 'application/json',
5326
+ 'X-Instar-Request': '1',
5327
+ };
5328
+ // Attribution only (never a refusal): lets the audit tell a dashboard human
5329
+ // from an agent session holding the same Bearer.
5330
+ if (operatorSessionToken) headers['X-Instar-Operator-Session'] = operatorSessionToken;
5331
+ const r = await fetch('/topic-profile/new-topic-default', { method: 'POST', headers, body: JSON.stringify(body) });
5332
+ let data = {};
5333
+ try { data = await r.json(); } catch { /* non-JSON error body */ }
5334
+ return { ok: r.ok, data };
5335
+ }
5336
+
5337
+ async function saveNewTopicDefault() {
5338
+ const fw = document.getElementById('ntdDoor').value;
5339
+ if (!fw) return;
5340
+ const model = document.getElementById('ntdModel').value || null;
5341
+ const { ok, data } = await postNewTopicDefault({ framework: fw, model });
5342
+ if (!ok) { showNote('ntdStatus', data.error || 'Could not save the default'); return; }
5343
+ await loadTopicProfileOptions();
5344
+ const saved = data.newTopicDefault;
5345
+ showNote('ntdStatus', 'Saved: ' + doorModelLabel(saved.framework, saved.model));
5346
+ }
5347
+
5348
+ async function clearNewTopicDefault() {
5349
+ const { ok, data } = await postNewTopicDefault({ clear: true });
5350
+ if (!ok) { showNote('ntdStatus', data.error || 'Could not clear the default'); return; }
5351
+ await loadTopicProfileOptions();
5352
+ fillDoorSelect('ntdDoor', '', false);
5353
+ fillModelSelect('ntdModel', document.getElementById('ntdDoor').value, null);
5354
+ showNote('ntdStatus', 'Cleared — dashboard-created topics start on the agent defaults.');
5355
+ }
5356
+
5357
+ // The session view's "Door + model" button — local Telegram tiles only
5358
+ // (remote and Slack tiles keep the read-only badge, §6).
5359
+ async function refreshDoorModelButton(session) {
5360
+ const btn = document.getElementById('termDoorModelBtn');
5361
+ btn.style.display = 'none';
5362
+ doorModelSheetTopic = null;
5363
+ if (!session || session.remote) return;
5364
+ try {
5365
+ const r = await fetch('/sessions', { headers: { 'Authorization': `Bearer ${token}` } });
5366
+ if (!r.ok) return;
5367
+ const list = await r.json();
5368
+ const arr = Array.isArray(list) ? list : (list.sessions || []);
5369
+ const s = arr.find(x => x.tmuxSession === session.tmuxSession);
5370
+ if (!s || s.platform !== 'telegram' || s.platformId == null) return;
5371
+ if (activeSession !== session.tmuxSession) return;
5372
+ doorModelSheetTopic = { topicId: String(s.platformId), tmuxSession: s.tmuxSession };
5373
+ btn.style.display = 'inline-block';
5374
+ } catch { /* read-only badge stays */ }
5375
+ }
5376
+
5377
+ function describeSource(profile) {
5378
+ const src = profile.resolved && profile.resolved.sources ? profile.resolved.sources.framework : '';
5379
+ const by = profile.pin && profile.pin.updatedBy ? profile.pin.updatedBy : '';
5380
+ if (src === 'profile-pin' && (by === 'system:dashboard-create' || by === 'system:new-topic-default')) return 'seeded at creation';
5381
+ if (src === 'profile-pin') return 'pinned by you';
5382
+ return 'default';
5383
+ }
5384
+
5385
+ async function openDoorModelSheet() {
5386
+ if (!doorModelSheetTopic) return;
5387
+ document.getElementById('doorModelSheet').style.display = 'flex';
5388
+ showNote('dmStatus', '');
5389
+ document.getElementById('dmCurrent').textContent = 'Loading…';
5390
+ await loadTopicProfileOptions();
5391
+ try {
5392
+ const r = await fetch('/topic-profile/' + encodeURIComponent(doorModelSheetTopic.topicId), { headers: { 'Authorization': `Bearer ${token}` } });
5393
+ const p = await r.json();
5394
+ if (!r.ok) { document.getElementById('dmCurrent').textContent = p.error || 'Could not read this topic\'s profile'; return; }
5395
+ document.getElementById('dmCurrent').textContent = 'Now: ' + doorModelLabel(p.resolved.framework, p.resolved.model) + ' (' + describeSource(p) + ')';
5396
+ fillDoorSelect('dmDoor', p.resolved.framework, false);
5397
+ fillModelSelect('dmModel', document.getElementById('dmDoor').value, p.resolved.model);
5398
+ showRegimeNote('dmRegimeNote');
5399
+ } catch (err) {
5400
+ document.getElementById('dmCurrent').textContent = 'Network error: ' + (err.message || 'Cannot reach server');
5401
+ }
5402
+ }
5403
+
5404
+ async function switchDoorModel() {
5405
+ if (!doorModelSheetTopic) return;
5406
+ const fw = document.getElementById('dmDoor').value;
5407
+ const model = document.getElementById('dmModel').value || null;
5408
+ const btn = document.getElementById('dmSwitchBtn');
5409
+ btn.disabled = true;
5410
+ try {
5411
+ const r = await fetch('/topic-profile/' + encodeURIComponent(doorModelSheetTopic.topicId), {
5412
+ method: 'POST',
5413
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'X-Instar-Request': '1' },
5414
+ body: JSON.stringify({ framework: fw, model }),
5415
+ });
5416
+ let data = {};
5417
+ try { data = await r.json(); } catch { /* non-JSON error body */ }
5418
+ // The server's reply verbatim — including no-bound-operator (§3.4).
5419
+ showNote('dmStatus', data.message || data.error || (r.ok ? 'Done.' : 'Refused.'));
5420
+ if (r.ok && !data.noop) {
5421
+ pendingDoorSwitch = { topicId: doorModelSheetTopic.topicId, model, since: Date.now(), tmuxSession: doorModelSheetTopic.tmuxSession };
5422
+ switchingModel.add(doorModelSheetTopic.tmuxSession);
5423
+ renderSessionList();
5424
+ watchDoorSwitch();
5425
+ }
5426
+ } catch (err) {
5427
+ showNote('dmStatus', 'Network error: ' + (err.message || 'Cannot reach server'));
5428
+ } finally {
5429
+ btn.disabled = false;
5430
+ }
5431
+ }
5432
+
5433
+ // "switching…" until GET /sessions reports the LAUNCHED model for the topic —
5434
+ // never an echo of the selection (§3.6 / P20 badge declaration).
5435
+ async function watchDoorSwitch() {
5436
+ const job = pendingDoorSwitch;
5437
+ if (!job) return;
5438
+ const badge = document.getElementById('termModelBadge');
5439
+ if (activeSession === job.tmuxSession) { badge.textContent = 'switching…'; badge.style.display = 'inline-block'; }
5440
+ for (let i = 0; i < 60 && pendingDoorSwitch === job; i++) {
5441
+ await new Promise(res => setTimeout(res, 3000));
5442
+ try {
5443
+ const r = await fetch('/sessions', { headers: { 'Authorization': `Bearer ${token}` } });
5444
+ if (!r.ok) continue;
5445
+ const list = await r.json();
5446
+ const arr = Array.isArray(list) ? list : (list.sessions || []);
5447
+ const s = arr.find(x => String(x.platformId) === job.topicId
5448
+ && new Date(x.startedAt).getTime() >= job.since - 1000
5449
+ && (job.model === null || x.model === job.model));
5450
+ if (s) {
5451
+ switchingModel.delete(job.tmuxSession);
5452
+ pendingDoorSwitch = null;
5453
+ if (activeSession === job.tmuxSession || activeSession === s.tmuxSession) {
5454
+ badge.textContent = s.model || '';
5455
+ badge.className = 'model-badge ' + (s.model || '');
5456
+ }
5457
+ renderSessionList();
5458
+ return;
5459
+ }
5460
+ } catch { /* retry */ }
5461
+ }
5462
+ if (pendingDoorSwitch === job) {
5463
+ switchingModel.delete(job.tmuxSession);
5464
+ pendingDoorSwitch = null;
5465
+ renderSessionList();
5000
5466
  }
5001
5467
  }
5002
5468
 
@@ -2,7 +2,7 @@ import { SessionManager } from '../core/SessionManager.js';
2
2
  import { TelegramAdapter } from '../messaging/TelegramAdapter.js';
3
3
  import { TopicMemory } from '../memory/TopicMemory.js';
4
4
  import { QuotaTracker } from '../monitoring/QuotaTracker.js';
5
- import type { IntelligenceProvider, InstarConfig } from '../core/types.js';
5
+ import type { IntelligenceProvider, UserProfile, InstarConfig } from '../core/types.js';
6
6
  import { UserManager } from '../users/UserManager.js';
7
7
  /**
8
8
  * Pure: should the emergency "fix command" gate intercept this message?
@@ -32,6 +32,26 @@ interface StartOptions {
32
32
  }
33
33
  /** Narrow, testable ownership of the stale-generation recovery side effect. */
34
34
  export declare function clearGenerationlessCodexResumeBinding(resumeMap: Pick<import('../core/TopicResumeMap.js').TopicResumeMap, 'remove'> | null, topicId: number): void;
35
+ export declare function spawnSessionForTopic(sessionManager: SessionManager, telegram: TelegramAdapter, sessionName: string, topicId: number, latestMessage?: string, topicMemory?: TopicMemory, userProfile?: UserProfile, precomputedContext?: string,
36
+ /** Subscription & Auth Standard P1.3 (additive): launch under this account's
37
+ * config home + record its id (the quota-aware account-swap mechanism).
38
+ * Omitted = unchanged behaviour. */
39
+ accountSwap?: {
40
+ configHome?: string;
41
+ accountId?: string;
42
+ },
43
+ /** Reap-notify spec R2.8 / L13 — explicit per-spawn working directory (the
44
+ * resume-queue drainer passes a queue entry's recorded cwd so interrupted
45
+ * worktree work resumes in ITS tree). Omitted = module project dir. */
46
+ spawnOpts?: {
47
+ cwd?: string;
48
+ awaitInitialInjection?: boolean;
49
+ /** dashboard-door-model-controls §3.3 step 4 — a dashboard-created topic
50
+ * starts IDLE: no bootstrap build (no context read, no temp file, no
51
+ * relay block) and no initial message, exactly what the raw
52
+ * spawnInteractiveSession(undefined, …) path injected before. */
53
+ silentStart?: boolean;
54
+ }): Promise<string>;
35
55
  /**
36
56
  * Resolve the hub-intent classifier deps (Conversion #3,
37
57
  * docs/specs/keyword-intent-conversions-1-and-3.md) from live config + the shared
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAuGA,OAAO,EAAE,cAAc,EAAwD,MAAM,2BAA2B,CAAC;AAUjH,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AA4BjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAmI7D,OAAO,KAAK,EAAW,oBAAoB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACjG,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAkFtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AA0ID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAoDD,+EAA+E;AAC/E,wBAAgB,qCAAqC,CACnD,SAAS,EAAE,IAAI,CAAC,OAAO,2BAA2B,EAAE,cAAc,EAAE,QAAQ,CAAC,GAAG,IAAI,EACpF,OAAO,EAAE,MAAM,GACd,IAAI,CAEN;AAocD,wBAAsB,oBAAoB,CACxC,cAAc,EAAE,cAAc,EAC9B,QAAQ,EAAE,eAAe,EACzB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,aAAa,CAAC,EAAE,MAAM,EACtB,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,kBAAkB,CAAC,EAAE,MAAM;AAC3B;;qCAEqC;AACrC,WAAW,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE;AACzD;;wEAEwE;AACxE,SAAS,CAAC,EAAE;IACV,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;sEAGkE;IAClE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GACA,OAAO,CAAC,MAAM,CAAC,CAqYjB;AA02BD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,YAAY,EACpB,YAAY,EAAE,oBAAoB,GAAG,IAAI,GACxC;IACD,YAAY,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IAC3C,QAAQ,EAAE,MAAM,CAAC;CAClB,CAqBA;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,EAQrD,oBAAoB,CAAC,EAAE,MAAM;IAC3B,YAAY,EAAE,OAAO,kBAAkB,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACrE,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IAC3C,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,IAAI,GACP,IAAI,CA8yBN;AAgmBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAknwBtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}