@intentic/sandbox-contract 1.348.0 → 1.350.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/contracts/agents.contract.d.ts +493 -0
  2. package/dist/contracts/agents.contract.d.ts.map +1 -1
  3. package/dist/contracts/agents.contract.js +19 -1
  4. package/dist/contracts/agents.contract.js.map +1 -1
  5. package/dist/contracts/device.contract.d.ts +7 -0
  6. package/dist/contracts/device.contract.d.ts.map +1 -1
  7. package/dist/contracts/system.contract.d.ts +13 -0
  8. package/dist/contracts/system.contract.d.ts.map +1 -1
  9. package/dist/events/system-events.d.ts +4 -0
  10. package/dist/events/system-events.d.ts.map +1 -1
  11. package/dist/ids/conversation-ids.d.ts +3 -0
  12. package/dist/ids/conversation-ids.d.ts.map +1 -1
  13. package/dist/ids/conversation-ids.js +4 -0
  14. package/dist/ids/conversation-ids.js.map +1 -1
  15. package/dist/index.d.ts +506 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/protocol/raw/raw-json-routes.d.ts +7 -0
  18. package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
  19. package/dist/schemas/agent-people.d.ts +23 -0
  20. package/dist/schemas/agent-people.d.ts.map +1 -0
  21. package/dist/schemas/agent-people.js +22 -0
  22. package/dist/schemas/agent-people.js.map +1 -0
  23. package/dist/schemas/agents-list.d.ts +456 -0
  24. package/dist/schemas/agents-list.d.ts.map +1 -1
  25. package/dist/schemas/agents-list.js +8 -0
  26. package/dist/schemas/agents-list.js.map +1 -1
  27. package/dist/schemas/agents.d.ts +7 -21
  28. package/dist/schemas/agents.d.ts.map +1 -1
  29. package/dist/schemas/agents.js +6 -20
  30. package/dist/schemas/agents.js.map +1 -1
  31. package/dist/schemas/conversation-links.d.ts +1 -0
  32. package/dist/schemas/conversation-links.d.ts.map +1 -1
  33. package/dist/schemas/conversation-links.js +1 -0
  34. package/dist/schemas/conversation-links.js.map +1 -1
  35. package/dist/schemas/device-folder.d.ts +8 -0
  36. package/dist/schemas/device-folder.d.ts.map +1 -1
  37. package/dist/schemas/device-folder.js +7 -0
  38. package/dist/schemas/device-folder.js.map +1 -1
  39. package/dist/schemas/devices.d.ts +37 -1
  40. package/dist/schemas/devices.d.ts.map +1 -1
  41. package/dist/schemas/devices.js +4 -2
  42. package/dist/schemas/devices.js.map +1 -1
  43. package/dist/state/workspace-state.d.ts +6 -0
  44. package/dist/state/workspace-state.d.ts.map +1 -1
  45. package/dist/state/workspace-state.js +2 -6
  46. package/dist/state/workspace-state.js.map +1 -1
  47. package/package.json +5 -5
  48. package/src/contracts/agents.contract.ts +24 -1
  49. package/src/ids/conversation-ids.test.ts +15 -0
  50. package/src/ids/conversation-ids.ts +11 -0
  51. package/src/schemas/agent-people.ts +36 -0
  52. package/src/schemas/agents-list.ts +16 -0
  53. package/src/schemas/agents.ts +10 -31
  54. package/src/schemas/conversation-links.ts +5 -0
  55. package/src/schemas/device-folder.ts +14 -0
  56. package/src/schemas/devices.ts +9 -9
  57. package/src/state/workspace-state.test.ts +2 -0
  58. package/src/state/workspace-state.ts +5 -6
@@ -4,7 +4,7 @@ import { AgentToolChildrenSchema, SessionTranscriptSchema } from "../events/tran
4
4
  import { AgentTranscriptSchema } from "../events/agent-transcript.js";
