instar 1.3.1619 → 1.3.1621
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/commands/server.d.ts +6 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +115 -9
- package/dist/commands/server.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +6 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +24 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/TopicProfileOrchestrator.d.ts +4 -0
- package/dist/core/TopicProfileOrchestrator.d.ts.map +1 -1
- package/dist/core/TopicProfileOrchestrator.js +14 -1
- package/dist/core/TopicProfileOrchestrator.js.map +1 -1
- package/dist/core/TopicSpawnBriefStore.d.ts +48 -0
- package/dist/core/TopicSpawnBriefStore.d.ts.map +1 -0
- package/dist/core/TopicSpawnBriefStore.js +107 -0
- package/dist/core/TopicSpawnBriefStore.js.map +1 -0
- package/dist/core/componentCategories.d.ts.map +1 -1
- package/dist/core/componentCategories.js +1 -0
- package/dist/core/componentCategories.js.map +1 -1
- package/dist/core/devGatedFeatures.js +1 -1
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/data/llmBenchCoverage.d.ts.map +1 -1
- package/dist/data/llmBenchCoverage.js +5 -0
- package/dist/data/llmBenchCoverage.js.map +1 -1
- package/dist/data/provenanceCoverage.d.ts +2 -0
- package/dist/data/provenanceCoverage.d.ts.map +1 -1
- package/dist/data/provenanceCoverage.js +15 -0
- package/dist/data/provenanceCoverage.js.map +1 -1
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/monitoring/MachineWedgeJudge.d.ts +28 -0
- package/dist/monitoring/MachineWedgeJudge.d.ts.map +1 -0
- package/dist/monitoring/MachineWedgeJudge.js +114 -0
- package/dist/monitoring/MachineWedgeJudge.js.map +1 -0
- package/dist/monitoring/MachineWedgeNotice.d.ts +38 -0
- package/dist/monitoring/MachineWedgeNotice.d.ts.map +1 -0
- package/dist/monitoring/MachineWedgeNotice.js +71 -0
- package/dist/monitoring/MachineWedgeNotice.js.map +1 -0
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +1 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/monitoring/machineMaintenance/DrainRehearsal.d.ts +6 -0
- package/dist/monitoring/machineMaintenance/DrainRehearsal.d.ts.map +1 -1
- package/dist/monitoring/machineMaintenance/DrainRehearsal.js +7 -2
- package/dist/monitoring/machineMaintenance/DrainRehearsal.js.map +1 -1
- package/dist/monitoring/machineMaintenance/HostMaintenanceStore.d.ts +36 -0
- package/dist/monitoring/machineMaintenance/HostMaintenanceStore.d.ts.map +1 -1
- package/dist/monitoring/machineMaintenance/HostMaintenanceStore.js +54 -0
- package/dist/monitoring/machineMaintenance/HostMaintenanceStore.js.map +1 -1
- package/dist/monitoring/machineMaintenance/WedgeMonitor.d.ts +191 -0
- package/dist/monitoring/machineMaintenance/WedgeMonitor.d.ts.map +1 -0
- package/dist/monitoring/machineMaintenance/WedgeMonitor.js +489 -0
- package/dist/monitoring/machineMaintenance/WedgeMonitor.js.map +1 -0
- package/dist/monitoring/machineMaintenance/readings.d.ts +46 -0
- package/dist/monitoring/machineMaintenance/readings.d.ts.map +1 -1
- package/dist/monitoring/machineMaintenance/readings.js +71 -8
- package/dist/monitoring/machineMaintenance/readings.js.map +1 -1
- package/dist/monitoring/machineMaintenance/wedgeEvidence.d.ts +73 -0
- package/dist/monitoring/machineMaintenance/wedgeEvidence.d.ts.map +1 -0
- package/dist/monitoring/machineMaintenance/wedgeEvidence.js +111 -0
- package/dist/monitoring/machineMaintenance/wedgeEvidence.js.map +1 -0
- package/dist/monitoring/machineMaintenance/wedgeWindow.d.ts +94 -0
- package/dist/monitoring/machineMaintenance/wedgeWindow.d.ts.map +1 -0
- package/dist/monitoring/machineMaintenance/wedgeWindow.js +126 -0
- package/dist/monitoring/machineMaintenance/wedgeWindow.js.map +1 -0
- package/dist/monitoring/selfaction/policies.d.ts.map +1 -1
- package/dist/monitoring/selfaction/policies.js +15 -0
- package/dist/monitoring/selfaction/policies.js.map +1 -1
- package/dist/native/instar-desktop-host +0 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +2 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/CapabilityIndex.js +1 -1
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +3 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +56 -12
- package/dist/server/routes.js.map +1 -1
- package/dist/server/sessionSpawnTopicBinding.d.ts +26 -0
- package/dist/server/sessionSpawnTopicBinding.d.ts.map +1 -1
- package/dist/server/sessionSpawnTopicBinding.js +50 -0
- package/dist/server/sessionSpawnTopicBinding.js.map +1 -1
- package/dist/testing/selfActionRegistry.d.ts.map +1 -1
- package/dist/testing/selfActionRegistry.js +63 -0
- package/dist/testing/selfActionRegistry.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +63 -63
- package/src/data/llmBenchCoverage.ts +7 -0
- package/src/data/provenanceCoverage.ts +18 -0
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/src/scaffold/templates.ts +2 -1
- package/upgrades/1.3.1620.md +26 -0
- package/upgrades/1.3.1621.md +27 -0
- package/upgrades/eli16/profile-restart-keeps-spawn-brief.md +24 -0
- package/upgrades/side-effects/machine-wedge-dryrun.eli16.md +15 -0
- package/upgrades/side-effects/machine-wedge-dryrun.md +146 -0
- package/upgrades/side-effects/profile-restart-keeps-spawn-brief.md +91 -0
|
@@ -20,7 +20,7 @@ import { JOB_DECLARED_FAILURE_AWARENESS } from "../scheduler/jobDeclaredFailure.
|
|
|
20
20
|
// existing-agent migration there) so the two can never drift. Imported as a runtime
|
|
21
21
|
// function call inside generateClaudeMd — no module-init cycle (PostUpdateMigrator
|
|
22
22
|
// never imports templates).
|
|
23
|
-
import { APPRENTICESHIP_STALL_GATE_CLAUDEMD_SECTION, SESSION_LISTING_HYGIENE_CLAUDEMD_SECTION, PLAYWRIGHT_PROFILE_REGISTRY_CLAUDEMD_SECTION, MACHINE_LOAD_ASSESSMENT_CLAUDEMD_SECTION, CODEX_CONNECT_CLAUDEMD_SECTION, FEATURE_SWITCH_CLAUDEMD_SECTION, NORMAL_CHROME_BROWSER_CLAUDEMD_SECTION, SINGLE_MACHINE_FAILOVER_GAP_CLAUDEMD_SECTION, MISSING_LOGIN_SESSION_CLAUDEMD_SECTION, DYNAMIC_MCP_CLAUDEMD_SECTION, ULTRACODE_SPAWN_CLAUDEMD_SECTION, AGENT_SIGNATURE_PROVENANCE_CLAUDEMD_SECTION, SENDER_REJECTION_CLAUDEMD_SECTION, SCOPE_ACCRETION_CLAUDEMD_SECTION, MESH_SELF_HEALING_CLAUDEMD_SECTION, LEASE_MEDIUM_CLAUDEMD_SECTION, WRITE_ADMISSION_CLAUDEMD_SECTION, DOORWAY_REGISTRY_CLAUDEMD_SECTION, EXTERNAL_HOG_CLAUDEMD_SECTION, ROUTING_SPEND_CLAUDEMD_SECTION, MATURATION_BOARD_CLAUDEMD_SECTION, QUEUE_PACE_CLAUDEMD_SECTION, ECOSYSTEM_IDEAS_CLAUDEMD_SECTION, MOBILE_PAIRING_CLAUDEMD_SECTION, MOBILE_CHAT_CLAUDEMD_SECTION, APP_CONVERSATIONS_CLAUDEMD_SECTION, MOBILE_ROUTES_CLAUDEMD_SECTION, MOBILE_PUSH_CLAUDEMD_SECTION, DECISION_QUALITY_CLAUDEMD_SECTION, BENCHMARK_DIVERGENCE_CLAUDEMD_SECTION, DUPLICATE_RECONCILER_CLAUDEMD_SECTION, AUDIT_CONVERGENCE_CLAUDEMD_SECTION, TONE_ADVISORY_MIGRATION_CLAUDEMD_SECTION, DECISION_JOURNAL_CONFIDENCE_CLAUDEMD_GUIDANCE, WINDOW_RUN_LIVENESS_CLAUDEMD_SECTION, SCRIPT_JOBS_CLAUDEMD_SECTION, JOB_RESULT_SUPERVISION_CLAUDEMD_SECTION, DEDICATED_PROFILE_REGISTRY_ROW_ANSWER, CODEX_ARM_SWEEPER_CLAUDEMD_SECTION, MACHINE_MAINTENANCE_CLAUDEMD_SECTION, APP_MEDIA_CLAUDEMD_SECTION, MACHINE_BROWSER_CARE_CLAUDEMD_SECTION, PAIRING_WITHOUT_TELEGRAM_CLAUDEMD_SECTION, SLACK_FROM_APP_CLAUDEMD_SECTION, MACHINE_MAINTENANCE_POOL_CLAUDEMD_SECTION } from '../core/PostUpdateMigrator.js';
|
|
23
|
+
import { APPRENTICESHIP_STALL_GATE_CLAUDEMD_SECTION, SESSION_LISTING_HYGIENE_CLAUDEMD_SECTION, PLAYWRIGHT_PROFILE_REGISTRY_CLAUDEMD_SECTION, MACHINE_LOAD_ASSESSMENT_CLAUDEMD_SECTION, CODEX_CONNECT_CLAUDEMD_SECTION, FEATURE_SWITCH_CLAUDEMD_SECTION, NORMAL_CHROME_BROWSER_CLAUDEMD_SECTION, SINGLE_MACHINE_FAILOVER_GAP_CLAUDEMD_SECTION, MISSING_LOGIN_SESSION_CLAUDEMD_SECTION, DYNAMIC_MCP_CLAUDEMD_SECTION, ULTRACODE_SPAWN_CLAUDEMD_SECTION, AGENT_SIGNATURE_PROVENANCE_CLAUDEMD_SECTION, SENDER_REJECTION_CLAUDEMD_SECTION, SCOPE_ACCRETION_CLAUDEMD_SECTION, MESH_SELF_HEALING_CLAUDEMD_SECTION, LEASE_MEDIUM_CLAUDEMD_SECTION, WRITE_ADMISSION_CLAUDEMD_SECTION, DOORWAY_REGISTRY_CLAUDEMD_SECTION, EXTERNAL_HOG_CLAUDEMD_SECTION, ROUTING_SPEND_CLAUDEMD_SECTION, MATURATION_BOARD_CLAUDEMD_SECTION, QUEUE_PACE_CLAUDEMD_SECTION, ECOSYSTEM_IDEAS_CLAUDEMD_SECTION, MOBILE_PAIRING_CLAUDEMD_SECTION, MOBILE_CHAT_CLAUDEMD_SECTION, APP_CONVERSATIONS_CLAUDEMD_SECTION, MOBILE_ROUTES_CLAUDEMD_SECTION, MOBILE_PUSH_CLAUDEMD_SECTION, DECISION_QUALITY_CLAUDEMD_SECTION, BENCHMARK_DIVERGENCE_CLAUDEMD_SECTION, DUPLICATE_RECONCILER_CLAUDEMD_SECTION, AUDIT_CONVERGENCE_CLAUDEMD_SECTION, TONE_ADVISORY_MIGRATION_CLAUDEMD_SECTION, DECISION_JOURNAL_CONFIDENCE_CLAUDEMD_GUIDANCE, WINDOW_RUN_LIVENESS_CLAUDEMD_SECTION, SCRIPT_JOBS_CLAUDEMD_SECTION, JOB_RESULT_SUPERVISION_CLAUDEMD_SECTION, DEDICATED_PROFILE_REGISTRY_ROW_ANSWER, CODEX_ARM_SWEEPER_CLAUDEMD_SECTION, MACHINE_MAINTENANCE_CLAUDEMD_SECTION, APP_MEDIA_CLAUDEMD_SECTION, MACHINE_BROWSER_CARE_CLAUDEMD_SECTION, PAIRING_WITHOUT_TELEGRAM_CLAUDEMD_SECTION, SLACK_FROM_APP_CLAUDEMD_SECTION, MACHINE_MAINTENANCE_POOL_CLAUDEMD_SECTION, MACHINE_WEDGE_CLAUDEMD_SECTION } from '../core/PostUpdateMigrator.js';
|
|
24
24
|
import { MINIMUM_OPERATOR_EFFORT_CLAUDEMD_SECTION } from '../core/PostUpdateMigrator.js';
|
|
25
25
|
import { CODEX_DEVICE_LIMIT_CLAUDEMD_BULLET } from '../core/PostUpdateMigrator.js';
|
|
26
26
|
import { UPDATE_APPLY_VERIFY_HOLD_CLAUDEMD_LINE } from '../core/PostUpdateMigrator.js';
|
|
@@ -459,6 +459,7 @@ ${CODEX_ARM_SWEEPER_CLAUDEMD_SECTION(port)}
|
|
|
459
459
|
${MACHINE_MAINTENANCE_CLAUDEMD_SECTION(port)}
|
|
460
460
|
${MACHINE_BROWSER_CARE_CLAUDEMD_SECTION(port)}
|
|
461
461
|
${MACHINE_MAINTENANCE_POOL_CLAUDEMD_SECTION(port)}
|
|
462
|
+
${MACHINE_WEDGE_CLAUDEMD_SECTION(port)}
|
|
462
463
|
|
|
463
464
|
### Cold-Start Lifeline Fallback (no silent resource rejection)
|
|
464
465
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
- On a development agent, one agent per machine login now watches for a wedged machine: every 10 minutes (every 2 while anything looks wrong) it checks whether the system can still list its processes in time, whether processes are piling up stuck, whether the agent server is impaired or lagging while the processor sits idle, and, on Linux and WSL, whether a no-op launch through the system still works.
|
|
9
|
+
- When the same two of those signs hold for 20 minutes, it records a wedge candidate, asks a judge model once what it would do (leave it, gentle steps, restart in the next small-hours window, or restart now), and writes down the steps it WOULD have taken. Nothing is restarted, closed, dismissed or stopped: those steps are not built.
|
|
10
|
+
- One sign alone for 2 hours, or any candidate, marks the next maintenance window as due early, on paper only.
|
|
11
|
+
- `GET /machine-maintenance` now has a `wedge` block (and so does the pool view). An unread sign reads `unknown`, never healthy. Audit: `logs/machine-maintenance.jsonl`.
|
|
12
|
+
- A real operator notice for a confirmed candidate exists but is OFF unless `machineMaintenance.wedgeRestart.dryRunNotice` is set to true.
|
|
13
|
+
|
|
14
|
+
## What to Tell Your User
|
|
15
|
+
|
|
16
|
+
Nothing yet: this is watch-only evidence gathering on development agents, it sends no messages by default, and it cannot restart anything. If they ask whether a machine is stuck, the agent can now read real evidence instead of guessing.
|
|
17
|
+
|
|
18
|
+
## Summary of New Capabilities
|
|
19
|
+
|
|
20
|
+
- `GET /machine-maintenance` → `wedge`: dry-run wedge detection, the judge's recorded proposal, and `forcedDueAt`.
|
|
21
|
+
|
|
22
|
+
## Evidence
|
|
23
|
+
|
|
24
|
+
- Unit tests drive the window rules on both sides of every limit (20 minutes, 3 observations, a 6-minute gap, 6-minute freshness, 2 hours for one sign, 6 hours since boot), the sampler's single-judge-per-condition rule across a restart and a maintainer takeover, the 24-hour judge budget, and the notice's default-off flag.
|
|
25
|
+
- An end-to-end test starts the sampler the way the server does, reads this machine's real process table, and gets a live `wedge` block from the real server.
|
|
26
|
+
- On macOS the GUI-path sign is not read yet (the canary app is not shipped), so a Mac can only form a candidate from the process table and the agent itself.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
- A session started directly into a topic (the spawn request with a written task) now starts on that topic's pinned model and engine, unless the request names its own. It also records what it started with.
|
|
9
|
+
- After a server restart, sessions with no record of how they started are no longer restarted "to apply the pinned profile". That restart was killing freshly started tasks: the new session rebuilt its context from the topic's chat history, which never held the task, so it sat idle. It happened more than 20 times between 2026-11-01 and 2026-11-03.
|
|
10
|
+
- When a directly started session does need a real profile restart that starts a new conversation, its original task brief is carried into the new session.
|
|
11
|
+
|
|
12
|
+
## What to Tell Your User
|
|
13
|
+
|
|
14
|
+
- **Started tasks keep going after restarts**: "When I start a task in one of our topics, a server restart no longer makes me drop it to switch models. I start on the topic's model to begin with."
|
|
15
|
+
|
|
16
|
+
## Summary of New Capabilities
|
|
17
|
+
|
|
18
|
+
| Capability | How to use |
|
|
19
|
+
|---|---|
|
|
20
|
+
| Direct topic spawns honour the topic profile | automatic |
|
|
21
|
+
| Profile restarts carry a direct spawn's brief | automatic |
|
|
22
|
+
|
|
23
|
+
## Evidence
|
|
24
|
+
|
|
25
|
+
- On 2026-11-03 two bug sessions (topics 10000001407 and 10000001426) were each restarted within seconds of a server boot by the profile check. The profile log shows `boot-sweep-armed` and then `respawn-applied` with `method: continuation, fresh: true`. Their spawn prompts were not in the topic history, so both sessions idled.
|
|
26
|
+
- The profile check's durable record had no entry for either session before the restart. Only the orchestrator's own restart wrote one.
|
|
27
|
+
- New tests: the direct-spawn route test (`tests/integration/sessions-spawn-topic-profile.test.ts`) and the full-server test (`tests/e2e/topic-profile-direct-spawn-lifecycle.test.ts`) spawn into a pinned topic, simulate a server restart, and confirm there is no restart. A session with no record is left alone, and a real pin change names the replaced session so its brief can be carried.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# ELI16 — Profile restarts stop wiping out freshly started tasks
|
|
2
|
+
|
|
3
|
+
## The problem in plain words
|
|
4
|
+
|
|
5
|
+
A conversation topic can have a "pinned profile": which AI engine and which model its session should run on. Instar keeps a small record of which profile each running session was actually started with. When the server restarts, it checks every running session against that record. If the record says "this session is on the wrong profile", it restarts the session on the right one.
|
|
6
|
+
|
|
7
|
+
There are two ways to start a session in a topic. A normal one starts when a message arrives in the topic. A direct one is started through the server's spawn request with a written task (the "brief"). The direct way never looked up the topic's profile, and it never wrote down what it started with.
|
|
8
|
+
|
|
9
|
+
So after the next server restart, the check found no record for those sessions and treated "no record" as "wrong profile". It restarted them. The restarted session rebuilds its memory from the topic's chat history, but a direct spawn's brief is never posted to the chat. The new session saw nothing to do, sat idle, and the task was lost. This happened to two bug-fixing sessions on 2026-11-03, and the profile log shows the same unneeded restart more than 20 times since 2026-11-01.
|
|
10
|
+
|
|
11
|
+
## The fix
|
|
12
|
+
|
|
13
|
+
1. **Direct spawns use the topic's profile.** If the request doesn't name an engine or model, the direct spawn looks up the topic's profile (from the same place normal spawns use) and starts on it. Then it writes down what it started with, so the restart check has a true record.
|
|
14
|
+
2. **No record no longer means wrong.** If the check finds a running session with no record at all, it leaves the session alone and logs that. The pinned profile still takes effect the next time that session starts normally.
|
|
15
|
+
3. **A real profile restart keeps the task.** The direct spawn also saves its brief. If the session is later restarted for a real profile change and the restart has to start a new conversation, the brief goes into the new session's opening message. The new session is told to check the history first, so it won't redo finished work. The saved brief only goes to the session it belongs to (and to that session's replacement), never to an unrelated later session in the same topic.
|
|
16
|
+
|
|
17
|
+
## What changed and what didn't
|
|
18
|
+
|
|
19
|
+
- Changed: direct spawns into a topic now run on that topic's profile unless the request names its own engine or model. A request that names one still gets what it asked for.
|
|
20
|
+
- Changed: the server no longer restarts sessions just because it has no record of how they started.
|
|
21
|
+
- Unchanged: when the record shows a session really is on the wrong profile, it is still restarted the same way, at the same moment (only when the session is idle).
|
|
22
|
+
- Unchanged: spawns with no topic behave exactly as before.
|
|
23
|
+
|
|
24
|
+
A revert of this one commit fully undoes it. There is no data migration. Saved briefs are small files in the agent's state folder.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Watching for a stuck computer, without touching it (plain English)
|
|
2
|
+
|
|
3
|
+
Sometimes a computer gets stuck in a way that is hard to see from the outside. On 1 November the Mac Studio was like that: nobody could reach it remotely, apps refused to open, and only a restart fixed it. The plan is for Instar to notice this on its own and, one day, restart the machine safely. This change builds only the noticing part, and it is careful to do nothing else.
|
|
4
|
+
|
|
5
|
+
Every ten minutes, one agent on each computer looks at a few simple signs. Can the computer still list its running programs quickly? Are programs piling up stuck, waiting on the disk? Is the agent's own server struggling even though the processor is mostly idle? On Linux and on Windows, can the system still start a tiny program that does nothing? If any sign looks bad, it checks every two minutes instead.
|
|
6
|
+
|
|
7
|
+
One bad sign is not enough. A busy computer can look slow for a moment. So the rule is strict: the same two different signs have to be bad at every check for twenty minutes, with at least three checks in that time. If either sign looks fine even once, the clock starts over. If the checks stop for more than six minutes, the clock also starts over, so a gap can never be counted as "still stuck".
|
|
8
|
+
|
|
9
|
+
When that happens, the agent asks an AI judge one question: should we leave it alone, try gentle fixes, restart in the small hours, or restart now? The judge only sees numbers and short labels, never what is on the screen. Its answer is written down, together with the steps that would have followed. None of those steps run, because they are not built yet. If the computer was restarted less than six hours ago, the answer is capped at "tell the owner", because restarting again would not help.
|
|
10
|
+
|
|
11
|
+
If one sign stays bad for two hours on its own, nothing is asked, but the next night-time maintenance slot is marked as due early, only on paper for now.
|
|
12
|
+
|
|
13
|
+
There is also a real message to the owner saying "this machine looks stuck", but it is switched off in this version. When someone turns it on, it says plainly that nothing was done and that the restart button does not exist yet. It can only be sent once per stuck episode, and at most twice a day, so a computer that keeps flickering cannot flood anyone.
|
|
14
|
+
|
|
15
|
+
On a Mac, one of the four signs cannot be read yet, so a Mac can only be flagged by the other two. The status page says so in plain words rather than pretending that sign is fine. Anything that could not be read shows as "unknown", never as "healthy".
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Side-Effects Review — Machine wedge detection, dry-run (Part E)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `machine-wedge-dryrun`
|
|
4
|
+
**Date:** `2026-11-03`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `coordinator's reviewer — concern raised, fixed, re-check concurred (2026-11-03 00:52 PST) (see the Second-pass review section)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Implements the dry-run half of Part E of `docs/specs/machine-self-maintenance.md` (CMT-1202). A new `WedgeMonitor` (`src/monitoring/machineMaintenance/WedgeMonitor.ts`) samples the evidence families (F1 GUI path, F2 process table, F4 agent symptoms; F3 reported `not-covered`) every 10 minutes, every 2 while any is positive, through four new fixed entries in the read-only command table (`readings.ts`: `ps -axo stat=`, `systemd-run --user … /bin/true`, `cmd.exe /c exit 0`, `tasklist.exe`). Pure window rules live in `wedgeWindow.ts`; probe interpretation in `wedgeEvidence.ts`. On a shared login only the host maintainer samples (new heartbeat + published state in `HostMaintenanceStore`, 10-minute takeover). A two-family candidate gets one judge call per wedge condition (`src/monitoring/MachineWedgeJudge.ts`, attribution `MachineWedgeJudge`, provenance decision point `machine-wedge-judge`), and the verdict row records the rungs that WOULD run. A candidate, or one family positive for 2 hours, sets `forcedDueAt`, which the paper drain (`DrainRehearsal`) now reads into its `due` flag. The rung-7 operator notice (`src/monitoring/MachineWedgeNotice.ts`) exists behind its own flag `machineMaintenance.wedgeRestart.dryRunNotice`, **default OFF in this increment**, and passes the self-action governor (`machine-wedge-notice`). `GET /machine-maintenance` and its pool view gain a `wedge` block. Wiring in `commands/server.ts` sits inside the existing dev-gated, throwaway-excluded machine-maintenance block.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `Is this machine wedged and what to do` (spec: `judgment-candidate`) — add — floor: two-family, 3-observation, 20-minute window with the 6-minute gap and freshness rules (code, `wedgeWindow.ts`); arbiter `machineWedgeJudge` with a bounded space of four; deterministic fallback (gentle rungs + `forcedDueAt`) on no provider, error, unparseable answer or budget. In this build the arbiter's output is recorded only.
|
|
15
|
+
- `forcedDueAt` → paper drain `due` — add — a signal into the watch-only rehearsal.
|
|
16
|
+
- `Wedge maintainer` (host heartbeat) — add — who samples on a shared login.
|
|
17
|
+
- `Send the dry-run notice` — add — invariant gates: flag on, one per wedge condition, ≤1 ordinary + ≤1 breaker notice per 24 h, governor admission.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. Over-block
|
|
22
|
+
|
|
23
|
+
No block/allow surface — over-block not applicable. Nothing in this change refuses, holds or stops anything; it only records. The closest analogue is a false candidate: a healthy-but-saturated machine. F4 reads `negative` when the processor is busy (idle < 30%), so a saturated machine does not feed F4; and a candidate needs a second, causally different family as well.
|
|
24
|
+
|
|
25
|
+
**Kept deliberately, and its cost:** F4's `cpu-busy-not-wedged` is a NEGATIVE observation, so it RESETS every pair that includes F4 (`F1+F4`, `F2+F4`), and the single-family F4 run. A machine that is wedged AND has a busy processor (for example a runaway process pinning the CPU while the GUI path is stuck) cannot form an F4 pair candidate; only `F1+F2` can still form. This is the spec's own F4 definition ("while CPU idle >= 30%"), chosen to keep healthy-but-saturated machines from forming candidates; the second-pass review asked that it stay as is and that the reset be stated here.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. Under-block
|
|
30
|
+
|
|
31
|
+
No block/allow surface. As a detector it misses, by construction and stated in the status `limits`:
|
|
32
|
+
- **macOS F1 is unread** (`canary-not-shipped`): the spec's signed no-UI canary app is not shipped and the capture probe is not wired. A Mac can only form a candidate from F2 + F4. The 2026-11-01 Studio incident (GUI wedged, `-600` launch failures) would likely have shown F1 only, so this build would probably NOT have caught it. That is the single largest gap.
|
|
33
|
+
- F2's kernel-zone reading (`data.kalloc.1024`) is not wired (`kernelZoneRead: false` in the detail).
|
|
34
|
+
- F4's "session spawn failures for system reasons" has no counter yet; it reads `null` (unknown), never zero.
|
|
35
|
+
- F3 is not built.
|
|
36
|
+
- F4 negative on a busy processor resets F4 pairs (see section 1).
|
|
37
|
+
- The window is in memory: a server restart or maintainer takeover starts it over (the gap rule would anyway), so a wedge that restarts the agent server every 15 minutes never forms a candidate.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 3. Level-of-abstraction fit
|
|
42
|
+
|
|
43
|
+
Detection is a low-level deterministic floor (window arithmetic over fixed probes), and the judgment sits at the authority level (one LLM call with structured context), matching the spec's Judgment Within Floors split. It uses existing primitives instead of re-implementing: the read-only command table (one `execFile`), the host store's lock, the machine load sampler (F4), the honest lock reading (F1 eligibility on macOS), the shared intelligence provider, the provenance seam, the self-action governor and the existing attention-item path. The judge and notice live outside the maintenance folder so the folder's source lint (no actor, import allowlist) still holds.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 4. Signal vs authority compliance
|
|
48
|
+
|
|
49
|
+
- [x] No — this change has no block/allow surface.
|
|
50
|
+
|
|
51
|
+
Every output is a record: audit rows, a status block, a `forcedDueAt` timestamp read by a paper drain that itself cannot act, and (only if the flag is turned on) one attention item. The judge's proposal has no consumer that acts on it.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 4b. Judgment-point check (Judgment Within Floors standard)
|
|
56
|
+
|
|
57
|
+
The wedge decision is a competing-signals point and is handled as the spec declares in `## Decision points touched`: floor (two-family window) + arbiter (`machineWedgeJudge`) + deterministic end rung. The static window rules are the floor, not the decision. The notice gates (one per condition, 24-hour cap) are invariants on a self-triggered emit, not a judgment.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 5. Interactions
|
|
62
|
+
|
|
63
|
+
- **Shadowing:** none. The monitor runs on its own 60 s timer, independent of the drain rehearsal and browser care.
|
|
64
|
+
- **Double-fire:** the rehearsal and the wedge monitor both read `kern.boottime`/`os.uptime`; reads only. The notice uses the same attention path as other system items; its id is `machine-wedge:<episode>` so the store dedupes a repeat.
|
|
65
|
+
- **Races:** the maintainer claim and the published state go through `HostMaintenanceStore.withLock` (200 ms bounded wait, then "not the maintainer this tick"). A write from a stale generation is refused. Two agents on one login never both sample; verified by unit test.
|
|
66
|
+
- **Feedback loops:** `ps`, `systemd-run` and `cmd.exe` add a little load every 2–10 minutes; at most four short children per sample, hard timeouts 5–10 s (SIGKILL), single-flight. A child stuck in the kernel cannot be killed by any signal; the runner settles it anyway after timeout + 500 ms as `timedOut` (code `no-exit`), and while that child is still out there the same probe is not started again (code `previous-still-running`), so a wedged machine holds at most one stuck child per probe (four in all) instead of one per sample. F4 reads the agent's own event-loop lag; sampling does not run on the event loop (async children), so it cannot meaningfully feed its own F4 signal.
|
|
67
|
+
- **Rehearsal:** `forcedDueAt` makes the paper drain record `due: true` for the next window(s) until a boot. That changes rehearsal evidence rows (`due`) only.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 6. External surfaces
|
|
72
|
+
|
|
73
|
+
- **Other agents on the same machine:** yes, deliberately: two new host files `~/.instar/host-maintenance/wedge-maintainer.json` and `wedge-state.json`, readable by every agent on the login. Only the maintainer writes them.
|
|
74
|
+
- **Install base:** dark on the fleet (machineMaintenance dev-agent gate). Throwaway agents excluded.
|
|
75
|
+
- **External systems:** one LLM call per wedge condition (≤6 per 24 h) through the shared provider. Telegram only if the notice flag is turned on.
|
|
76
|
+
- **Persistent state:** the two host files and `logs/machine-maintenance.jsonl` (2 MB rotation, metadata only: reason codes, numbers, ids).
|
|
77
|
+
- **Linux:** `systemd-run --user` starts one transient no-op unit per sample (collected). On a machine without a user manager it fails; F1 then stays `ineligible` because the probe never passed this boot.
|
|
78
|
+
- **Operator surface:** no operator-facing action is added. The notice, when enabled, says plainly that nothing was done and the restart button is not built; it offers no control that does nothing.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 6b. Operator-surface quality
|
|
83
|
+
|
|
84
|
+
No operator surface — not applicable (no dashboard file touched).
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 7. Multi-machine posture
|
|
89
|
+
|
|
90
|
+
**machine-local BY DESIGN, proxied-on-read.** Wedge samples are readings of this machine's own hardware and session (spec: `machine-local-justification: hardware-bound-resource`). The pool-wide question is answered by the existing `GET /machine-maintenance?scope=pool`, whose self row now carries `wedge`. Notices are one-voice per host (only the maintainer can send) and per machine (episode ids carry the machine id). Durable state lives in the host store, so a co-resident takeover inherits it; it does not strand on topic transfer (not topic-bound). No URLs generated.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 8. Rollback cost
|
|
95
|
+
|
|
96
|
+
Pure code change. Revert and ship a patch, or set `machineMaintenance.enabled: false` (read live: the monitor stops sampling at its next tick). Leftover host files are inert when nothing reads them; deleting `~/.instar/host-maintenance/wedge-*.json` is safe. No user-visible regression during rollback: the notice is off by default.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Conclusion
|
|
101
|
+
|
|
102
|
+
The dry-run half of Part E is built and bounded: sampling, window rules, maintainer rule, judge with provenance, `forcedDueAt`, status, audit, and the rung-7 notice behind its own default-off flag. Nothing actuates. Main honest gaps: macOS F1 unread, F3 and the kernel-zone and spawn-failure readings not wired, enrolment absent (so "maintainer" is first-claim, not the enrolling agent), no operator dismissal of `forcedDueAt`, and the Jev-first cascade not wired (the judge uses the shared provider's routing). The second-pass review raised a blocking hang (a probe child stuck in the kernel stopped the sampler) and two durability gaps; all three are fixed below and await the reviewer's re-check.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Second-pass review
|
|
107
|
+
|
|
108
|
+
**Reviewer:** coordinator's second-pass reviewer.
|
|
109
|
+
**Independent read of the artifact: concern raised.** Re-check of the fixes below is PENDING.
|
|
110
|
+
|
|
111
|
+
1. **BLOCKING — a probe child stuck in the kernel (U/D state) never exits**, so `execFile`'s timeout never fires its callback. `runCall` (readings.ts) never settled, `Promise.all` in `WedgeMonitor.sample()` never settled, `sampling` stayed true for good, and the heartbeat stopped: on exactly the machine the feature exists for, the sampler would go silent after one sample. **Fixed:**
|
|
112
|
+
- `runCall` now races the child against its own timer and settles `{ timedOut: true, code: 'no-exit' }` at timeout + 500 ms whether or not the child ever exits; `killSignal: 'SIGKILL'`; a synchronous spawn throw (EAGAIN) settles as a failure.
|
|
113
|
+
- A probe name whose child never exited is not started again (`previous-still-running`, still a timed-out result), so stuck children cannot pile up.
|
|
114
|
+
- The monitor bounds every await of a sample independently of the runner: each probe (timeout + 2 s → timed-out), boot-time and lock readings (10 s → null/unknown), the judge (120 s → fallback `judge-timeout`) and the notice (60 s → `failed`). A sample always completes and `sampling` always resets.
|
|
115
|
+
- Timed-out results are evidence: F2 reads `ps-timed-out` (positive), F1 `probe-timed-out` (positive once the probe has passed this boot), WSL `tasklist-timed-out`.
|
|
116
|
+
- Tests: a fake spawner that never calls back — the sample completes within the bound, F2 is `ps-timed-out`, the next tick samples again, and no second child is started; the runner settles at exactly timeout + grace and asks for SIGKILL; a probe dependency that never settles is bounded by the monitor.
|
|
117
|
+
2. **Unknown boot time was treated as "not recent"**, so a `restart-now` proposal would record rungs 1–5 on a machine that may have just restarted. **Fixed:** `recentlyRestarted === null` now caps the outcome like a recent restart (`cappedAtNotice: true`, `cappedBecause: 'boot-time-unknown'`, `bootTime: 'unknown'`, no rungs recorded). Test added.
|
|
118
|
+
3. **The judge call was persisted only after it returned**, so a crash or maintainer takeover mid-call would call the judge again for the same condition. **Fixed:** the call is counted in `judgeCalls` and the condition is marked judged, then published to the host store, BEFORE the judge is awaited; the same is done before the notice is awaited (`notice: 'pending'`), so a crash mid-send never sends a second one. The cost, accepted: a crash mid-call leaves that condition with no recorded verdict. Test: the host file read from inside the judge call already holds the call and the judged condition, and a restarted monitor on the same store never calls the judge again.
|
|
119
|
+
4. **F4 `cpu-busy-not-wedged` resets a pair.** Kept as is at the reviewer's request; stated in sections 1 and 2.
|
|
120
|
+
|
|
121
|
+
Reviewer re-check of 1–3: **pending.**
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Evidence pointers
|
|
126
|
+
|
|
127
|
+
- `tests/unit/machine-wedge-window.test.ts`, `tests/unit/machine-wedge-monitor.test.ts`, `tests/unit/machine-wedge-judge.test.ts`
|
|
128
|
+
- `tests/integration/machine-wedge-route.test.ts`
|
|
129
|
+
- `tests/e2e/machine-wedge-lifecycle.test.ts`
|
|
130
|
+
- `tests/unit/self-action-convergence.test.ts` (controller `machine-wedge-notice`)
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Class-Closure Declaration (display-only mirror)
|
|
135
|
+
|
|
136
|
+
- **defectClass:** `unbounded-self-action` — this change adds a self-triggered recurring controller: the `WedgeMonitor` sampler, which on its own evidence calls an LLM judge and (flag on) raises an operator notice.
|
|
137
|
+
- **closure:** `guard`
|
|
138
|
+
- **guardEvidence.citation:** `tests/unit/self-action-convergence.test.ts` (controller `machine-wedge-notice` in `src/testing/selfActionRegistry.ts`, `modelsPath: src/monitoring/MachineWedgeNotice.ts`), plus the governor policy `machine-wedge-notice` in `src/monitoring/selfaction/policies.ts`.
|
|
139
|
+
- **guardEvidence.enforcement:** `ratchet`
|
|
140
|
+
- **guardEvidence.howCaught:** under the pinned worst case (two families positive at every 2-minute sample forever, notice flag ON) the notice fires ONCE: the judge and the notice are keyed on a durable wedge *condition* in the host store, which closes only on a real negative observation of one of its families or a new boot. A server restart or maintainer takeover starts a fresh window and re-forms the candidate as a new episode, but it is the same condition, so no second judge call and no second notice (`boundK` 1, `perTargetBoundK` 1 over 24 h and at 2× the horizon; same bound when reconstructed at 25/50/75% and before every tick). For a flapping machine (outside the fixture) notices are further capped at one ordinary and one breaker notice per 24 h, and the governor class allows 1 per machine and 3 in all per 24 h.
|
|
141
|
+
- **component:** `WedgeMonitor` / `MachineWedgeNotice`
|
|
142
|
+
|
|
143
|
+
**The sampler itself and registration.** The recurring work that is not a notice is bounded in code and does not need its own registry entry: one sample at a time (single-flight), at most one every 2 minutes and one every 10 while nothing is positive; each sample runs at most four fixed read-only children with 5–10 s hard timeouts; the 60 s heartbeat is one lock + one small file write. The LLM judge is at most one call per wedge condition and at most `JUDGE_MAX_PER_DAY` (6) per rolling 24 h, persisted in the host store so a restart cannot reset it, and runs only on the host maintainer. Registration in `selfActionRegistry.ts` is done for the emit that reaches a person (`machine-wedge-notice`); the probes are reads, and the judge call is an LLM call bounded by the budget above and enrolled in the Decision Quality census (`machine-wedge-judge`, `budget:20`).
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
Reviewer re-check (2026-11-03 00:52 PST): Concur with the review. All three fixes hold. Residual note: the second-run guard is per process, so the worst case is about one stuck `ps` per agent on the login, which is bounded.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Side-Effects Review — profile restarts stop wiping out freshly started tasks
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `profile-restart-keeps-spawn-brief`
|
|
4
|
+
**Date:** `2026-11-03`
|
|
5
|
+
**Author:** `Echo`
|
|
6
|
+
**Second-pass reviewer:** `independent subagent (see the end of this file)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Echo Bugs topic 10000001430. A session started with `POST /sessions/spawn` and a `topicId` was restarted by the topic-profile boot sweep at the next server boot ("this topic's pinned profile wasn't applied before my last restart"). The restart was a fresh CONTINUATION that rebuilt context from chat history, which never held the spawn prompt, so the new session idled and the task was lost. Root cause: the direct spawn route neither resolved the topic profile nor recorded what it launched, and `bootReconcileSweep` read a missing `lastApplied` record as divergence. Every non-orchestrator spawn path that skipped `recordSpawnSuccess` was therefore restarted at every boot. The profile audit log shows 20+ `boot-sweep-armed` rows from 2026-11-01 to 2026-11-03.
|
|
11
|
+
|
|
12
|
+
Three changes, one class:
|
|
13
|
+
|
|
14
|
+
1. `POST /sessions/spawn` with a topic resolves the topic profile (via `resolveTopicSpawnProfile`, the same resolver `spawnSessionForTopic` uses) for framework and model when the request leaves them unset, then calls `orchestrator.recordSpawnSuccess` with what it actually launched. thinkingMode/effort are recorded as null because this path has no launch control for them.
|
|
15
|
+
2. `bootReconcileSweep` skips a live session with no `lastApplied` record (audit `boot-sweep-unknown-applied`) instead of arming a respawn.
|
|
16
|
+
3. A new `TopicSpawnBriefStore` keeps the direct spawn's prompt, keyed by topic and tmux session. The orchestrator's spawn directive now names `replacedSessionName`. On a `continuation` respawn the server's spawn port passes the brief to `spawnSessionForTopic` (`carriedBrief`), which adds it to the bootstrap before the handoff instruction. The brief is then re-keyed to the new session.
|
|
17
|
+
|
|
18
|
+
Tier 1, declared. 5 source files, about 200 LOC of source plus tests in all three tiers. One commit attempt; no spec rounds (narrow fix under the existing TOPIC-PROFILE-SPEC, whose §8 boot-sweep text is updated).
|
|
19
|
+
|
|
20
|
+
## Decision-point inventory
|
|
21
|
+
|
|
22
|
+
- Boot sweep "restart this session?" — modify — a missing record is no longer a reason to kill.
|
|
23
|
+
- Direct spawn's model/framework — modify — profile fills unset axes; explicit request values still win.
|
|
24
|
+
- Respawn bootstrap content — add — a carried brief on continuation respawns of the brief's own session.
|
|
25
|
+
|
|
26
|
+
## 1. Over-block
|
|
27
|
+
|
|
28
|
+
The boot sweep now kills fewer sessions. A session that really is on the wrong profile and has no record (spawned before this release, or by an unknown path) is no longer corrected at boot. It is corrected at its next natural spawn, and any pin change made while it is live still goes through the normal respawn path. That trade is deliberate: an unknown launch is not evidence, and acting on it destroyed live work.
|
|
29
|
+
|
|
30
|
+
## 2. Under-block
|
|
31
|
+
|
|
32
|
+
- A direct spawn into a topic whose profile pins thinkingMode or effort records null for those. The boot sweep will still see a real difference and restart the session once to apply them. That restart now carries the brief, so the task survives.
|
|
33
|
+
- A crash or compaction recovery that restarts a direct-spawned session goes through `spawnSessionForTopic`, so it clears the brief rather than carrying it. That recovery already rebuilt context from chat history alone, so the brief was lost there before this change too. Recovery continuity is outside this profile-restart fix and is filed as feedback.
|
|
34
|
+
- Other spawn paths that bind a topic without going through `spawnSessionForTopic` or this route (if any exist) are covered by change 2. With no record, they are left alone rather than killed.
|
|
35
|
+
|
|
36
|
+
## 3. Level-of-abstraction fit
|
|
37
|
+
|
|
38
|
+
Profile resolution stays in the one resolver. The route only fills unset axes, mirroring `spawnSessionForTopic`. Recording uses the existing `recordSpawnSuccess` entry point. The brief carry is in the server's spawn port, the only place that has both the replaced session and the bootstrap builder.
|
|
39
|
+
|
|
40
|
+
## 4. Signal vs authority compliance
|
|
41
|
+
|
|
42
|
+
This removes kill authority from a no-evidence case. It adds no new blocking. The boot sweep keeps its authority only for recorded divergence.
|
|
43
|
+
|
|
44
|
+
## 4b. Judgment-point check
|
|
45
|
+
|
|
46
|
+
None added.
|
|
47
|
+
|
|
48
|
+
## 5. Interactions
|
|
49
|
+
|
|
50
|
+
- `recordSpawnSuccess` also resets the §10.4 breaker and clears the suppression marker for the topic. A successful direct spawn is a successful spawn, so this is the right reading. The orchestrator-initiated path is unaffected (it does not go through this route).
|
|
51
|
+
- The `ultracode` framework guard now reads the profile-resolved framework, so a codex-pinned topic refuses `ultracode: true` honestly instead of launching on claude-code against its pin.
|
|
52
|
+
- Brief carry happens only on `continuation` respawns. A `resume` respawn keeps the whole transcript, so the prompt is already there. Briefs are matched to the replaced session's tmux name. Topic sessions reuse one name, so that match alone is not enough: every spawn through `spawnSessionForTopic` that the orchestrator did not start clears the topic's brief. A later natural or user-driven session therefore never inherits a finished task.
|
|
53
|
+
- `spawnSessionForTopic` ignores `carriedBrief` when a user message drives the spawn, or on `silentStart`.
|
|
54
|
+
|
|
55
|
+
## 6. External surfaces
|
|
56
|
+
|
|
57
|
+
Direct topic spawns may now launch on a different model than before (the topic's pin instead of the account default). That is the intended behaviour: the profile is what the operator asked for. New state: small JSON files under `.instar/state/topic-spawn-briefs/`.
|
|
58
|
+
|
|
59
|
+
## 6b. Operator-surface quality
|
|
60
|
+
|
|
61
|
+
The misleading "pinned profile wasn't applied" notice stops firing for sessions that just started on the right profile.
|
|
62
|
+
|
|
63
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
64
|
+
|
|
65
|
+
machine-local BY DESIGN: the brief and the `lastApplied` record describe a tmux session on this machine, and the orchestrator side-state is already machine-local. A topic moved to another machine is spawned there by that machine's spawn path, which records its own launch.
|
|
66
|
+
|
|
67
|
+
## 8. Rollback cost
|
|
68
|
+
|
|
69
|
+
Revert the commit. Leftover brief files are inert.
|
|
70
|
+
|
|
71
|
+
## Conclusion
|
|
72
|
+
|
|
73
|
+
Safe to ship. It removes a kill path that had no evidence behind it, closes the reason that path fired, and keeps the task for the restarts that are still legitimate.
|
|
74
|
+
|
|
75
|
+
## Evidence pointers
|
|
76
|
+
|
|
77
|
+
- `logs/topic-profile-changes.jsonl` 2026-11-03: `boot-sweep-armed` 10000001407 at 05:45:05Z leading to `respawn-applied continuation fresh` at 06:08:33Z. `boot-sweep-armed` 10000001426 at 07:17:09Z leading to `respawn-applied continuation fresh` at 07:17:20Z.
|
|
78
|
+
- `logs/server.log`: 10000001426 was bound by the direct spawn route at 07:03:40Z with no `[spawnSessionForTopic]` line, i.e. no profile record.
|
|
79
|
+
- Tests: `tests/unit/TopicProfileOrchestrator.test.ts` (unknown-applied is left alone; a matching record is not restarted; the respawn names the replaced session), `tests/unit/TopicSpawnBriefStore.test.ts`, `tests/unit/session-spawn-topic-binding.test.ts` (`resolveTopicSpawnProfile`), `tests/unit/topic-profile-server-wiring.test.ts` (port carries the brief on continuation only, same store instance as the routes), `tests/integration/sessions-spawn-topic-profile.test.ts`, `tests/e2e/topic-profile-direct-spawn-lifecycle.test.ts`.
|
|
80
|
+
|
|
81
|
+
## Class-Closure Declaration
|
|
82
|
+
|
|
83
|
+
- defect class: `unbounded-self-action`
|
|
84
|
+
- closure: `structural`
|
|
85
|
+
- reason: the self-triggered restart is now gated on recorded evidence. The route that produced the missing evidence records it.
|
|
86
|
+
|
|
87
|
+
## Second-pass review
|
|
88
|
+
|
|
89
|
+
Independent subagent, 2026-11-03. **Concern raised:** topic-path sessions reuse one tmux name (built from the topic name) and briefs never expired. A brief re-keyed to that name would be carried into a later, unrelated session's profile respawn, bringing back a finished task. **Fixed:** `spawnSessionForTopic` clears the topic's brief on every spawn the orchestrator did not start (`TopicSpawnBriefStore.clear`, through SafeFsExecutor). Tests: `clear` in the store test, plus a wiring assertion. The reviewer's other notes were accepted as documented behaviour and need no change. In each, the restart is a legitimate one and the brief is now carried: a local-model binding or a thinking/effort default forces one boot restart, and an explicit request override is switched back to the pin. The reviewer judged the null-record skip safe, because the pin-change path still treats a missing record as divergence. The reviewer also judged the ultracode guard and the bootstrap formatting correct.
|
|
90
|
+
|
|
91
|
+
Re-check after the fix (same reviewer): **Concur.** The clear runs only after the new session spawned. Orchestrator respawns still carry and re-key the brief. The direct route does not call `spawnSessionForTopic`, so a freshly recorded brief is not wiped.
|