instar 1.3.1650 → 1.3.1652
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/init.d.ts.map +1 -1
- package/dist/commands/init.js +10 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/install/engine.d.ts +2 -14
- package/dist/commands/install/engine.d.ts.map +1 -1
- package/dist/commands/install/engine.js.map +1 -1
- package/dist/commands/install.d.ts +2 -10
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +15 -147
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/machine.d.ts +2 -0
- package/dist/commands/machine.d.ts.map +1 -1
- package/dist/commands/machine.js +19 -0
- package/dist/commands/machine.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +373 -14
- package/dist/commands/server.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +3 -0
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/slack-cli.d.ts.map +1 -1
- package/dist/commands/slack-cli.js +2 -1
- package/dist/commands/slack-cli.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +2 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/BackupManager.d.ts.map +1 -1
- package/dist/core/BackupManager.js +1 -0
- package/dist/core/BackupManager.js.map +1 -1
- package/dist/core/CodexLifecycleProductionComposition.d.ts +1 -1
- package/dist/core/CodexLifecycleProductionComposition.d.ts.map +1 -1
- package/dist/core/MachinePoolRegistry.d.ts +8 -0
- package/dist/core/MachinePoolRegistry.d.ts.map +1 -1
- package/dist/core/MachinePoolRegistry.js +25 -2
- package/dist/core/MachinePoolRegistry.js.map +1 -1
- package/dist/core/OverloadShedPlanner.d.ts +63 -7
- package/dist/core/OverloadShedPlanner.d.ts.map +1 -1
- package/dist/core/OverloadShedPlanner.js +169 -39
- package/dist/core/OverloadShedPlanner.js.map +1 -1
- package/dist/core/OwnershipReconciler.d.ts +11 -0
- package/dist/core/OwnershipReconciler.d.ts.map +1 -1
- package/dist/core/OwnershipReconciler.js +27 -9
- package/dist/core/OwnershipReconciler.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +18 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +106 -6
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/RebalancePlanner.d.ts +1 -0
- package/dist/core/RebalancePlanner.d.ts.map +1 -1
- package/dist/core/RebalancePlanner.js +1 -1
- package/dist/core/RebalancePlanner.js.map +1 -1
- package/dist/core/SessionDrainRunner.d.ts +10 -1
- package/dist/core/SessionDrainRunner.d.ts.map +1 -1
- package/dist/core/SessionDrainRunner.js +60 -3
- package/dist/core/SessionDrainRunner.js.map +1 -1
- package/dist/core/SessionManager.d.ts +53 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +217 -6
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/StageAdvancer.d.ts +19 -0
- package/dist/core/StageAdvancer.d.ts.map +1 -1
- package/dist/core/StageAdvancer.js +51 -1
- package/dist/core/StageAdvancer.js.map +1 -1
- package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
- package/dist/core/WriteDomainRegistry.js +12 -0
- package/dist/core/WriteDomainRegistry.js.map +1 -1
- package/dist/core/appInboundDispatch.d.ts +5 -0
- package/dist/core/appInboundDispatch.d.ts.map +1 -1
- package/dist/core/appInboundDispatch.js +5 -1
- package/dist/core/appInboundDispatch.js.map +1 -1
- package/dist/core/leaseMediumSelection.d.ts +8 -0
- package/dist/core/leaseMediumSelection.d.ts.map +1 -1
- package/dist/core/leaseMediumSelection.js +17 -0
- package/dist/core/leaseMediumSelection.js.map +1 -1
- package/dist/core/memorySandboxProbe.d.ts +19 -0
- package/dist/core/memorySandboxProbe.d.ts.map +1 -0
- package/dist/core/memorySandboxProbe.js +73 -0
- package/dist/core/memorySandboxProbe.js.map +1 -0
- package/dist/core/memorySessionPolicy.d.ts +44 -0
- package/dist/core/memorySessionPolicy.d.ts.map +1 -0
- package/dist/core/memorySessionPolicy.js +122 -0
- package/dist/core/memorySessionPolicy.js.map +1 -0
- package/dist/core/poolReadiness.d.ts +67 -0
- package/dist/core/poolReadiness.d.ts.map +1 -0
- package/dist/core/poolReadiness.js +156 -0
- package/dist/core/poolReadiness.js.map +1 -0
- package/dist/core/quietTopicScanner.d.ts +39 -0
- package/dist/core/quietTopicScanner.d.ts.map +1 -0
- package/dist/core/quietTopicScanner.js +57 -0
- package/dist/core/quietTopicScanner.js.map +1 -0
- package/dist/core/rebalanceProof.d.ts +114 -0
- package/dist/core/rebalanceProof.d.ts.map +1 -0
- package/dist/core/rebalanceProof.js +192 -0
- package/dist/core/rebalanceProof.js.map +1 -0
- package/dist/core/types.d.ts +18 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/data/stageBCertifiedSet.d.ts +12 -0
- package/dist/data/stageBCertifiedSet.d.ts.map +1 -1
- package/dist/data/stageBCertifiedSet.js +16 -0
- package/dist/data/stageBCertifiedSet.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/install/doorSteps.d.ts +72 -0
- package/dist/install/doorSteps.d.ts.map +1 -0
- package/dist/install/doorSteps.js +162 -0
- package/dist/install/doorSteps.js.map +1 -0
- package/dist/{commands/install → install}/doors.d.ts +4 -4
- package/dist/install/doors.d.ts.map +1 -0
- package/dist/{commands/install → install}/doors.js +6 -6
- package/dist/install/doors.js.map +1 -0
- package/dist/messaging/SpawnRequestManager.d.ts +2 -0
- package/dist/messaging/SpawnRequestManager.d.ts.map +1 -1
- package/dist/messaging/SpawnRequestManager.js +3 -0
- package/dist/messaging/SpawnRequestManager.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts +20 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +69 -7
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/messaging/slack/ChannelManager.d.ts +3 -1
- package/dist/messaging/slack/ChannelManager.d.ts.map +1 -1
- package/dist/messaging/slack/ChannelManager.js +5 -2
- package/dist/messaging/slack/ChannelManager.js.map +1 -1
- package/dist/messaging/slack/SlackAdapter.d.ts +14 -0
- package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
- package/dist/messaging/slack/SlackAdapter.js +26 -0
- package/dist/messaging/slack/SlackAdapter.js.map +1 -1
- package/dist/monitoring/TriageOrchestrator.d.ts +1 -0
- package/dist/monitoring/TriageOrchestrator.d.ts.map +1 -1
- package/dist/monitoring/TriageOrchestrator.js +5 -0
- package/dist/monitoring/TriageOrchestrator.js.map +1 -1
- package/dist/native/InstarWedgeCanary.app/Contents/MacOS/InstarWedgeCanary +0 -0
- package/dist/native/instar-desktop-host +0 -0
- package/dist/recommended/offerPrerequisites.d.ts +16 -0
- package/dist/recommended/offerPrerequisites.d.ts.map +1 -0
- package/dist/recommended/offerPrerequisites.js +84 -0
- package/dist/recommended/offerPrerequisites.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +10 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/scheduler/JobScheduler.d.ts +2 -0
- package/dist/scheduler/JobScheduler.d.ts.map +1 -1
- package/dist/scheduler/JobScheduler.js +8 -1
- package/dist/scheduler/JobScheduler.js.map +1 -1
- package/dist/server/AgentServer.d.ts +2 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +3 -1
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +22 -5
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/memorySessionGuard.d.ts +29 -0
- package/dist/server/memorySessionGuard.d.ts.map +1 -0
- package/dist/server/memorySessionGuard.js +57 -0
- package/dist/server/memorySessionGuard.js.map +1 -0
- package/dist/server/middleware.d.ts +8 -1
- package/dist/server/middleware.d.ts.map +1 -1
- package/dist/server/middleware.js +22 -1
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/mobileCapacityRoutes.d.ts +258 -0
- package/dist/server/mobileCapacityRoutes.d.ts.map +1 -0
- package/dist/server/mobileCapacityRoutes.js +710 -0
- package/dist/server/mobileCapacityRoutes.js.map +1 -0
- package/dist/server/mobileConversationRoutes.js +1 -1
- package/dist/server/mobileConversationRoutes.js.map +1 -1
- package/dist/server/mobileOpsRoutes.d.ts +120 -0
- package/dist/server/mobileOpsRoutes.d.ts.map +1 -0
- package/dist/server/mobileOpsRoutes.js +432 -0
- package/dist/server/mobileOpsRoutes.js.map +1 -0
- package/dist/server/mobilePairingRoutes.d.ts.map +1 -1
- package/dist/server/mobilePairingRoutes.js +11 -0
- package/dist/server/mobilePairingRoutes.js.map +1 -1
- package/dist/server/recommendedServiceRoutes.d.ts +1 -0
- package/dist/server/recommendedServiceRoutes.d.ts.map +1 -1
- package/dist/server/recommendedServiceRoutes.js +2 -1
- package/dist/server/recommendedServiceRoutes.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +378 -137
- package/dist/server/routes.js.map +1 -1
- package/dist/testing/selfActionRegistry.d.ts.map +1 -1
- package/dist/testing/selfActionRegistry.js +8 -3
- package/dist/testing/selfActionRegistry.js.map +1 -1
- package/dist/threadline/PairingInviteTelegram.d.ts.map +1 -1
- package/dist/threadline/PairingInviteTelegram.js +5 -2
- package/dist/threadline/PairingInviteTelegram.js.map +1 -1
- package/dist/users/MemoryAudience.d.ts +131 -0
- package/dist/users/MemoryAudience.d.ts.map +1 -0
- package/dist/users/MemoryAudience.js +198 -0
- package/dist/users/MemoryAudience.js.map +1 -0
- package/dist/users/MemoryInboundTracker.d.ts +40 -0
- package/dist/users/MemoryInboundTracker.d.ts.map +1 -0
- package/dist/users/MemoryInboundTracker.js +62 -0
- package/dist/users/MemoryInboundTracker.js.map +1 -0
- package/dist/users/MemoryPrivacy.d.ts +132 -0
- package/dist/users/MemoryPrivacy.d.ts.map +1 -0
- package/dist/users/MemoryPrivacy.js +347 -0
- package/dist/users/MemoryPrivacy.js.map +1 -0
- package/dist/users/wireMemoryPrivacy.d.ts +38 -0
- package/dist/users/wireMemoryPrivacy.d.ts.map +1 -0
- package/dist/users/wireMemoryPrivacy.js +94 -0
- package/dist/users/wireMemoryPrivacy.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +68 -68
- package/src/data/feature-ladders.json +1 -1
- package/src/data/stageBCertifiedSet.ts +16 -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/data/state-coherence-registry.json +15 -0
- package/src/scaffold/templates.ts +10 -1
- package/src/templates/scripts/slack-reply.sh +3 -1
- package/upgrades/1.3.1651.md +30 -0
- package/upgrades/1.3.1652.md +74 -0
- package/upgrades/side-effects/memory-privacy-sandbox-setup.md +66 -0
- package/upgrades/side-effects/memory-privacy.md +79 -0
- package/upgrades/side-effects/mobile-native-capacity-screens.md +108 -0
- package/upgrades/side-effects/mobile-native-ops-screens.md +96 -0
- package/upgrades/side-effects/owner-checks-fail-closed.md +57 -0
- package/upgrades/side-effects/recommended-services-prereqs.md +69 -0
- package/upgrades/side-effects/session-pool-default-on.md +53 -0
- package/upgrades/side-effects/session-pool-rebalancing-finish.md +151 -0
- package/dist/commands/install/doors.d.ts.map +0 -1
- package/dist/commands/install/doors.js.map +0 -1
|
@@ -35,6 +35,22 @@ export const STAGE_B_CERTIFIED_SET = {
|
|
|
35
35
|
"file": "src/utils/jsonl-rotation.ts",
|
|
36
36
|
"reason": "Size-based log rotation used only by the evidence log in this closure; it never throws and does not alter the certified Codex Stage-B delivery path."
|
|
37
37
|
},
|
|
38
|
+
{
|
|
39
|
+
"file": "src/permissions/SlackPrincipalResolver.ts",
|
|
40
|
+
"reason": "Slack identity lookup reached only through memory privacy from the excluded SessionManager; no delivery logic, outside the Stage-B canaried path."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"file": "src/permissions/types.ts",
|
|
44
|
+
"reason": "Role type declarations reached through SlackPrincipalResolver; types and constants only, outside the Stage-B canaried path."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"file": "src/users/MemoryAudience.ts",
|
|
48
|
+
"reason": "Pure audience rules reached through the excluded SessionManager; no delivery logic, outside the Stage-B canaried path."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"file": "src/users/MemoryPrivacy.ts",
|
|
52
|
+
"reason": "Memory audience state reached through the excluded SessionManager; decides a session's memory folder, not message delivery, outside the Stage-B canaried path."
|
|
53
|
+
},
|
|
38
54
|
{
|
|
39
55
|
"file": "src/scheduler/jobScriptProtocol.ts",
|
|
40
56
|
"reason": "Shared deterministic script-job output parser reached through scheduler imports; it does not alter the certified Codex Stage-B delivery path and has separate script protocol unit and lifecycle coverage"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "e9eb5bb7f56ce6f81c5f80210c1ec7999d4b8c7d2ff1ab5cfd97a269172ce2ac",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1652",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "03432ef567abeb4ac189930419528bf621f74b464a4cd9987d60a0d74a929a72",
|
|
3
3
|
"registrySha256": "e9eb5bb7f56ce6f81c5f80210c1ec7999d4b8c7d2ff1ab5cfd97a269172ce2ac",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1652"
|
|
5
5
|
}
|
|
@@ -2173,6 +2173,21 @@
|
|
|
2173
2173
|
"note": "Never pruned by design: dropping a row could undo a person's no. Volume is tiny: at most a few rows per service per person."
|
|
2174
2174
|
}
|
|
2175
2175
|
},
|
|
2176
|
+
{
|
|
2177
|
+
"category": "offer-prerequisites",
|
|
2178
|
+
"description": "Whether this machine holds a new agent or a finished per-owner migration, and the first reply a platform accepted here. Read before an offer is reserved, and only able to hold one back. Per machine today; the recommended-services rollout requires a decision before offers read them.",
|
|
2179
|
+
"scope": "machine-local",
|
|
2180
|
+
"freshness": "eventual",
|
|
2181
|
+
"conflictShape": "single-writer",
|
|
2182
|
+
"transport": "none",
|
|
2183
|
+
"paths": ["state/offer-prerequisites/*.json"],
|
|
2184
|
+
"grandfathered": false,
|
|
2185
|
+
"retention": {
|
|
2186
|
+
"class": "fixed-cardinality",
|
|
2187
|
+
"maxRecords": 2,
|
|
2188
|
+
"note": "Two replace-in-place files."
|
|
2189
|
+
}
|
|
2190
|
+
},
|
|
2176
2191
|
{
|
|
2177
2192
|
"category": "ecosystem-ideas",
|
|
2178
2193
|
"description": "The ecosystem idea pipeline (docs/specs/ecosystem-idea-pipeline.md): an append-only event ledger that replicates as the ecosystemIdeas store, a per-machine framework admission record and notice dedupe. Source mirrors and index.db in the same directory are a rebuildable cache.",
|
|
@@ -24,6 +24,7 @@ import { APPRENTICESHIP_STALL_GATE_CLAUDEMD_SECTION, SESSION_LISTING_HYGIENE_CLA
|
|
|
24
24
|
import { SURFACE_REFERRAL_CLAUDEMD_SECTION } from '../core/PostUpdateMigrator.js';
|
|
25
25
|
import { RECOMMENDED_SERVICES_CLAUDEMD_SECTION } from '../core/PostUpdateMigrator.js';
|
|
26
26
|
import { MINIMUM_OPERATOR_EFFORT_CLAUDEMD_SECTION } from '../core/PostUpdateMigrator.js';
|
|
27
|
+
import { POOL_READINESS_CLAUDE_MD_LINE } from '../core/PostUpdateMigrator.js';
|
|
27
28
|
import { CODEX_DEVICE_LIMIT_CLAUDEMD_BULLET } from '../core/PostUpdateMigrator.js';
|
|
28
29
|
import { UPDATE_APPLY_VERIFY_HOLD_CLAUDEMD_LINE } from '../core/PostUpdateMigrator.js';
|
|
29
30
|
|
|
@@ -454,6 +455,9 @@ ${CLAUDE_KEY_HEALTH_BULLET}
|
|
|
454
455
|
- On a machine pool, add \`&scope=pool\` for one chronologically merged, registry-machine-tagged history across every machine. Refused shutoffs such as \`not-lease-holder\` remain visible by owning machine; a dark, old, unreachable, or malformed peer appears in \`pool.failed\` instead of looking like an empty log. \`pool.sources\` reports \`returned\` and \`truncated\` per machine, so a newest-N denominator cannot be mistaken for a complete or goal-scoped history. Reap-log rows — including normally producer-clamped \`workEvidence\` strings — cross only authenticated machines belonging to the same operator-owned agent pool; do not use this contract for cross-operator federation.
|
|
455
456
|
- Notices are PER-TOPIC and durable: every topic that lost a session gets one plain-English notice in THAT topic (bursts coalesce per topic; the lifeline gets unbound sessions + a cross-topic index — except a background job whose work completed normally, which stays in the reap-log only), delivered through a durable store + always-on drain that retries with backoff. Rollback levers: \`{"monitoring": {"reapNotify": {"perTopic": false}}}\` (legacy single-buffer), \`{"drainEnabled": false}\` (legacy direct send), \`{"enabled": false}\` (no notices). Recovery-bounces and your own operator kills stay silent.
|
|
456
457
|
- **Mid-work resume queue** (ships observe-only/dry-run by default): a session reaped MID-WORK (strong work evidence at kill time) is queued for ordered automatic revival once the machine recovers — at most one resume per minute, only after sustained calm + quota headroom. \`GET /sessions/resume-queue\` shows entries, paused/breaker state, and lastTickAt; \`POST /sessions/resume-queue/:id/cancel\` · \`/:id/requeue\` (gave-up entries only) · \`/resume\` (unpause) · \`/drain\` (manual single step). Emergency stops pause the queue; an explicit per-topic stop cancels that topic's entries. Jobs only auto-resume when their definition sets \`resumeOnReap: true\`.
|
|
458
|
+
- **Memory privacy**: when more than one person can reach this agent, what I remember belongs to an audience, and the place sets it. A private chat serves one person; a channel or group serves everyone who can read it, even when the person asking told me the fact privately. A session for a person or a shared place keeps its memory in its own folder and cannot reach the rest, so a refused read there is expected, not a fault. I never copy a person's facts into CLAUDE.md, AGENT.md or anything shared.
|
|
459
|
+
- Status: \`curl -H "Authorization: Bearer $AUTH" http://localhost:${port}/memory-privacy\` → \`{ enabled, multiPerson, ownerUserId, sandboxReady }\`. People who share memory freely go in \`memoryPrivacy.sharedMemoryPeople\` in .instar/config.json (profile ids); an agent reached only by them behaves as single-person.
|
|
460
|
+
- Proactive: user asks "why did you forget that here?" / "why can't you see what I told you in the channel?" / "can others see what I tell you?" → explain the audience rule, and check the status above.
|
|
457
461
|
- **A stale emergency-stop pause self-heals**: an emergency-stop pauses the WHOLE revival queue, and that pause used to never lift — silently stranding later, unrelated active-run revivals (the 2026-06-14 4-hour-silent-strand). Now: while the queue is paused with sessions waiting, you get ONE plain-English heads-up that revival is paused (Layer 1, always on); and if the pause is a stale emergency/sentinel stop AND an active autonomous run has since been recycled and queued well after the stop, the queue auto-resumes itself (Layer 2, on by default — \`monitoring.resumeQueue.autoResumeStalePause: false\` to disable; \`staleEmergencyPauseAutoResumeMin\` tunes the window, default 60). Any topic you actually stopped stays blocked by its per-topic operator-stop record even after the queue resumes, and a deliberate \`autonomous stop-all\` halt is NEVER auto-cleared. Proactive: user asks "why did my session restart by itself after a stop?" / "why is revival paused?" → GET /sessions/resume-queue (paused state) and the resume-queue audit log, then explain in plain words.
|
|
458
462
|
- Proactive: user asks "where did my session go?" / "why did X disappear?" / "did something get killed?" → GET /sessions/reap-log and explain the most recent reaped/skipped entries for that session; on a multi-machine agent use \`scope=pool\` so another machine's refusal or shutoff cannot disappear. User asks "did my interrupted work come back?" / "is a restart queued?" → GET /sessions/resume-queue and report the entry's status in plain words.
|
|
459
463
|
- **An autonomous run must outlive its session** (standard; dev-enabled, fleet-default-OFF self-heal): the revival queue takes a host-local lock so two machines can't share its state. A machine RENAME used to leave a stale lock the queue mistook for a shared-volume conflict → it silently disabled the whole revival guard (the 2026-06-15 incident). Now: on the dev agent, a stale FOREIGN-host lock that is provably a single-host rename (host-local disk + dead pid + ≥5min-stale heartbeat) is AUTO-HEALED instead of disabling (fail-closed on any uncertainty; \`monitoring.resumeQueue.autoHealStaleHostLock\`, fleet-default false). And a disabled revival queue now self-reports to the guard-posture inventory — it shows as \`off-runtime-divergent\` on \`GET /guards\` and raises one aggregated attention item, never silently inert. Proactive: user asks "why didn't my autonomous run come back after a restart/rename?" → GET /guards (is the resume queue off-runtime-divergent?) and GET /sessions/resume-queue (disabled reason), then explain.
|
|
@@ -629,7 +633,8 @@ Two layers keep my machine-to-machine \"ropes\" (Tailscale / LAN / Cloudflare) h
|
|
|
629
633
|
- **Idle vs broken machine (WS4.2):** the same \`pool.machines[]\` carries an explicit per-machine state so an idle machine never reads as broken. A machine with ZERO sessions gets \`pool.machines[].emptyState\` = \`online — no active sessions\` (heartbeat-fresh, just idle) / \`offline since <t>\` (known offline) / \`unreachable (last seen <t>)\` (was online, now not answering — the \`failed\` case). Honest derivation from the registry online flag + last-seen + the live fan-out — never a fabricated "looks fine". The dashboard sessions view renders these per-machine; a machine WITH sessions gets no empty-state (its tiles already name it). Single-machine install = just the lone self row.
|
|
630
634
|
- **Honest machine state (trustworthy-machine-load, P1):** every \`pool.machines[]\` row now carries \`liveness\` = \`live\` / \`unreachable\` (shape \`struggling\` | \`refusing\` | \`silent\`) / \`stale\` / \`unknown\`, each with the evidence age and source, plus a ready-made \`livenessLine\` (\`<liveness>, last evidence <age> ago from <source>\`) and the freshest other-machine sighting ("Mac Mini reached it 3 min ago"). The last DIRECT contact is saved to disk, so a server restart renders \`stale, last evidence 20 min ago\` instead of "offline". **Never say a machine is "offline" without positive evidence** (a shutdown announcement or the sleep gate): quote the liveness, its age and its source — an overloaded machine looks exactly like a dead one to a plain timeout. Each row also carries a multi-signal \`loadReport\` (cores, CPU idle, available memory, native memory pressure, server event-loop lag, sessions in use) and \`loadVerdict\` = \`ok\` / \`elevated\` / \`saturated\` / \`impaired\` / \`unknown\` — a report older than 60 s reads \`unknown\`, and a peer on older code reads \`unknown\`, never \`ok\`. Reporting only: the \`online\` flag, placement and ownership keep their existing semantics in this phase.
|
|
631
635
|
- **Overload admission (trustworthy-machine-load, P2):** each machine protects its own resources. When its own \`loadVerdict\` is \`saturated\` or \`impaired\` it CLOSES to new automatic work (scheduled jobs, helpers, placement of new conversations): the work waits and is retried, nothing is dropped, and existing conversations keep working. It reopens after five consecutive good samples (about 75 s). One host-wide **recovery slot** (shared by every agent on the machine) stays free so a session the operator asks for can always start and assess the machine. Placement never puts new work on a machine that is refusing it; if every machine refuses, the message stays in the durable inbound queue (\`all-machines-refusing-work\`). Registry First: \`GET /work-admission\` → \`{ enabled, dryRun, enforced, acceptingWork, lastVerdict, recoverySlot, counters }\`; each machine's \`loadReport.acceptingWork\` / \`admissionEnforced\` in \`GET /pool\`; audit \`logs/work-admission.jsonl\`. Ships dev-gated + dry-run (\`monitoring.workAdmission\`: it logs what it WOULD refuse, refuses nothing, and peers keep placing work here until \`dryRun:false\`). When the user asks "why didn't a job run / why is this machine not taking work?" → read \`GET /work-admission\` before guessing.
|
|
632
|
-
|
|
636
|
+
${POOL_READINESS_CLAUDE_MD_LINE}
|
|
637
|
+
- **Overload shed (trustworthy-machine-load, P3):** a machine that has refused new work for 3 minutes AND is at 85% or more of its session limit moves a conversation off, one at a time, to a healthy machine (direct contact under 2 min, \`loadVerdict\` \`ok\`, not refusing work, at least 2 free session slots after what it has already reserved there). Only conversations with a running session that has sat quiet for 10 minutes move, so this frees session slots and memory, not CPU. The move goes through the same drain as a manual transfer: the session here closes and the conversation carries on there at its next message. Pinned conversations, Slack conversations and conversations with a live autonomous run stay. Brakes: at most 2 moves an hour, 2 tries per conversation per overload episode, a moved conversation stays put for 6 hours wherever it landed, and a machine seen refusing work in the last 30 min never receives moved work. Live moves need pool stage \`rebalance\`, a fresh rebalance proof on this machine, and \`dryRun: false\`; otherwise it only logs what it WOULD move. Each machine earns its proof with \`POST /session-pool/rebalance-proof\` \`{ topic, to }\` (202 with a run id; \`GET /session-pool/rebalance-proof/RUN_ID\` for the result), which really moves a topic listed in \`multiMachine.sessionPool.rebalanceProofTopics\` and passes only if the old session is gone and the target answers once. A proof lasts 30 days; without a fresh one the stage steps down to \`live-transfer\`. Stale-owner notices say "struggling" or "refusing" when that is the evidence. Registry First: \`GET /overload-shed\` → \`{ enabled, dryRun, overloadedSince, isSource, liveMove, reservations, movesLastHour, counters }\`; audit \`logs/overload-shed.jsonl\` (\`would-move\` and \`move-outcome\` rows carry the numbers behind each decision). Ships dev-gated + dry-run (\`monitoring.overloadShed\`). When the user asks "why did my conversation move machines?" or "why is this overloaded machine still holding work?" → read \`GET /overload-shed\` before guessing.
|
|
633
638
|
- **Post-transfer closeout (automatic):** when a topic moves to another machine, the OLD machine's session for it is closed automatically (closed by the transfer's drain step when the current machine can drain, otherwise within ~2 reaper ticks) — no duplicate sessions doing duplicate work. The close is recorded in the reap-log with reason "topic moved to <machine>"; protected sessions are never auto-closed.
|
|
634
639
|
- **Quota-aware placement (automatic):** capacity heartbeats carry each machine's LLM-account quota state, and placement avoids machines whose account is currently rate-limited/blocked (no more topics placed onto a silent machine). A hard pin still wins (flagged \`pinned-machine-quota-blocked\`); if EVERY machine is blocked, placement proceeds least-loaded with \`all-machines-quota-blocked\` flagged. \`GET /pool\` shows each machine's \`quotaState\`.
|
|
635
640
|
- **Signed-out machines are skipped (automatic):** the same heartbeat also says whether the machine has any signed-in account for the framework new conversations run on (from its subscription pool). A machine where every such account needs sign-in reports \`quotaState.reason: no-signed-in-account\`, and placement sends new conversations elsewhere instead of to a session that would sit at "Not logged in". A hard pin still wins (flagged \`pinned-machine-not-signed-in\`). Unknown (no pool, or no account for that framework) never blocks. When the user asks "why did my new topic go silent / land on a machine that can't log in?" → read that machine's \`quotaState\` in \`GET /pool\`.
|
|
@@ -2201,6 +2206,10 @@ My \`threadline_*\` tools normally come from one small helper process per sessio
|
|
|
2201
2206
|
|
|
2202
2207
|
A send to a peer with no \`threadId\` continues my existing conversation with that peer. To open a separate thread with the same peer, I pass \`newThread: true\` to \`threadline_send\` (or \`newThread\` / \`fork: true\` to \`POST /threadline/relay-send\`): the response's \`threadId\` is the new thread, and later sends on it pass that \`threadId\`. \`newThread\` together with a \`threadId\` is refused (400 \`fork-conflicts-with-threadId\`), and a new thread that did not go out as one is a 502 \`fork-not-honoured\`, never a success on the old thread. **When to use** (PROACTIVE): a second, unrelated piece of work with a peer I already talk to → \`newThread: true\`, so the two conversations do not mix.
|
|
2203
2208
|
|
|
2209
|
+
### Who can answer my Telegram buttons
|
|
2210
|
+
|
|
2211
|
+
Approval buttons and typed prompt answers only work for my configured owner (\`ownerUserId\` in the Telegram block of \`.instar/config.json\`, or \`promptGate.ownerId\`), or, if none is set, the one person verified on this machine. With no single owner, they refuse the tap. Saying "I'm the owner" in a message proves nothing, because it only shows who sent it. If the person I work for hits that refusal, their next authenticated message binds them when nobody else has been verified on this machine. If several people have been verified, I set \`ownerUserId\` myself only when the sender's id matches my existing record of that person (my user registry or \`.instar/USER.md\`), and otherwise I report that I can't confirm the owner. \`config.json\` is per machine, so I set it on each machine and restart. I never ask them to edit anything.
|
|
2212
|
+
|
|
2204
2213
|
### Telegram internal routes need the lifeline secret
|
|
2205
2214
|
|
|
2206
2215
|
The two routes that carry Telegram input from my lifeline into my server (\`POST /internal/telegram-forward\` for messages, \`POST /internal/telegram-callback\` for button taps) take the sender's Telegram id from the request body. My ordinary API token used to be enough to call them, so any session could have sent a message or an Approve tap that claimed to come from the operator. They now also require a secret that only the lifeline and the server hold: 32 random bytes in \`.instar/state/lifeline-ingress-secret\` (owner-only), sent in the \`X-Instar-Lifeline-Secret\` header. It is never placed in any process environment, so sessions do not inherit it, and the file viewer, backups and git sync refuse the file and the guard's own records. I never call these routes myself and never read that file. No message is refused until the guard is armed: the lifeline has proven it sends the secret (it does so within seconds of starting, through \`POST /internal/lifeline-proof\`), or there is no lifeline to wait for. A lifeline still on older code is accepted and counted, never cut off, and is asked to restart; while the guard is unarmed the server handles it exactly as before (in \`enforce\`, for at most 24 hours). Once the guard is armed (or, in \`enforce\`, those 24 hours have passed): a button tap without the secret is always refused, and a message without it is delivered with no sender identity (it shows as from "Unverified sender", binds no operator and carries no command authority). Two modes decide what else happens to such a message: \`observe\` delivers it that way and counts a would-refuse; \`enforce\` refuses it with HTTP 401 \`{ error: 'lifeline-secret-required', refused: true }\`, and the lifeline keeps it in its saved queue, keeps retrying (every minute at first, then less often), and tells the user after ten minutes. Default: enforce on a development agent; on other agents observe, and observe ends by itself: after 24 hours with zero would-refuse and at least 10 verified Telegram requests from the lifeline, or 7 days after the guard armed, whichever comes first. The counts are saved to disk and survive a restart. In the config file (read live, not changeable over the API): \`telegramIngress.lifelineSecret.enforce: false\` holds observe, \`true\` enforces (still only once armed), and \`telegramIngress.lifelineSecret.bypass: true\` is the full rollback that restores the old handling. The same button tap delivered twice is applied once. Mode, whether it is refusing messages (\`refusing\`) and taps (\`refusingTaps\`), the exit-rule progress and the counts: authed \`/health\` → \`telegramIngress.lifelineSecret\`; it is also a row on \`/guards\`. Honest limit: no tool hook refuses a read of the secret file today. What protects it is the owner-only (0600) file mode plus the key-material path list, which keeps it out of the file viewer, backups and git sync. Everything here runs as one OS user, so this is a cooperative boundary: it stops a session forging through its normal token; it does not stop a session that deliberately reads the file. **When to use** (PROACTIVE): the user asks "could a session fake a message or an approval from me?" or "why was a Telegram message refused with lifeline-secret-required?" or "why does a message show as from Unverified sender?" → read that \`/health\` block; the counts are running totals, so compare times: a \`lastWouldRefuseAt\` or \`lastRefusedAt\` later than \`armedAt\` means something other than a healthy lifeline called the route (a session using the ordinary token, or a lifeline that cannot read its secret file).
|
|
@@ -64,7 +64,9 @@ fi
|
|
|
64
64
|
|
|
65
65
|
# Get port from env or config
|
|
66
66
|
PORT="${INSTAR_PORT:-}"
|
|
67
|
-
|
|
67
|
+
# The env token first and outside the config test below: a session in the memory
|
|
68
|
+
# sandbox cannot read config.json and must still authenticate.
|
|
69
|
+
AUTH="${INSTAR_AUTH_TOKEN:-}"
|
|
68
70
|
AGENT_ID="${INSTAR_AGENT_ID:-}"
|
|
69
71
|
|
|
70
72
|
if [ -f ".instar/config.json" ]; then
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Agents on several machines can now read whether their machines could serve conversations across each other. `GET /session-pool/readiness` reports line by line per peer, and an hourly history goes to `logs/pool-readiness.jsonl`. It only reports: it writes no config, no stage and no signed result. `instar pair` and `instar join` now say so when automatic takeover does not work because the pair has no git remote. The description of the `shadow` stage is corrected: it places conversations for real.
|
|
9
|
+
|
|
10
|
+
Load-driven moves between machines now work and have a real on-switch. Peers' session limits reach the capacity row, which is what Overload shed checks, so it can find a target at all. It picks only conversations with a running session that has sat quiet for 10 minutes, on a machine at 85% or more of its session limit, and moves them through the same drain as a manual transfer. Pool stage `rebalance` is now the gate: it needs a fresh proof from `POST /session-pool/rebalance-proof`, kept in its own signed store, and steps down if that proof is red or over 30 days old. Failover test runs no longer count toward stage 3.
|
|
11
|
+
|
|
12
|
+
Peer session numbers also feed placement scoring for new conversations, at every stage.
|
|
13
|
+
|
|
14
|
+
## What to Tell Your User
|
|
15
|
+
|
|
16
|
+
If you run me on several machines, I can now tell you plainly what would and wouldn't work if conversations moved between them, or if one machine died. Nothing about how I run has changed.
|
|
17
|
+
|
|
18
|
+
If you run me on several machines, a machine that runs short of session slots can hand a quiet conversation to another machine by itself. This stays off until each machine has proved it can do it, and it frees slots and memory, not CPU.
|
|
19
|
+
|
|
20
|
+
## Summary of New Capabilities
|
|
21
|
+
|
|
22
|
+
| Capability | How to Use |
|
|
23
|
+
|---|---|
|
|
24
|
+
| Pool readiness report | `GET /session-pool/readiness` |
|
|
25
|
+
| Readiness history | `logs/pool-readiness.jsonl` |
|
|
26
|
+
|
|
27
|
+
| Capability | How to Use |
|
|
28
|
+
|---|---|
|
|
29
|
+
| Rebalance proof | `POST /session-pool/rebalance-proof` with `{ topic, to }`, then `GET /session-pool/rebalance-proof/RUN_ID` |
|
|
30
|
+
| Proof topics | `multiMachine.sessionPool.rebalanceProofTopics` |
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: minor -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
On a fresh Linux server the sandbox used for private memory needs two packages. The agent now installs them at startup if it has passwordless sudo, instead of only reporting that they are missing.
|
|
9
|
+
|
|
10
|
+
When an agent serves more than one person, one person's remembered facts could reach another. Each conversation now has an audience set by its place: a private chat serves one person, and a channel or group serves everyone who can read it.
|
|
11
|
+
|
|
12
|
+
Each audience has its own memory folder. Sessions outside the owner's private chats run in an operating system sandbox and hold a limited key that the server refuses everywhere except a short list. If the sandbox cannot start, the agent sends a fixed reply and does not run unprotected.
|
|
13
|
+
|
|
14
|
+
A Slack reply now goes to the thread its message came from.
|
|
15
|
+
|
|
16
|
+
The Instar iPhone app can now show the agent's accounts, spending and resource use natively. Three new read-only routes answer a paired owner phone (or the agent's token): `GET /mobile/accounts` (each subscription account once, with its login email as the console shows it, signed in or needing sign-in on each machine, usage windows highest first with reset times, and how old the reading is; accounts needing sign-in first), `GET /mobile/spend?window=today|week|month` (recorded pay-per-use cost with how each figure was worked out, added across the machines that answer with each machine's own state, this machine's paid routes with committed spend against their limits and whether limits enforce, conversation token totals, and exactly what period each figure covers) and `GET /mobile/resources` (CPU and memory for the agent, its server and each session on this machine over the last hour, with old readings marked by age). They are trimmed summaries of what the console's Accounts, What thinking costs and Resource usage views show: no login folders, machine or account ids, credential references or raw errors. Only owner phones may read them (an MSP phone gets 403 `owner-phone-required`); they share their own 20-per-minute allowance, and pool-wide answers are built at most once every 15 seconds. The raw `/subscription-pool`, `/routing-spend/*`, `/tokens/*` and `/resources/*` routes stay closed to phones. A failed read records a `MobileCapacityScreens.read` degradation. The console's pool account view now names a peer's accounts by the machine that was asked, never by a name the peer supplied.
|
|
17
|
+
|
|
18
|
+
The Instar iPhone app can now show the agent's schedule, promises and machines natively. Three new read-only routes answer a paired owner phone (or the agent's token): `GET /mobile/jobs` (every job with its schedule in plain words, last run and outcome, next run, and whether it is late or failing, late or failing first, merged across the machines that answer), `GET /mobile/commitments` (open promises first with their due dates and what they are waiting on, then those closed in the last week) and `GET /mobile/machines` (each machine by name with a plain status line, load and Instar version). They are trimmed summaries of what the console's Schedule, Promises and Machines pages show: no job instructions, scripts, file paths, hostnames, machine ids, tokens or settings, and free text is secret-scrubbed and shortened. An MSP phone gets 403 `owner-phone-required`. The raw `/jobs`, `/commitments` and `/pool` routes stay closed to phones. A failed read records a `MobileOpsScreens.read` degradation instead of failing silently.
|
|
19
|
+
|
|
20
|
+
Telegram approval buttons no longer accept anyone when no owner is known. Prompt-gate buttons and typed answers need the configured owner or the one verified operator. Tunnel consent needs the configured owner. Pairing taps refuse a tap with no chat id. A non-owner's typed reply is no longer taken as a prompt's answer, though the text can still reach the session as an ordinary message. An agent that has met someone but can't confirm a single owner records this at startup.
|
|
21
|
+
|
|
22
|
+
Groundwork for recommended services, with no offers sent. The door and Tailscale setup steps moved out of the install command so the server can run them later. New agents are marked ready for offers when created, and existing agents wait until their own first list ships. Agents also note the first reply Telegram or Slack accepted. The recommended services route reports both values.
|
|
23
|
+
|
|
24
|
+
## What to Tell Your User
|
|
25
|
+
|
|
26
|
+
Nothing to do. On a new Linux server, private memory now sets itself up when it can.
|
|
27
|
+
|
|
28
|
+
If you are the only person using me, nothing changes in our private chats. In a channel with other people, I now keep your private memory out, so I may know less there. If several people use me, each person's memory stays theirs.
|
|
29
|
+
|
|
30
|
+
Once the app update with these screens is on your phone, your agent's page in the app has a "Capacity" section: Accounts, What thinking costs, and Resource usage. Accounts that need signing in are at the top, each account shows how much of its 5-hour and weekly limits is used and when they reset, the cost screen adds up what AI use cost across your machines, and the resource screen shows what is using the computer. If a machine is offline or didn't answer, the screen says so. These screens only look: signing in and spending limits stay in the dashboard. Only the owner's phone sees them.
|
|
31
|
+
|
|
32
|
+
Once the app update with the new screens is on your phone, your agent's page in the app has an "Is everything up?" section: Schedule, Promises, and Machines and health. Late or failing jobs, overdue promises and machines that aren't answering are at the top, and you pull down to refresh. These screens only look; nothing on them changes anything. Only the owner's phone sees them.
|
|
33
|
+
|
|
34
|
+
Approval buttons in Telegram now only work for you, even if your owner was never set up explicitly. If a button ever says I can't confirm who my owner is, tell me. Usually your message settles it. If I still can't confirm, I'll say so.
|
|
35
|
+
|
|
36
|
+
Nothing changes for you yet. This prepares your agent to recommend helpful services later.
|
|
37
|
+
|
|
38
|
+
## Summary of New Capabilities
|
|
39
|
+
|
|
40
|
+
| Capability | How to Use |
|
|
41
|
+
|-----------|-----------|
|
|
42
|
+
| Sandbox tools installed at boot | Automatic on Linux with passwordless sudo |
|
|
43
|
+
|
|
44
|
+
| Capability | How to Use |
|
|
45
|
+
|-----------|-----------|
|
|
46
|
+
| Memory privacy status | `GET /memory-privacy` |
|
|
47
|
+
| Name people who share memory freely | `memoryPrivacy.sharedMemoryPeople` in `.instar/config.json` |
|
|
48
|
+
| Switch it off | `memoryPrivacy.enabled: false` in `.instar/config.json` |
|
|
49
|
+
|
|
50
|
+
- The owner's phone can see every subscription account, whether it is signed in on each machine, and its usage and reset times.
|
|
51
|
+
- The owner's phone can see what AI use cost across machines, how each figure was worked out, and whether spending limits are enforcing.
|
|
52
|
+
- The owner's phone can see CPU and memory use for the agent, its server and each session.
|
|
53
|
+
|
|
54
|
+
- The owner's phone can read the agent's schedule, with late or failing jobs first.
|
|
55
|
+
- The owner's phone can read the agent's promises, open ones first with due dates.
|
|
56
|
+
- The owner's phone can see each machine's status and version, alongside the agent's health.
|
|
57
|
+
|
|
58
|
+
No new capabilities. This closes a gap where an unset owner meant anyone in the chat could answer.
|
|
59
|
+
|
|
60
|
+
No new capabilities. `GET /recommended-services` now reports `migrationComplete` and `firstResult`.
|
|
61
|
+
|
|
62
|
+
## Evidence
|
|
63
|
+
|
|
64
|
+
Found on a real test server that had neither tool.
|
|
65
|
+
|
|
66
|
+
Tested with real sessions on macOS and Ubuntu 24.04. On Ubuntu 24.04 the sandbox needs a one-time AppArmor profile; the agent installs it if `sudo -n` works.
|
|
67
|
+
|
|
68
|
+
Unit tests cover both sides of every rule in the summaries: each sign-in state (including a failed login check and a repair in progress), usage windows and their tone and staleness lines, peer readings dated in the future, in-use only for this machine's rows, offline and unreached machines scoping the headline, spend basis words and unpriced rows, merging across machines with per-machine states and the state precedence, the paid-route designated machine and committed figures booked elsewhere, every limits sentence, resource staleness and ended sessions, the 15-second shared read, and the owner-or-token check. Integration tests go through the real device check: an owner phone is served with no folder, id, credential reference or raw error text; the token is served; an MSP phone, a demoted phone, a removed person's phone, a revoked, unsigned or tampered request (including a changed query), the notification token and a POST are refused, and the 21st read in a minute gets 429. A production-path test boots the server, pairs a phone, reads all three routes, checks the spend state against the server's own spend view, and confirms the raw routes stay closed to the phone. A migration test confirms existing agents get the awareness line once.
|
|
69
|
+
|
|
70
|
+
Unit tests cover both sides of every rule in the summaries (the 10-minute late line, placement before failures, a running retry after failures, older peers, replicated-promise staleness, the single-machine rule, stable row keys, scrubbing) and the owner-or-token check, including a missing principal failing closed. Integration tests go through the real device check: an owner phone is served with no prompt, path, hostname or machine-id fields; the token is served; an MSP phone, a phone demoted to MSP, a removed person's phone, a revoked, unsigned or tampered request, the notification token and a POST are all refused. A production-path test boots the server, pairs a phone, reads all three routes (a promise recorded through the real tracker appears) and confirms the raw routes stay closed to it.
|
|
71
|
+
|
|
72
|
+
`tests/unit/callback-owner-fails-closed.test.ts`, `tests/unit/threadline/PairingInviteTelegram.test.ts`, `tests/integration/callback-owner-wiring.test.ts`. The wiring test fails when the route wiring is removed. `tests/integration/pairing-invite-telegram-routes.test.ts`, `tests/integration/telegram-internal-routes-lifeline-secret.test.ts` and `tests/e2e/telegram-internal-routes-lifeline-secret-lifecycle.test.ts` still pass.
|
|
73
|
+
|
|
74
|
+
`tests/unit/offer-prerequisites.test.ts`, `tests/unit/install-door-steps.test.ts`, `tests/unit/install-flow.test.ts`, `tests/integration/first-result-wiring.test.ts`, `tests/integration/recommended-services-routes.test.ts`, `tests/integration/install-agent-init.test.ts`, `tests/e2e/recommended-services-alive.test.ts`. The wiring test fails when the Telegram hook is removed.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Side-Effects Review — Memory privacy: install the sandbox tools at boot
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `memory-privacy-sandbox-setup`
|
|
4
|
+
**Date:** 2026-11-04
|
|
5
|
+
**Author:** echo
|
|
6
|
+
**Second-pass reviewer:** not required
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
A fresh Ubuntu server has no `bubblewrap` or `socat`, so on a multi-person agent every scoped conversation got the fixed reply, as seen on nimbus. When the boot probe fails on Linux, the agent now tries `sudo -n apt-get install bubblewrap socat` before the AppArmor profile, then probes again. Also counts named people during the boot window, and makes the owner notice plain. Files: `src/core/memorySandboxProbe.ts`, `src/users/wireMemoryPrivacy.ts`, `src/users/MemoryPrivacy.ts`. A no-sandbox mode with a reply-only shell was built and dropped: a real session ran a recursive `grep` despite it.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
None added. The existing probe still decides.
|
|
15
|
+
|
|
16
|
+
## 1. Over-block
|
|
17
|
+
|
|
18
|
+
No issue identified. It removes a block.
|
|
19
|
+
|
|
20
|
+
## 2. Under-block
|
|
21
|
+
|
|
22
|
+
A machine without passwordless sudo or without apt still gets the fixed reply and a degradation report, as before.
|
|
23
|
+
|
|
24
|
+
## 3. Level-of-abstraction fit
|
|
25
|
+
|
|
26
|
+
Same place as the existing profile step, at boot, only after the probe failed.
|
|
27
|
+
|
|
28
|
+
## 4. Signal vs authority compliance
|
|
29
|
+
|
|
30
|
+
No decision logic is added.
|
|
31
|
+
|
|
32
|
+
## 4b. Judgment-point check (Judgment Within Floors standard)
|
|
33
|
+
|
|
34
|
+
No judgment points.
|
|
35
|
+
|
|
36
|
+
## 5. Interactions
|
|
37
|
+
|
|
38
|
+
Runs only on Linux, only when the probe failed. A one-person agent with a working sandbox never reaches it. It can run on a one-person agent whose sandbox is missing, which installs two small packages; `sudo -n` means nothing happens without existing rights.
|
|
39
|
+
|
|
40
|
+
## 6. External surfaces
|
|
41
|
+
|
|
42
|
+
Installs two system packages on Linux hosts where the agent already has passwordless sudo.
|
|
43
|
+
|
|
44
|
+
## 6b. Operator-surface quality (Operator-Surface Quality standard)
|
|
45
|
+
|
|
46
|
+
No operator surface changes.
|
|
47
|
+
|
|
48
|
+
## 7. Multi-machine posture
|
|
49
|
+
|
|
50
|
+
Per machine; each machine probes for itself.
|
|
51
|
+
|
|
52
|
+
## 8. Rollback cost
|
|
53
|
+
|
|
54
|
+
Revert the commit. The packages are harmless if left installed.
|
|
55
|
+
|
|
56
|
+
## Conclusion
|
|
57
|
+
|
|
58
|
+
Closes the gap found on nimbus, where a fresh server could not start the sandbox.
|
|
59
|
+
|
|
60
|
+
## Second-pass review
|
|
61
|
+
|
|
62
|
+
**Reviewer:** not required. This adds no gate, sentinel or session lifecycle logic, and only widens an existing best-effort setup step.
|
|
63
|
+
|
|
64
|
+
## Evidence pointers
|
|
65
|
+
|
|
66
|
+
First verify run stopped on unhandled errors with no named test, on a loaded machine; rerun on this head. `tests/unit/memory-sandbox-probe.test.ts`. Live on 2026-11-04: on a bare Ubuntu 24.04 server with both packages and the profile removed, the probe failed, the install and profile steps ran through `sudo -n`, and the probe then passed.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Side-Effects Review — Memory privacy: a session loads only what the people it serves may see
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `memory-privacy`
|
|
4
|
+
**Date:** 2026-11-03
|
|
5
|
+
**Author:** echo
|
|
6
|
+
**Second-pass reviewer:** independent Claude reviewer, two rounds
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
On an agent more than one person can reach, memory was one pool, and an ordinary file search handed one person's fact to another. This adds an audience per session, decided by the place: a private chat serves one person, a channel or group serves its readers. Sessions outside the owner's private places run with their own memory folder (outside the agent home), an OS sandbox, an exact file-tool allow-list, no MCP, and a session token the server refuses on every route not listed. Files: `src/users/MemoryAudience.ts`, `MemoryPrivacy.ts`, `MemoryInboundTracker.ts`, `wireMemoryPrivacy.ts`, `src/core/memorySessionPolicy.ts`, `memorySandboxProbe.ts`, `src/server/memorySessionGuard.ts`, plus wiring in `SessionManager.ts`, `middleware.ts`, `server.ts`, `routes.ts`, the Slack and Telegram adapters, `JobScheduler.ts`, `TriageOrchestrator.ts`, `SpawnRequestManager.ts`, generated hooks and `slack-reply.sh`. Also closes fb-99bf432a-05f: a Slack reply goes to the thread its message came from.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- Audience resolution — add — deterministic, fails closed to `none`.
|
|
15
|
+
- Multi-person detection, with a first-boot baseline — add — deterministic.
|
|
16
|
+
- Reader count per shared place — add — deterministic; no fresh proof means shared.
|
|
17
|
+
- Session token route guard — add — default deny, in the auth middleware.
|
|
18
|
+
- Inbound refusal when no sandbox can run — add — fixed reply.
|
|
19
|
+
- Agent-to-agent spawn refusal on a multi-person agent — add.
|
|
20
|
+
|
|
21
|
+
## 1. Over-block
|
|
22
|
+
|
|
23
|
+
A busy channel on a one-person agent loses memory on purpose (operator chose this, 2026-11-03). A `[human, bot]` Slack allowlist with Slack not authenticated within two minutes of first boot flips a one-person agent, stickily; `sharedMemoryPeople` is the way back. A Telegram group gaining a non-admin bot loses its confirmation and reads as shared; the fix is `memoryPrivacy.privateGroups`. Unlinked ids added after baseline read as a new person until linked. Non-owners lose semantic and episodic recall. iMessage non-owners on a multi-person agent get the fixed reply. Agent-to-agent sessions are refused on multi-person agents.
|
|
24
|
+
|
|
25
|
+
## 2. Under-block
|
|
26
|
+
|
|
27
|
+
Two people who were unlinked on different platforms at first boot read as one operator until someone new appears. An unknown reader already in a one-person agent's Telegram group is not caught, nor a member replaced at an unchanged count. A joiner who reads between messages is caught at the next message. A job the owner wrote can post owner content to a shared place. A session's own environment holds the agent's Claude credential where it uses an env token, as today. Agents on one OS user can read each other's files (fb-79e7b408-ad2), which is separate.
|
|
28
|
+
|
|
29
|
+
## 3. Level-of-abstraction fit
|
|
30
|
+
|
|
31
|
+
The boundary is the session, not each store, because copies of facts sit in logs, backups, attachments and transcripts. `SessionManager` computes the audience in one place, so a caller that passes no place lands on `none`. Identity stays in `UserManager`, `deriveRole` and `SlackPrincipalResolver`.
|
|
32
|
+
|
|
33
|
+
## 4. Signal vs authority compliance
|
|
34
|
+
|
|
35
|
+
These are deterministic rules on identity and place, failing closed. They read no message content and hold no brittle judgment, so authority is appropriate under `docs/signal-vs-authority.md`. No LLM decides anything here.
|
|
36
|
+
|
|
37
|
+
## 4b. Judgment-point check (Judgment Within Floors standard)
|
|
38
|
+
|
|
39
|
+
No judgment points. Every decision is a deterministic boundary on identity and place; the spec classifies each as invariant with its reason.
|
|
40
|
+
|
|
41
|
+
## 5. Interactions
|
|
42
|
+
|
|
43
|
+
The auth middleware recognises `mps_` before the agent-token compare, so a session token cannot fall through. Exact-compare routes refuse it. Hooks run outside the sandbox with the session token; API-backed hook sections are refused and stay silent. Sessions are ended at inbound through `terminateSession` with `memoryAudienceClose`, which lifts the keep-guards except `protected`, because a busy session is the one that would answer. If one still survives, the message is not delivered. `linkAgentOwnedMemory` is skipped for scoped sessions. Resume is dropped when a place's audience changes.
|
|
44
|
+
|
|
45
|
+
Four files are added to the Stage-B excluded list with reasons (`src/data/stageBCertifiedSet.ts`). They are reachable only through `SessionManager`, which is already excluded, and hold no delivery logic. The certified set and its evidence are unchanged.
|
|
46
|
+
|
|
47
|
+
## 6. External surfaces
|
|
48
|
+
|
|
49
|
+
Sessions in busy channels and for non-owners change. `groups:read` joins the Slack manifest; existing apps without it read private channels as shared. New `GET /memory-privacy`, listed in `/capabilities`. On Linux, boot tries `sudo -n` once to add the bubblewrap AppArmor profile; without it scoped conversations get the fixed reply and one degradation report.
|
|
50
|
+
|
|
51
|
+
## 6b. Operator-surface quality (Operator-Surface Quality standard)
|
|
52
|
+
|
|
53
|
+
The operator sees a fixed plain reply and one degradation report if the sandbox cannot start, and can read `GET /memory-privacy`. No new dashboard page.
|
|
54
|
+
|
|
55
|
+
## 7. Multi-machine posture
|
|
56
|
+
|
|
57
|
+
Scope folders are machine-local by design, under `~/.instar/memory-scopes/<agent>/`, so a topic that moves machines starts empty, never wider. Audience is computed per spawn. `state/memory-privacy.json` is per machine. An older peer enforces nothing during a rollout; that is a stated gap.
|
|
58
|
+
|
|
59
|
+
## 8. Rollback cost
|
|
60
|
+
|
|
61
|
+
`memoryPrivacy.enabled: false` restores today's behaviour for new sessions. No data moved. Scope folders stay and are simply not loaded. A hot-fix release is enough; no data repair.
|
|
62
|
+
|
|
63
|
+
## Conclusion
|
|
64
|
+
|
|
65
|
+
Closes the proven leak at the session boundary for both the shell and Instar's own routes, at a stated cost in recall.
|
|
66
|
+
|
|
67
|
+
## Second-pass review
|
|
68
|
+
|
|
69
|
+
**Reviewer:** independent Claude reviewer subagent, four rounds of code review after the spec rounds.
|
|
70
|
+
|
|
71
|
+
Round 1, independent reviewer: Concern raised, eight findings, all accepted and fixed: a second person in a confirmed Telegram group kept owner memory; `GET /commitments`, assembled topic context and app reply files reached outside the scope; wiring ran after adapters started; iMessage and app never recorded a place, so one-person agents would have lost memory; WhatsApp and self-registered ids did not match the baseline; session retirement compared the wrong inputs; the Claude floor was declared and never checked; the thread fallback could misroute. Round 2: same reviewer, Concern raised, six findings, all accepted and fixed: retirement moved from boot to a per-message check, which covers pre-update sessions and lost reader proof; `/internal/stop-gate/` narrowed to the two routes hooks use and `compaction-resume` refused; baseline waits for the Slack bot id; boot probe failure no longer stops the server; floor check uses the launched binary. Round 3: Concern raised, four findings, all accepted and fixed: the close now lifts keep-guards and a surviving session blocks delivery; baseline no longer waits forever and held senders are not double-counted; same-name reuse checks the audience; jobs, headless, rerouted and triage are skipped. Round 4: the same reviewer confirmed those and found two more, both accepted and fixed: a forced baseline could merge two Slack people, and a topic-bound rerouted session could receive a scoped message. Final verdict: Concur with the review.
|
|
72
|
+
|
|
73
|
+
## Evidence pointers
|
|
74
|
+
|
|
75
|
+
Local verify: all unit tests and 3,763 E2E tests pass; E2E found the session-manager mock gap and a WhatsApp function length, both fixed and rerun (100 tests in those files pass). The boot version check is exempt from the Claude auth class test, because `--version` makes no model call. `tests/unit/memory-*.test.ts`, `tests/integration/memory-privacy-*.test.ts`, `tests/e2e/memory-privacy-lifecycle.test.ts`. Live on Ubuntu and macOS 2026-11-03 with real sessions: saved fact returned only to its owner in a fresh session; another person, a brand-new person and a channel got nothing, with the same figure sitting in the legacy folder.
|
|
76
|
+
|
|
77
|
+
## Class-Closure Declaration
|
|
78
|
+
|
|
79
|
+
`unbounded-self-action`, closure `n/a`: one-shot user-driven action, not a self-triggered loop. A session is ended only when a person's inbound message reaches a place whose live session has a different audience. The respawned session records the new audience, so the next message matches and nothing repeats until the audience itself changes.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Side-Effects Review — Native Accounts, Spend and Resource usage screens (read-only)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `mobile-native-capacity-screens`
|
|
4
|
+
**Date:** `2026-11-04`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `independent subagent — concur (see below)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Three read-only routes for the Instar iPhone app, owner phones (device-signed) or the bearer token only: `GET /mobile/accounts` (subscription accounts across the machines that answer: sign-in state per machine, usage windows with reset times), `GET /mobile/spend?window=today|week|month` (recorded pay-per-use cost added across machines with each machine's own state, this machine's paid-route limits, conversation token totals, and what each figure covers), `GET /mobile/resources` (CPU and memory for the agent, its server and sessions on this machine). New module `src/server/mobileCapacityRoutes.ts` (pure summarizers with explicit field allowlists, a 15 s single-flight cache, owner-or-bearer guard, degradation reporting); `routes.ts` extracts `subscriptionPoolScopeView()` and `routingSpendSummaryResult()` from their handlers (moves, plus one hardening: a peer's account rows are attributed to the peer that was asked and named from this machine's registry) and mounts the routes with a peer fan-out for spend; `DEVICE_ALLOWLIST` gains three `ownerOnly` GET entries with their own 20/min bucket; CapabilityIndex gets a `mobileCapacityScreens` entry (no prefix); the Mobile app routes CLAUDE.md block gets one bullet (template + idempotent migration). Tier 2; spec converged at the 6-round cap. App screens ship in the agent-home `apps/instar-ios` repo (separate commit).
|
|
11
|
+
|
|
12
|
+
Proportion: ~10 files, ~1,100 added lines including tests; 6 review rounds (the Tier-2 cap); the late rounds were multi-machine money semantics, not access.
|
|
13
|
+
|
|
14
|
+
## Decision-point inventory
|
|
15
|
+
|
|
16
|
+
- `DEVICE_ALLOWLIST` (mobilePairingRoutes.ts) — add — three exact-path GET rules, `ownerOnly`, own bucket `capacity-reads` (20/min).
|
|
17
|
+
- `ownerOrBearer` (mobileCapacityRoutes.ts) — add — route-level re-check: explicit bearer match or owner-role device; everything else 403.
|
|
18
|
+
- Field allowlists in the summarizers — add — fixed redaction contract.
|
|
19
|
+
- `/subscription-pool?scope=pool` peer attribution — modify — remote account and email-gap rows take the asked peer's id and the local registry nickname instead of a peer-supplied id/nickname.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. Over-block
|
|
24
|
+
|
|
25
|
+
An MSP (non-owner) phone is refused these three screens; that is the people-and-roles owner-only default, not an over-block. The 20-per-minute shared bucket could refuse a very fast manual refresher (the app only reads on open, pull or window change); the app words a 429 as "Too many refreshes in a minute." No other legitimate input is refused.
|
|
26
|
+
|
|
27
|
+
## 2. Under-block
|
|
28
|
+
|
|
29
|
+
An owner phone that is lost but not revoked can read account emails, usage and spend until revoked — the same exposure as every other owner-phone read (chat, sessions); revocation is the existing control. A bearer-token holder already reads all raw sources, so the summaries add nothing for it.
|
|
30
|
+
|
|
31
|
+
## 3. Level-of-abstraction fit
|
|
32
|
+
|
|
33
|
+
Right layer: authentication stays in the existing device authenticator (`ownerOnly`), the routes are thin read models over the same server functions the console's routes use (extracted, not copied), and display rules live in pure summarizers next to the console's view-model rules they mirror. No new gate.
|
|
34
|
+
|
|
35
|
+
## 4. Signal vs authority compliance
|
|
36
|
+
|
|
37
|
+
- [x] No — this change has no block/allow surface beyond deterministic authentication of the principal.
|
|
38
|
+
|
|
39
|
+
The only refusals are principal checks (path allowlist, resolved owner role, explicit bearer match): invariants in the existing device model, not judgment calls on content. Nothing here blocks agent behaviour or messages.
|
|
40
|
+
|
|
41
|
+
## 4b. Judgment-point check
|
|
42
|
+
|
|
43
|
+
No new static heuristic at a competing-signals decision point. Display classifications (sign-in state, usage tone, spend basis, staleness lines) copy the console's documented rules and gate nothing.
|
|
44
|
+
|
|
45
|
+
## 5. Interactions
|
|
46
|
+
|
|
47
|
+
- **Shadowing:** `ownerOnly` in the device layer refuses non-owner phones before the route; the route's own check is defence in depth and never contradicts it. The ops-screens PR (#2706, open) adds sibling routes and its own allowlist lines, CLAUDE.md bullet (after a different anchor line) and CapabilityIndex text (a different entry); the two can merge in either order, with a rebase keeping all allowlist entries.
|
|
48
|
+
- **Double-fire:** the spend fan-out and accounts fan-out reuse `poolPeerFetch` (rope recording). The 15 s single-flight cache keeps phone refreshes from multiplying peer reads.
|
|
49
|
+
- **Races:** none with state; reads only. The in-use resolver is one instance for the server's life (its own 60 s cache), never per read.
|
|
50
|
+
- **Feedback loops:** none. A failed read records one `MobileCapacityScreens.read` degradation; nothing reads that back into these routes.
|
|
51
|
+
- **Peer-attribution hardening:** the console's Accounts page now names a peer's rows by the asked peer and this registry's nickname. For any peer on current code this is identical (the plain route never sets `machineId`); only a peer that wrote another machine's id into its rows changes.
|
|
52
|
+
|
|
53
|
+
## 6. External surfaces
|
|
54
|
+
|
|
55
|
+
- New HTTP routes visible to paired owner phones and bearer callers; no change to existing route answers except the peer-attribution hardening above.
|
|
56
|
+
- Peer traffic: `/mobile/spend` asks each online peer's plain `/routing-spend/summary` and `/tokens/summary` (bearer, 5 s timeout), at most once per 15 s per window.
|
|
57
|
+
- No persistent state, no messages, no notices, no URLs generated.
|
|
58
|
+
- **Operator surface:** no operator actions are added (read-only). The phone screens say where actions live ("Signing in is done from the dashboard", "Limits are changed from the dashboard"), and the agent page keeps its Open dashboard link.
|
|
59
|
+
|
|
60
|
+
## 6b. Operator-surface quality
|
|
61
|
+
|
|
62
|
+
No dashboard/console file is touched — not applicable to the console. The app screens (separate repo) lead with a plain headline, show no ids, and were checked at phone width in the simulator (screenshots).
|
|
63
|
+
|
|
64
|
+
## 7. Multi-machine posture
|
|
65
|
+
|
|
66
|
+
- Accounts: **proxied-on-read** (`subscriptionPoolScopeView`, `?scope=pool` fan-out); registered offline machines listed in `offline`, peers that failed in `unread`, and any of either scopes the headline.
|
|
67
|
+
- Spend recorded cost + conversation tokens: **proxied-on-read** (each online peer's plain `/routing-spend/summary` and `/tokens/summary`), each machine listed with its state; offline machines listed, not counted.
|
|
68
|
+
- Spend limits: machine-local-justification: physical-credential-locality — the caps store, committed money ledger and money-layer status are kept where the metered credential is used; each paid route names its designated machine, and committed figures booked elsewhere are null, never a local $0.
|
|
69
|
+
- Resources: machine-local-justification: hardware-bound-resource — CPU and memory of this machine's processes; the answer names the machine.
|
|
70
|
+
- No user-facing notices, no durable state, no generated URLs.
|
|
71
|
+
|
|
72
|
+
## 8. Rollback cost
|
|
73
|
+
|
|
74
|
+
Pure code change: revert and ship a patch. No persistent state. The app's screens then get `403 device-not-allowed` and show "update the agent to see this here". The revert also restores the old peer attribution in the console's pool view.
|
|
75
|
+
|
|
76
|
+
## Conclusion
|
|
77
|
+
|
|
78
|
+
The review reshaped the design substantially (peer attribution, pool-wide spend with per-machine states, the designated-machine money semantics, honest coverage windows, a cache and bucket against fan-out). Remaining accepted risks are listed in the convergence report's Left behind (a UTC-midnight edge on peer spend windows, snapshot disjointness, retired machines in the registry). Clear to ship after Local Verify.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Second-pass review (if required)
|
|
83
|
+
|
|
84
|
+
**Reviewer:** independent general-purpose subagent (Opus 5.5)
|
|
85
|
+
**Independent read of the artifact: concur**
|
|
86
|
+
|
|
87
|
+
Concur with the review: the owner check fails closed at the device layer and again in the route, and the field allowlists send no paths, ids, credential references or lease-holder ids. Its one note (a peer's `emailGaps` rows still kept a peer-supplied machine id/nickname in the console's pool view) was fixed in the same change: gap rows are now attributed to the asked peer like its accounts.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Evidence pointers
|
|
92
|
+
|
|
93
|
+
- Unit: `tests/unit/mobile-native-capacity-screens.test.ts` (26), `tests/unit/mobile-native-capacity-screens-claudemd.test.ts` (4); neighbouring CapabilityIndex, discoverability, subscription-pool scope, routing-spend view, mobile-pairing, scaffold-templates suites green.
|
|
94
|
+
- Integration: `tests/integration/mobile-native-capacity-screens.test.ts` (14) through the production auth + device middleware.
|
|
95
|
+
- E2E: `tests/e2e/mobile-native-capacity-screens-alive.test.ts` (1) on a production AgentServer boot with a phone paired via redeem/confirm.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Follow-up after Local Verify (rebase onto main with #2707)
|
|
100
|
+
|
|
101
|
+
- `tests/unit/mobile-session-terminal.test.ts` pinned the owner-only rule count at 2; the three capacity rules made it 5. The test now names each owner-only rule by one request it admits (both directions), so a new owner-only screen adds a line instead of breaking a count.
|
|
102
|
+
- `tests/unit/no-silent-fallbacks.test.ts` counted the single-flight cache's `.catch` that evicts a failed read (495 > baseline 494). It is not a silent fallback (the rejection still reaches the caller through the returned promise); it is tagged `@silent-fallback-ok` with that reason. No behaviour change.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Class-Closure Declaration (display-only mirror)
|
|
107
|
+
|
|
108
|
+
No agent-authored-artifact defect — not applicable. No self-triggered controller added (the 15 s cache is request-driven, not a loop).
|