5
5
  import { AgentAssignSchema, AgentAutoLandSchema, AgentBreakPolicySchema, AgentFileDiffQuerySchema, AgentIdSchema, AgentToolChildrenQuerySchema, AgentTranscriptQuerySchema, AgentIdsSchema, AgentLandSchema, AgentReactSchema, AgentRenameSchema, AgentStopJobSchema, AgentStopWatchingSchema, AgentSubagentQuerySchema, AgentsArchivedSchema, AgentsMovedSchema, AgentsRemovedSchema, AgentSummarySchema, AgentUnsentSchema, LandResultSchema } from "../schemas/agents.js";
6
6
  import { AgentSearchQuerySchema, AgentSearchResultSchema } from "../schemas/agent-search.js";
7
- import { AgentsAttentionSchema, AgentsListSchema } from "../schemas/agents-list.js";
7
+ import { AgentsAttentionSchema, AgentsListSchema, HomeConversationSchema } from "../schemas/agents-list.js";
8
8
  import { AgentChangesSchema, AgentConflictsSchema, AgentIgnoredSchema, AgentScratchSchema, AgentHistorySchema } from "../schemas/git/git.js";
9
9
  import { FileDiffSchema } from "../schemas/history.js";
10
10
  import { OkSchema } from "../schemas/shared.js";
@@ -43,6 +43,29 @@ export const agentsContract = {
43
43
  })
44
44
  .meta({ floor: "owner", control: "never", panel: false })
45
45
  .output(AgentsAttentionSchema),
46
+ // Every member's own, guests' too: the count a rail chip of the master view wears for this sandbox, over the caller's
47
+ // own conversations rather than the fleet, so a member's chip never counts somebody else's waiting work.
48
+ attentionMine: procedure
49
+ .route({
50
+ method: "GET",
51
+ path: "/agents/attention/mine",
52
+ summary: "What needs you, and what is working, of your own conversations",
53
+ description:
54
+ "The same two counts as `attention`, over only the conversations you answer for (and their subagents, through their family's one card). What a sandbox's icon shows in the master view without reading the whole fleet.",
55
+ })
56
+ .meta({ guest: true })
57
+ .output(AgentsAttentionSchema),
58
+ // The id is minted here, from the verified caller, never from a body: a client deriving it could name somebody else's.
59
+ home: procedure
60
+ .route({
61
+ method: "GET",
62
+ path: "/agents/home",
63
+ summary: "Your home conversation in this sandbox",
64
+ description:
65
+ "The one long-running conversation the master view opens on this sandbox, yours alone among its members: its id, its card once it has started, and whether the work it accepts lands by itself. It orchestrates subagents, judges their work with review and test sessions, and merges what it accepts.",
66
+ })
67
+ .meta({ guest: true })
68
+ .output(HomeConversationSchema),
46
69
  archived: procedure
47
70
  .route({
48
71
  method: "GET",
@@ -1,5 +1,8 @@
1
1
  import {
2
2
  CI_FIX_PREFIX,
3
+ CONVERSATION_ID,
4
+ homeConversationId,
5
+ isHomeConversationId,
3
6
  ciFixConversationId,
4
7
  fixAttemptId,
5
8
  fixAttemptOf,
@@ -136,3 +139,15 @@ test("an attempt id passes the guard and fits, even for the widest run id a forg
136
139
  expect(id.length).toBeLessThanOrEqual(64);
137
140
  }
138
141
  });
142
+
143
+ // One home conversation per member, derived from who they are: the same in every browser and in the daemon, whatever the
144
+ // email's case, a valid conversation id, and never the email itself.
145
+ test("a home conversation id is its member's alone, stable and folded, and says nothing of the address", () => {
146
+ const ada = homeConversationId("ada@example.com");
147
+ expect(homeConversationId(" Ada@Example.COM ")).toBe(ada);
148
+ expect(homeConversationId("bob@example.com")).not.toBe(ada);
149
+ expect(CONVERSATION_ID.test(ada)).toBe(true);
150
+ expect(isHomeConversationId(ada)).toBe(true);
151
+ expect(isHomeConversationId(newConversationId())).toBe(false);
152
+ expect(ada).not.toContain("ada");
153
+ });
@@ -180,6 +180,17 @@ export const fnvDigest = (text: string): string => {
180
180
  return (hash >>> 0).toString(36).padStart(7, "0");
181
181
  };
182
182
 
183
+ // A PERSON'S HOME CONVERSATION in one sandbox: the one long-running chat the master view opens on it, one per member,
184
+ // so two people with the same sandbox connected never share it. Derived from the email rather than drawn, so every
185
+ // browser and the daemon name the same conversation without asking, and an email never sits in an id other members read
186
+ // on the board. Two independent FNV digests (64 bits between them): a sandbox's members never come near a collision.
187
+ // The daemon is the one that says which id is whose (`GET /agents/home`); a client computing it is only a shortcut.
188
+ export const HOME_PREFIX = "home-";
189
+ const homeKey = (email: string): string => email.trim().toLowerCase();
190
+ export const homeConversationId = (email: string): string => `${HOME_PREFIX}${fnvDigest(homeKey(email))}${fnvDigest(`home:${homeKey(email)}`)}`;
191
+ // Whether an id is shaped like somebody's home conversation; whose it is takes the email (`homeConversationId`).
192
+ export const isHomeConversationId = (id: string): boolean => id.startsWith(HOME_PREFIX);
193
+
183
194
  // A failing main-line check that no conversation still holding the work could take: keyed by the project and the moment
184
195
  // it began failing, so every attempt at one streak shares a base and a later streak in the same project starts over.
185
196
  // Seconds in base36 keep it short; the workspace root is spelled `workspace`, since its project folder is empty.
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+
3
+ // THE PEOPLE ON A CONVERSATION'S CARD: who answers for it, and who marked it. Their own module so agents.ts, the fleet's
4
+ // summary, stays within its size; agents.ts re-exports every name, so importers keep reading them from there.
5
+
6
+ // One person behind one mark. The instant rides along so the group can be ordered by who was first, which is also the
7
+ // order the names read in.
8
+ export const ReactorSchema = z.object({
9
+ email: z.string().describe("Who it was, as the sandbox verified them."),
10
+ name: z.string().optional().describe("What to call them, when their sign-in carried a name. Absent leaves the address to stand for them."),
11
+ at: z.number().describe("When they marked it, in milliseconds."),
12
+ });
13
+ export type Reactor = z.infer<typeof ReactorSchema>;
14
+ // The member answerable for a conversation: whose questions its cards are, who a reviewer asks about its work. Derived
15
+ // once from the starter, moved only by `agents.assign`; never a program, which is what `startedBy` is for.
16
+ export const SessionOwnerSchema = z.object({
17
+ email: z.string().describe("Who answers for this conversation, as the sandbox verified them."),
18
+ name: z
19
+ .string()
20
+ .optional()
21
+ .describe("What to call them, when the sign-in that made them its owner carried a name. Absent leaves the address to stand for them."),
22
+ since: z.number().describe("When they became its owner, in milliseconds."),
23
+ });
24
+ export type SessionOwner = z.infer<typeof SessionOwnerSchema>;
25
+ // Grouped by emoji on the wire, not as a flat list of presses: every surface draws one chip per emoji, and grouping
26
+ // here is what stops three of them each grouping it differently.
27
+ export const AgentReactionSchema = z.object({
28
+ emoji: z.string().describe("The mark itself, one emoji, carried as the character rather than as a name that would need a table on both sides."),
29
+ by: z
30
+ .array(ReactorSchema)
31
+ .min(1)
32
+ .describe(
33
+ "Everyone wearing this mark, oldest first, each by name. Carried whole rather than as a count, because a chip reading 3 that cannot say whose is a number nobody can answer. Never empty: the last person taking theirs back takes the whole chip with it.",
34
+ ),
35
+ });
36
+ export type AgentReaction = z.infer<typeof AgentReactionSchema>;
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { AgentSummarySchema } from "./agents.js";
3
+ import { ConversationIdSchema } from "./agent.js";
3
4
  import { AutomationApprovalSchema } from "./automations.js";
4
5
 
5
6
  // The fleet list: its own module, since it joins conversations (agents.ts) with held automation wakes (automations.ts),
@@ -35,3 +36,18 @@ export const AgentsAttentionSchema = z.object({
35
36
  working: z.number().int().nonnegative().describe("How many conversations are producing something right now."),
36
37
  });
37
38
  export type AgentsAttention = z.infer<typeof AgentsAttentionSchema>;
39
+
40
+ // The caller's home conversation in this sandbox (ids/conversation-ids.ts homeConversationId), as the master view opens it:
41
+ // the id is the daemon's answer to whose it is, so no client has to derive it, and the card once its first turn opened it.
42
+ export const HomeConversationSchema = z.object({
43
+ conversationId: ConversationIdSchema.describe(
44
+ "Your home conversation's id here. Send to it as to any conversation (`POST /agent`, isolated): its first turn opens it as your home.",
45
+ ),
46
+ agent: AgentSummarySchema.optional().describe("Its card, once a first turn has opened it. Absent until then."),
47
+ lands: z
48
+ .enum(["itself", "asks"])
49
+ .describe(
50
+ "What becomes of the work it accepts: `itself`, it lands and commits when its turn ends, because you may merge here; `asks`, it waits for a maintainer, because you may not.",
51
+ ),
52
+ });
53
+ export type HomeConversation = z.infer<typeof HomeConversationSchema>;
@@ -10,6 +10,8 @@ import { AgentNeedSchema } from "./needs.js";
10
10
  import { EMOJI_MAX_LENGTH, isSingleEmoji } from "../text/emoji.js";
11
11
  import { ProgramAskSchema } from "../events/requests.js";
12
12
  import { ReviewTallySchema, TurnProofSchema } from "./work-assurance.js";
13
+ import { AgentReactionSchema, SessionOwnerSchema } from "./agent-people.js";
14
+ export { AgentReactionSchema, ReactorSchema, SessionOwnerSchema, type AgentReaction, type Reactor, type SessionOwner } from "./agent-people.js";
13
15
  // A fleet agent is any conversation with a registry entry, keyed by conversationId. Isolated ones own a git worktree
14
16
  // (branch agent/<id>); workspace conversations have none, but both share one status/activity/cost lifecycle.
15
17
 
@@ -237,37 +239,6 @@ export type LandedMessageDraft = z.infer<typeof LandedMessageDraftSchema>;
237
239
  // offer the press that clears a refusal without knowing whose refusal it is.
238
240
  export const LandConflictReasonSchema = z.enum(["workspace", "diverged", "binary"]);
239
241
  export type LandConflictReason = z.infer<typeof LandConflictReasonSchema>;
240
- // One person behind one mark. The instant rides along so the group can be ordered by who was first, which is also the
241
- // order the names read in.
242
- export const ReactorSchema = z.object({
243
- email: z.string().describe("Who it was, as the sandbox verified them."),
244
- name: z.string().optional().describe("What to call them, when their sign-in carried a name. Absent leaves the address to stand for them."),
245
- at: z.number().describe("When they marked it, in milliseconds."),
246
- });
247
- export type Reactor = z.infer<typeof ReactorSchema>;
248
- // The member answerable for a conversation: whose questions its cards are, who a reviewer asks about its work. Derived
249
- // once from the starter, moved only by `agents.assign`; never a program, which is what `startedBy` is for.
250
- export const SessionOwnerSchema = z.object({
251
- email: z.string().describe("Who answers for this conversation, as the sandbox verified them."),
252
- name: z
253
- .string()
254
- .optional()
255
- .describe("What to call them, when the sign-in that made them its owner carried a name. Absent leaves the address to stand for them."),
256
- since: z.number().describe("When they became its owner, in milliseconds."),
257
- });
258
- export type SessionOwner = z.infer<typeof SessionOwnerSchema>;
259
- // Grouped by emoji on the wire, not as a flat list of presses: every surface draws one chip per emoji, and grouping
260
- // here is what stops three of them each grouping it differently.
261
- export const AgentReactionSchema = z.object({
262
- emoji: z.string().describe("The mark itself, one emoji, carried as the character rather than as a name that would need a table on both sides."),
263
- by: z
264
- .array(ReactorSchema)
265
- .min(1)
266
- .describe(
267
- "Everyone wearing this mark, oldest first, each by name. Carried whole rather than as a count, because a chip reading 3 that cannot say whose is a number nobody can answer. Never empty: the last person taking theirs back takes the whole chip with it.",
268
- ),
269
- });
270
- export type AgentReaction = z.infer<typeof AgentReactionSchema>;
271
242
  export const AgentSummarySchema = z.object({
272
243
  id: z.string().describe("The conversation id, which is how every other call addresses it."),
273
244
  sessionId: z.string().optional().describe("The provider session behind the last turn. It is retired whenever the model or account changes."),
@@ -412,6 +383,14 @@ export const AgentSummarySchema = z.object({
412
383
  fresh: FreshSessionSchema.optional().describe(
413
384
  "Set on a conversation started on another conversation's work without its messages (a code review, a test pass, a person's own ask): which one, for what, and when. Absent for every other conversation.",
414
385
  ),
386
+ // Latched on the opening turn (ids/conversation-ids.ts homeConversationId). Rides the summary because it decides who
387
+ // may see the row at all (auth/fleet-scope.ts visibleTo): its owner, and the sandbox's owner.
388
+ home: z
389
+ .literal(true)
390
+ .optional()
391
+ .describe(
392
+ "This is its owner's home conversation in this sandbox: the one long-running chat the master view opens, which orchestrates its own subagents, holds their work until it accepts it, and lands and commits by itself. Only its owner and the sandbox's owner see it. Absent for every other conversation.",
393
+ ),
415
394
  // Root repo's short base sha; per-repo bases stay daemon-internal.
416
395
  base: z.string().optional().describe("The commit its private copy started from, shortened."),
417
396
  costUsd: z.number().optional().describe("What it has cost so far, in dollars. A subagent's spend is its own and is not folded in here."),
@@ -36,6 +36,11 @@ export const FreshSessionSchema = z.object({
36
36
  of: ConversationIdSchema.describe("The conversation whose work it started from."),
37
37
  kind: FreshKindSchema.describe("What it was opened for: a code review, a test pass, or the person's own ask."),
38
38
  at: z.number().describe("When it started, in milliseconds. Work its origin changed after this is work it has not seen."),
39
+ // An orchestrator judging its own subagent's work (spawn with `freshOf`): the session is still the origin's child, cut
40
+ // from its work and landing back into it, but it reports to the agent that opened it, which supervises it.
41
+ by: ConversationIdSchema.optional().describe(
42
+ "The agent that opened it, when an agent did rather than a person: an orchestrator judging the work of a subagent it spawned. Its report goes to that agent. Absent for a person's session.",
43
+ ),
39
44
  });
40
45
  export type FreshSession = z.infer<typeof FreshSessionSchema>;
41
46
 
@@ -42,6 +42,20 @@ export const clearableOnDevice = (conflict: DeviceConflict): boolean =>
42
42
  export const DeviceSyncHoldSchema = z.enum(["owner", "gone", "container-missing", "container-trashed", "swap", "unreachable", "low-disk", "transfer"]);
43
43
  export type DeviceSyncHold = z.infer<typeof DeviceSyncHoldSchema>;
44
44
 
45
+ // HOW FAR A FOLDER'S COPY IS (2026-10-21), in Mutagen's own counts, so nothing walks the folder a second time to say
46
+ // it: what the sending side's last scan holds (`files`, `bytes`, ignored paths left out as the sync leaves them), and
47
+ // while the receiving side stages a pass, how many of that pass's files it has (`stagedFiles` of `stagingFiles`) and
48
+ // how many bytes have crossed (`stagedBytes`). A folder's first copy is one such pass over everything. Each is absent
49
+ // until Mutagen has said it; the whole is absent from an agent older than it, and from a pairing with no session.
50
+ export const DeviceCopySchema = z.object({
51
+ files: z.number().int().nonnegative().optional(),
52
+ bytes: z.number().nonnegative().optional(),
53
+ stagedFiles: z.number().int().nonnegative().optional(),
54
+ stagingFiles: z.number().int().nonnegative().optional(),
55
+ stagedBytes: z.number().nonnegative().optional(),
56
+ });
57
+ export type DeviceCopy = z.infer<typeof DeviceCopySchema>;
58
+
45
59
  // The drive reading that paused a pairing for want of room: when (epoch ms), and how much was free of how much.
46
60
  export const DeviceLowDiskSchema = z.object({
47
61
  since: z.number(),
@@ -1,7 +1,7 @@
1
1
  // What one of the user's own machines is running.
2
2
  import { z } from "zod";
3
3
  import { PortNumberSchema } from "./device-commands.js";
4
- import { DeviceBridgeHoldSchema, DeviceConflictSchema, DeviceLowDiskSchema } from "./device-folder.js";
4
+ import { DeviceBridgeHoldSchema, DeviceConflictSchema, DeviceCopySchema, DeviceLowDiskSchema } from "./device-folder.js";
5
5
  import { parseHostConnection, type DeviceFacts, DeviceFactsSchema, MachineIdentitySchema, MachineIdSchema, userDistrosOf, WslEnvironmentSchema } from "./hosts.js";
6
6
  import { RollbackTargetSchema, UpdateOutcomeSchema } from "./updates.js";
7
7
  import { DEV_VERSION } from "../state/versions.js";
@@ -249,15 +249,15 @@ export const DevicePairingSchema = z.object({
249
249
  remoteDir: z.string().optional(),
250
250
  // Whether landed work in this project folder is delivered to `localDir` by itself (`deliverProject`). A project
251
251
  // sandbox's folder says "auto"; absent means Bring back by hand, as before. (2026-10-21) The folderless
252
- // `projectsHost` pairing of the desktop app's one sandbox per computer went with that sandbox; an older agent's report
253
- // that still carries it parses, the key dropped.
252
+ // `projectsHost` pairing of the one sandbox per computer went with it; an older report carrying it parses, key dropped.
254
253
  deliver: z.enum(["auto", "off"]).optional(),
255
254
  // (2026-10-20) The slug this machine's ic knows the sandbox by: how the desktop app starts or forgets a folder's sandbox.
256
255
  slug: z.string().optional(),
257
256
  // Machine-owned switch (agent's own `sync mirror off`); absent reads as "on", not merely unknown.
258
257
  mirroring: z.enum(["on", "off"]).optional(),
259
- // Mutagen's own status word, kept verbatim rather than reduced to a traffic light.
258
+ // Mutagen's own status word, kept verbatim rather than reduced to a traffic light, and how far its copy is.
260
259
  mutagenStatus: z.string().optional(),
260
+ copy: DeviceCopySchema.optional(),
261
261
  // Whole conflict count, including any Mutagen's own state truncates (`excludedConflicts`).
262
262
  conflicts: z.number().int().nonnegative().optional(),
263
263
  // Conflicted paths, capped at `CONFLICT_PATHS_MAX`; `conflicts` above is the true, uncapped count.
@@ -304,8 +304,7 @@ export const DevicePortSchema = z.object({
304
304
  command: z.string().optional(),
305
305
  });
306
306
  export type DevicePort = z.infer<typeof DevicePortSchema>;
307
- // The device's one agent process as a single block; `running` also gates whether the rest of the report is still
308
- // current, a dead loop leaves every other row reading as it did before it died.
307
+ // The device's one agent process; `running` gates whether the rest of a report is current (a dead loop freezes it).
309
308
  export const DeviceAgentSchema = z.object({
310
309
  running: z.boolean(),
311
310
  pid: z.number().int().optional(),
@@ -313,12 +312,13 @@ export const DeviceAgentSchema = z.object({
313
312
  installed: z.string().optional(),
314
313
  // Build actually serving, stamped in the pidfile; can lag `installed` indefinitely (see `agentBuildSkew`).
315
314
  build: z.string().optional(),
316
- // When the loop last finished a pass; a live process can have a dead loop (see `agentStalled`).
315
+ // When the sync loop last moved; absent while nothing is paired (no loop runs). A live process can have a dead loop.
317
316
  lastTickAt: z.number().optional(),
318
317
  });
319
318
  export type DeviceAgent = z.infer<typeof DeviceAgentSchema>;
320
- // Loop polls every 5s with two 10s network timeouts per pairing; a minute is several passes of slack.
321
- export const AGENT_STALL_AFTER_MS = 60_000;
319
+ // Stalled only once the agent's own restart of a loop that stopped moving (machine sync/mirror.ts) is overdue.
320
+ export const WATCHER_STALL_MS = 20 * 60_000;
321
+ export const AGENT_STALL_AFTER_MS = WATCHER_STALL_MS + 2 * 60_000;
322
322
  export const agentStalled = (agent: DeviceAgent, now: number): boolean =>
323
323
  agent.running && agent.lastTickAt !== undefined && now - agent.lastTickAt > AGENT_STALL_AFTER_MS;
324
324
  export const DeviceReportSchema = z.object({
@@ -398,6 +398,8 @@ describe(`VERSIONED_STATE_PATHS`, () => {
398
398
  // Which commands count as heavy, and how many may run at once; a shared-box decision worth a `git log`
399
399
  // line.
400
400
  `${STATE_DIR}/config/heavy-commands.json`,
401
+ // What a home conversation reviews, merges and asks a person about; same reason as the safety policy.
402
+ `${STATE_DIR}/config/home.md`,
401
403
  `${STATE_DIR}/config/loop-designs.json`,
402
404
  `${STATE_DIR}/config/personas.json`,
403
405
  // A persona's prompt and reachable skills: the text that decides how it behaves, same reason as its card.
@@ -100,6 +100,10 @@ const STATE_FILES = [
100
100
  // may do unasked, so a fresh sandbox should start already governed and changes should show in review. Absent falls
101
101
  // back to `DEFAULT_SAFETY_POLICY`, not unconfigured.
102
102
  { path: ".intentic/config/safety.md", invalidates: ["safety-policy"], portability: "carry", versioned: true },
103
+ // What every home conversation works by (conversations/home/home-policy.ts): when it reviews, tests, merges and asks a
104
+ // person. `carry`+`versioned` like the safety policy, since it governs what an orchestrator lands unasked. Absent falls
105
+ // back to `DEFAULT_HOME_POLICY`.
106
+ { path: ".intentic/config/home.md", invalidates: [], portability: "carry", versioned: true, why: "Read per home turn; no view shows it." },
103
107
  // Verdicts the policy above produced, newest first. `derived`/`local`: evidence about this machine, self-trimming,
104
108
  // not carried. Own query key, separate from the policy's, since a verdict must not invalidate the document someone
105
109
  // is editing.
@@ -305,12 +309,7 @@ const STATE_FILES = [
305
309
  // Unlike the settings file, this switch is observed by the Extensions tab's query, so a flip elsewhere shows up
306
310
  // live. Toggling doesn't itself rerun the host, that happens via the loader's reconcile, triggered by the tab's own
307
311
  // toggle.
308
- {
309
- path: ".intentic/config/extension-enablement.json",
310
- invalidates: ["extensions"],
311
- portability: "carry",
312
- versioned: true,
313
- },
312
+ { path: ".intentic/config/extension-enablement.json", invalidates: ["extensions"], portability: "carry", versioned: true },
314
313
  // Workspace extensions: one directory per extension, written directly by the agent's file tools and live
315
314
  // immediately (no clone, no install step). `authored`, unlike `.intentic/extensions/` below which holds clones.
316
315
  // `versioned`: every other load path is already reviewable (a git-installed extension's sha, a baked one's image);