@paigy/harness 0.3.41 → 0.3.42

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 (3) hide show
  1. package/dist/cli.js +305 -159
  2. package/dist/main.js +299 -153
  3. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -11041,6 +11041,31 @@ var AgendaTurnSchema = external_exports.object({
11041
11041
  ifSilent: external_exports.boolean().optional()
11042
11042
  });
11043
11043
  var CLAIM_STALE_MS = 30 * 6e4;
11044
+ var CallQuestionSchema = external_exports.object({
11045
+ id: external_exports.string(),
11046
+ entryId: external_exports.string(),
11047
+ title: external_exports.string(),
11048
+ state: external_exports.enum(["open", "answered"]),
11049
+ answer: external_exports.string().nullable(),
11050
+ /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
11051
+ * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
11052
+ * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
11053
+ * author is not an agent this account holds (unpaired since, or a person). */
11054
+ agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional(),
11055
+ /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
11056
+ * its need offers, exactly as its own card carries them, present only when one of them has a
11057
+ * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
11058
+ * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
11059
+ options: external_exports.array(OptionSchema).optional(),
11060
+ /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
11061
+ * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
11062
+ * numbered chips above the call's controls, so a checklist is never only something read aloud. A
11063
+ * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
11064
+ * preview layout for any `options`, and must see exactly what it did. */
11065
+ choices: external_exports.array(OptionSchema).optional(),
11066
+ /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
11067
+ select: external_exports.enum(["one", "many", "rank"]).optional()
11068
+ });
11044
11069
  var InboxItemSchema = external_exports.object({
11045
11070
  id: external_exports.string(),
11046
11071
  tokenId: external_exports.string().optional(),
@@ -11082,31 +11107,10 @@ var InboxItemSchema = external_exports.object({
11082
11107
  * `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
11083
11108
  * the card's own concise heading; `answer` the accepted answer in words, null while open. It
11084
11109
  * REPLACED `agenda` (turns), which nothing ever filled. */
11085
- questions: external_exports.array(external_exports.object({
11086
- id: external_exports.string(),
11087
- entryId: external_exports.string(),
11088
- title: external_exports.string(),
11089
- state: external_exports.enum(["open", "answered"]),
11090
- answer: external_exports.string().nullable(),
11091
- /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
11092
- * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
11093
- * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
11094
- * author is not an agent this account holds (unpaired since, or a person). */
11095
- agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional(),
11096
- /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
11097
- * its need offers, exactly as its own card carries them, present only when one of them has a
11098
- * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
11099
- * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
11100
- options: external_exports.array(OptionSchema).optional(),
11101
- /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
11102
- * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
11103
- * numbered chips above the call's controls, so a checklist is never only something read aloud. A
11104
- * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
11105
- * preview layout for any `options`, and must see exactly what it did. */
11106
- choices: external_exports.array(OptionSchema).optional(),
11107
- /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
11108
- select: external_exports.enum(["one", "many", "rank"]).optional()
11109
- })).optional(),
11110
+ questions: external_exports.array(CallQuestionSchema).optional(),
11111
+ /** The same call carrying order, including request Entries with no DecisionNeed as reports.
11112
+ * Additive: questions stays decision-only for every other surface and older bundles. */
11113
+ callItems: external_exports.array(CallQuestionSchema.extend({ state: external_exports.enum(["open", "answered", "report"]) })).optional(),
11110
11114
  visuals: external_exports.array(VisualSchema).optional(),
11111
11115
  /** The connected agent's name (the single pairing name — user-typed, or the
11112
11116
  * agent's suggestion, or a default silly name). */
@@ -11305,12 +11309,29 @@ var UserSettingsSchema = external_exports.object({
11305
11309
  * clobber guard as voiceMode: absent = leave unchanged on write. */
11306
11310
  broker: BrokerTuningSchema.optional()
11307
11311
  });
11312
+ var GoalLatestSchema = external_exports.object({
11313
+ from: external_exports.enum(["person", "agent"]),
11314
+ said: external_exports.string(),
11315
+ at: external_exports.string(),
11316
+ entryId: external_exports.string(),
11317
+ /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
11318
+ * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
11319
+ * none was. The Work row reads it before `said`. */
11320
+ summary: external_exports.string().optional(),
11321
+ /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
11322
+ * of each unit"): each at most 50 characters, in order; absent when it has none. */
11323
+ titles: external_exports.array(external_exports.string()).optional(),
11324
+ /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
11325
+ * update's own line, before its units; the row caps it at 200. Absent on anything else. */
11326
+ line: external_exports.string().optional()
11327
+ });
11308
11328
  var HistoryWorkSchema = external_exports.object({
11309
11329
  id: external_exports.string(),
11310
11330
  title: external_exports.string(),
11311
11331
  state: external_exports.enum(["done", "cancelled"]),
11312
11332
  /** Who held it (`agent:<tokenId>` or `human:<userId>`). */
11313
- assignee: external_exports.string()
11333
+ assignee: external_exports.string(),
11334
+ latest: GoalLatestSchema.nullable().optional()
11314
11335
  });
11315
11336
  var HistoryEntrySchema = external_exports.union([
11316
11337
  external_exports.object({ at: external_exports.string(), card: InboxItemSchema }),
@@ -11378,7 +11399,7 @@ var ConnectionSummarySchema = external_exports.object({
11378
11399
  /** The @paigy/harness this host is running — a machine the self-update has not reached
11379
11400
  * shows its age here (`apps/desktop/src/update.ts`). */
11380
11401
  version: external_exports.string().optional(),
11381
- harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
11402
+ harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string(), models: external_exports.array(external_exports.string()).optional() })).optional(),
11382
11403
  workspaces: external_exports.array(external_exports.string()).optional(),
11383
11404
  /** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
11384
11405
  * repo, and each repo directly inside one, with its `origin` remote. A session started for
@@ -11538,22 +11559,7 @@ var QueueItemSchema = external_exports.object({
11538
11559
  * shows the moment it is written, and the agent's reply or next note replaces it by being newer.
11539
11560
  * `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
11540
11561
  * Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
11541
- latest: external_exports.object({
11542
- from: external_exports.enum(["person", "agent"]),
11543
- said: external_exports.string(),
11544
- at: external_exports.string(),
11545
- entryId: external_exports.string(),
11546
- /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
11547
- * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
11548
- * none was. The Work row reads it before `said`. */
11549
- summary: external_exports.string().optional(),
11550
- /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
11551
- * of each unit"): each at most 50 characters, in order; absent when it has none. */
11552
- titles: external_exports.array(external_exports.string()).optional(),
11553
- /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
11554
- * update's own line, before its units; the row caps it at 200. Absent on anything else. */
11555
- line: external_exports.string().optional()
11556
- }).nullable().optional(),
11562
+ latest: GoalLatestSchema.nullable().optional(),
11557
11563
  /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
11558
11564
  * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
11559
11565
  * answer to a question, a voice note filed as work. Null when the only hands on it have been its
@@ -12397,7 +12403,8 @@ var CATALOG = [
12397
12403
  authProbe: ["claude", "auth", "status"],
12398
12404
  loginHint: "Run `claude` once in a terminal and sign in.",
12399
12405
  installCli: "curl -fsSL https://claude.ai/install.sh | bash",
12400
- installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp"
12406
+ installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp",
12407
+ models: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"]
12401
12408
  },
12402
12409
  {
12403
12410
  name: "codex",
@@ -12407,7 +12414,8 @@ var CATALOG = [
12407
12414
  authProbe: ["codex", "login", "status"],
12408
12415
  loginHint: "Run `codex login` to authenticate.",
12409
12416
  installCli: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
12410
- installAdapter: "npm install -g @agentclientprotocol/codex-acp"
12417
+ installAdapter: "npm install -g @agentclientprotocol/codex-acp",
12418
+ models: ["gpt-4o", "gpt-4o-mini", "o1", "o1-mini"]
12411
12419
  },
12412
12420
  {
12413
12421
  name: "agy",
@@ -12417,7 +12425,8 @@ var CATALOG = [
12417
12425
  adapter: "paigy-agy-acp",
12418
12426
  authProbe: ["agy", "help"],
12419
12427
  loginHint: "Install and set up Antigravity (`agy`).",
12420
- installCli: "curl -fsSL https://antigravity.dev/install.sh | bash"
12428
+ installCli: "curl -fsSL https://antigravity.dev/install.sh | bash",
12429
+ models: ["gemini-1.5-pro", "gemini-1.5-flash", "gemini-2.0-pro-exp"]
12421
12430
  }
12422
12431
  ];
12423
12432
  function probeDirs(home = homedir3()) {
@@ -12453,7 +12462,7 @@ function runProbe(argv) {
12453
12462
  function detect(entry, deps = {}) {
12454
12463
  const find = deps.which ?? which;
12455
12464
  const probe = deps.probe ?? runProbe;
12456
- const base = { name: entry.name, label: entry.label };
12465
+ const base = { name: entry.name, label: entry.label, models: entry.models };
12457
12466
  const cliPath = find(entry.cli);
12458
12467
  if (!cliPath) return { ...base, status: "missing", hint: `${entry.label} is not installed.`, install: entry.installCli };
12459
12468
  const adapterPath = entry.adapter ? find(entry.adapter) : null;
@@ -14852,6 +14861,31 @@ var AgendaTurnSchema2 = external_exports.object({
14852
14861
  ifSilent: external_exports.boolean().optional()
14853
14862
  });
14854
14863
  var CLAIM_STALE_MS2 = 30 * 6e4;
14864
+ var CallQuestionSchema2 = external_exports.object({
14865
+ id: external_exports.string(),
14866
+ entryId: external_exports.string(),
14867
+ title: external_exports.string(),
14868
+ state: external_exports.enum(["open", "answered"]),
14869
+ answer: external_exports.string().nullable(),
14870
+ /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
14871
+ * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
14872
+ * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
14873
+ * author is not an agent this account holds (unpaired since, or a person). */
14874
+ agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional(),
14875
+ /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
14876
+ * its need offers, exactly as its own card carries them, present only when one of them has a
14877
+ * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
14878
+ * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
14879
+ options: external_exports.array(OptionSchema2).optional(),
14880
+ /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
14881
+ * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
14882
+ * numbered chips above the call's controls, so a checklist is never only something read aloud. A
14883
+ * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
14884
+ * preview layout for any `options`, and must see exactly what it did. */
14885
+ choices: external_exports.array(OptionSchema2).optional(),
14886
+ /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
14887
+ select: external_exports.enum(["one", "many", "rank"]).optional()
14888
+ });
14855
14889
  var InboxItemSchema2 = external_exports.object({
14856
14890
  id: external_exports.string(),
14857
14891
  tokenId: external_exports.string().optional(),
@@ -14893,31 +14927,10 @@ var InboxItemSchema2 = external_exports.object({
14893
14927
  * `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
14894
14928
  * the card's own concise heading; `answer` the accepted answer in words, null while open. It
14895
14929
  * REPLACED `agenda` (turns), which nothing ever filled. */
14896
- questions: external_exports.array(external_exports.object({
14897
- id: external_exports.string(),
14898
- entryId: external_exports.string(),
14899
- title: external_exports.string(),
14900
- state: external_exports.enum(["open", "answered"]),
14901
- answer: external_exports.string().nullable(),
14902
- /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
14903
- * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
14904
- * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
14905
- * author is not an agent this account holds (unpaired since, or a person). */
14906
- agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional(),
14907
- /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
14908
- * its need offers, exactly as its own card carries them, present only when one of them has a
14909
- * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
14910
- * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
14911
- options: external_exports.array(OptionSchema2).optional(),
14912
- /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
14913
- * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
14914
- * numbered chips above the call's controls, so a checklist is never only something read aloud. A
14915
- * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
14916
- * preview layout for any `options`, and must see exactly what it did. */
14917
- choices: external_exports.array(OptionSchema2).optional(),
14918
- /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
14919
- select: external_exports.enum(["one", "many", "rank"]).optional()
14920
- })).optional(),
14930
+ questions: external_exports.array(CallQuestionSchema2).optional(),
14931
+ /** The same call carrying order, including request Entries with no DecisionNeed as reports.
14932
+ * Additive: questions stays decision-only for every other surface and older bundles. */
14933
+ callItems: external_exports.array(CallQuestionSchema2.extend({ state: external_exports.enum(["open", "answered", "report"]) })).optional(),
14921
14934
  visuals: external_exports.array(VisualSchema2).optional(),
14922
14935
  /** The connected agent's name (the single pairing name — user-typed, or the
14923
14936
  * agent's suggestion, or a default silly name). */
@@ -15116,12 +15129,29 @@ var UserSettingsSchema2 = external_exports.object({
15116
15129
  * clobber guard as voiceMode: absent = leave unchanged on write. */
15117
15130
  broker: BrokerTuningSchema2.optional()
15118
15131
  });
15132
+ var GoalLatestSchema2 = external_exports.object({
15133
+ from: external_exports.enum(["person", "agent"]),
15134
+ said: external_exports.string(),
15135
+ at: external_exports.string(),
15136
+ entryId: external_exports.string(),
15137
+ /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
15138
+ * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
15139
+ * none was. The Work row reads it before `said`. */
15140
+ summary: external_exports.string().optional(),
15141
+ /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
15142
+ * of each unit"): each at most 50 characters, in order; absent when it has none. */
15143
+ titles: external_exports.array(external_exports.string()).optional(),
15144
+ /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
15145
+ * update's own line, before its units; the row caps it at 200. Absent on anything else. */
15146
+ line: external_exports.string().optional()
15147
+ });
15119
15148
  var HistoryWorkSchema2 = external_exports.object({
15120
15149
  id: external_exports.string(),
15121
15150
  title: external_exports.string(),
15122
15151
  state: external_exports.enum(["done", "cancelled"]),
15123
15152
  /** Who held it (`agent:<tokenId>` or `human:<userId>`). */
15124
- assignee: external_exports.string()
15153
+ assignee: external_exports.string(),
15154
+ latest: GoalLatestSchema2.nullable().optional()
15125
15155
  });
15126
15156
  var HistoryEntrySchema2 = external_exports.union([
15127
15157
  external_exports.object({ at: external_exports.string(), card: InboxItemSchema2 }),
@@ -15189,7 +15219,7 @@ var ConnectionSummarySchema2 = external_exports.object({
15189
15219
  /** The @paigy/harness this host is running — a machine the self-update has not reached
15190
15220
  * shows its age here (`apps/desktop/src/update.ts`). */
15191
15221
  version: external_exports.string().optional(),
15192
- harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
15222
+ harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string(), models: external_exports.array(external_exports.string()).optional() })).optional(),
15193
15223
  workspaces: external_exports.array(external_exports.string()).optional(),
15194
15224
  /** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
15195
15225
  * repo, and each repo directly inside one, with its `origin` remote. A session started for
@@ -15349,22 +15379,7 @@ var QueueItemSchema2 = external_exports.object({
15349
15379
  * shows the moment it is written, and the agent's reply or next note replaces it by being newer.
15350
15380
  * `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
15351
15381
  * Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
15352
- latest: external_exports.object({
15353
- from: external_exports.enum(["person", "agent"]),
15354
- said: external_exports.string(),
15355
- at: external_exports.string(),
15356
- entryId: external_exports.string(),
15357
- /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
15358
- * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
15359
- * none was. The Work row reads it before `said`. */
15360
- summary: external_exports.string().optional(),
15361
- /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
15362
- * of each unit"): each at most 50 characters, in order; absent when it has none. */
15363
- titles: external_exports.array(external_exports.string()).optional(),
15364
- /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
15365
- * update's own line, before its units; the row caps it at 200. Absent on anything else. */
15366
- line: external_exports.string().optional()
15367
- }).nullable().optional(),
15382
+ latest: GoalLatestSchema2.nullable().optional(),
15368
15383
  /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
15369
15384
  * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
15370
15385
  * answer to a question, a voice note filed as work. Null when the only hands on it have been its
@@ -15692,6 +15707,8 @@ var CallRecapSchema2 = external_exports.object({
15692
15707
 
15693
15708
  // src/host.ts
15694
15709
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
15710
+ import { execSync } from "child_process";
15711
+ import { randomBytes } from "crypto";
15695
15712
  import { homedir as homedir9 } from "os";
15696
15713
  import { join as join11 } from "path";
15697
15714
 
@@ -16417,7 +16434,8 @@ function startSession(opts) {
16417
16434
  if (!existsSync4(cwd)) {
16418
16435
  queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
16419
16436
  }
16420
- const codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
16437
+ let codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
16438
+ if (opts.model && opts.harness === "codex") codexConfig = { ...codexConfig, model: opts.model };
16421
16439
  if (codexConfig) {
16422
16440
  const key3 = "mcp_servers.paigy.env_vars";
16423
16441
  const configPath = join9(process.env.CODEX_HOME || join9(homedir8(), ".codex"), "config.toml");
@@ -16443,6 +16461,8 @@ function startSession(opts) {
16443
16461
  PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
16444
16462
  ...opts.token ? { PAIGY_TOKEN: opts.token } : {},
16445
16463
  ...opts.instance ? { PAIGY_INSTANCE_ID: opts.instance } : {},
16464
+ ...opts.model && opts.harness === "claude" ? { CLAUDE_MODEL_CONFIG: JSON.stringify({ provider: "anthropic", model: opts.model }) } : {},
16465
+ ...opts.model && opts.harness === "agy" ? { AGY_MODEL_CONFIG: JSON.stringify({ model: opts.model }) } : {},
16446
16466
  ...codexConfig ? { CODEX_CONFIG: JSON.stringify(codexConfig) } : {},
16447
16467
  ...opts.session ?? opts.resume ? { PAIGY_SESSION_ID: opts.session ?? opts.resume } : {},
16448
16468
  // A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
@@ -16457,19 +16477,56 @@ function startSession(opts) {
16457
16477
  stdio: ["pipe", "pipe", "pipe"]
16458
16478
  });
16459
16479
  let ended2 = false;
16460
- child.stdin?.on("error", () => {
16461
- });
16480
+ let failure;
16481
+ let hush;
16482
+ const pending = /* @__PURE__ */ new Set();
16483
+ const end = (error) => {
16484
+ ended2 = true;
16485
+ failure ??= error;
16486
+ clearTimeout(hush);
16487
+ for (const receipt of pending) receipt.reject(failure);
16488
+ pending.clear();
16489
+ };
16490
+ const broken = (error) => {
16491
+ if (ended2) return;
16492
+ end(new Error(`session_write_failed: ${error.message}`));
16493
+ opts.onEvent({ kind: "error", message: failure.message });
16494
+ child.kill();
16495
+ };
16496
+ child.stdin?.on("error", broken);
16462
16497
  const write = (frames) => {
16463
16498
  if (ended2) return;
16464
- for (const f of frames) child.stdin?.write(`${f}
16465
- `);
16466
- if (frames.length) listen();
16499
+ for (const f of frames) {
16500
+ if (ended2) break;
16501
+ if (!child.stdin || child.stdin.destroyed || !child.stdin.writable) {
16502
+ broken(new Error("stdin unavailable"));
16503
+ break;
16504
+ }
16505
+ const frame3 = JSON.parse(f);
16506
+ const receipts2 = frame3.method === "session/prompt" ? [...pending].filter((receipt) => !receipt.writing).slice(0, frame3.params.prompt.length) : [];
16507
+ for (const receipt of receipts2) receipt.writing = true;
16508
+ try {
16509
+ child.stdin.write(`${f}
16510
+ `, (error) => {
16511
+ if (error) {
16512
+ broken(error);
16513
+ return;
16514
+ }
16515
+ if (ended2) return;
16516
+ for (const receipt of receipts2) if (pending.delete(receipt)) receipt.resolve();
16517
+ });
16518
+ } catch (error) {
16519
+ broken(error instanceof Error ? error : new Error(String(error)));
16520
+ }
16521
+ }
16522
+ if (frames.length && !ended2) listen();
16467
16523
  };
16468
16524
  const apply = ({ events, writes }) => {
16525
+ const fatal = events.find((event) => event.kind === "error" && event.fatal);
16526
+ if (fatal?.kind === "error") end(new Error(fatal.message));
16469
16527
  write(writes);
16470
16528
  for (const event of events) opts.onEvent(event);
16471
16529
  };
16472
- let hush;
16473
16530
  const listen = () => {
16474
16531
  clearTimeout(hush);
16475
16532
  if (!ended2) hush = setTimeout(() => apply(driver.quiet(QUIET_MS)), QUIET_MS);
@@ -16479,12 +16536,12 @@ function startSession(opts) {
16479
16536
  if (text) opts.onEvent({ kind: "error", message: text });
16480
16537
  });
16481
16538
  child.on("exit", (code) => {
16482
- ended2 = true;
16483
- clearTimeout(hush);
16539
+ end(new Error(`session_ended: ${code ?? "signal"}`));
16484
16540
  opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
16485
16541
  opts.onExit?.();
16486
16542
  });
16487
16543
  child.on("error", (e) => {
16544
+ end(new Error(`session_start_failed: ${e.message}`));
16488
16545
  opts.onEvent({ kind: "error", message: e.message });
16489
16546
  opts.onExit?.();
16490
16547
  });
@@ -16504,15 +16561,24 @@ function startSession(opts) {
16504
16561
  write(driver.open());
16505
16562
  return {
16506
16563
  send(text) {
16507
- write(driver.send(text));
16564
+ const sent = new Promise((resolve3, reject) => {
16565
+ if (ended2) {
16566
+ reject(failure ?? new Error("session_ended"));
16567
+ return;
16568
+ }
16569
+ pending.add({ resolve: resolve3, reject, writing: false });
16570
+ write(driver.send(text));
16571
+ });
16572
+ void sent.catch(() => {
16573
+ });
16574
+ return sent;
16508
16575
  },
16509
16576
  respond(id3, decision) {
16510
16577
  write(driver.respond(id3, decision));
16511
16578
  },
16512
16579
  stop() {
16513
16580
  write(driver.close());
16514
- ended2 = true;
16515
- clearTimeout(hush);
16581
+ end(new Error("session_stopped"));
16516
16582
  child.kill();
16517
16583
  }
16518
16584
  };
@@ -16672,12 +16738,13 @@ var LIMITS = episodes();
16672
16738
  // src/paigy/bridge.ts
16673
16739
  function railFor(opts) {
16674
16740
  return {
16675
- next: async () => {
16676
- const waiting = await receive(opts);
16741
+ next: async (alreadyRead) => {
16742
+ const waiting = alreadyRead ?? await receive(opts);
16677
16743
  const goalId = waiting.claimable?.goalId ?? waiting.review?.[0]?.goalId;
16678
16744
  return goalId ? getGoals([goalId], opts).then(([goal]) => goal) : { state: "idle", message: "" };
16679
16745
  },
16680
16746
  read: (goalId) => getGoals([goalId], opts).then(([goal]) => goal),
16747
+ delivery: (deliveryId) => readDelivery(deliveryId, opts),
16681
16748
  complete: async (goalId, revision) => {
16682
16749
  const done = await manageGoals({ changes: [{ kind: "state", goalId, state: "completed", ...revision !== void 0 ? { revision } : {} }] }, opts);
16683
16750
  const result = done.results[0];
@@ -16858,7 +16925,7 @@ function beat() {
16858
16925
  }
16859
16926
  var BEAT_REPORT_MS = 10 * 6e4;
16860
16927
  var ended = (state) => state === "done" || state === "cancelled";
16861
- function unheard(brief, state, since = null, after = -1, reviewSince) {
16928
+ function unheard(brief, state, since = null, after = -1, reviewSince, gathered) {
16862
16929
  const seen = state.seen ??= /* @__PURE__ */ new Set();
16863
16930
  const words = [];
16864
16931
  const pendingAt = reviewSince ? Date.parse(reviewSince) : null;
@@ -16873,10 +16940,13 @@ function unheard(brief, state, since = null, after = -1, reviewSince) {
16873
16940
  continue;
16874
16941
  }
16875
16942
  if (since !== null && Date.parse(e.createdAt) <= Date.parse(since)) continue;
16876
- seen.add(e.entryId);
16877
16943
  if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
16944
+ seen.add(e.entryId);
16878
16945
  const text = entryWords(e).trim();
16879
- if (text) words.push(text);
16946
+ if (text) {
16947
+ words.push(text);
16948
+ gathered?.add(e.entryId);
16949
+ }
16880
16950
  }
16881
16951
  if (pendingAt !== null) for (const answer of brief.answers ?? []) {
16882
16952
  const key3 = `answer:${answer.decisionNeedId}`;
@@ -16917,12 +16987,28 @@ function named(goalId, title, brief) {
16917
16987
  const name = line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
16918
16988
  return name ? `On Goal "${name}" (${goalId}):` : `On Goal ${goalId}:`;
16919
16989
  }
16920
- async function gather(state, rail, held) {
16921
- const looks = /* @__PURE__ */ new Map([[state.goal.id, { via: "held", brief: held }]]);
16990
+ async function gather(state, rail, held, alreadyRead, log2) {
16991
+ const looks = /* @__PURE__ */ new Map();
16992
+ if (held && state.goal) looks.set(state.goal.id, { via: "held", brief: held });
16922
16993
  const look2 = (goalId, via, title, since) => {
16923
16994
  if (!looks.has(goalId)) looks.set(goalId, { via, ...title ? { title } : {}, ...since !== void 0 ? { since } : {} });
16924
16995
  };
16925
- const waiting = await rail.waiting().catch(() => null);
16996
+ const waiting = alreadyRead ?? await rail.waiting().catch(() => null);
16997
+ const direct = [];
16998
+ const staged = new Set(state.seen);
16999
+ const deliveries = /* @__PURE__ */ new Set();
17000
+ for (const d of waiting?.deliveries ?? []) {
17001
+ if (d.goalIds.length || deliveries.has(d.deliveryId) || state.acked?.get(`delivery:${d.deliveryId}`) === "handled") continue;
17002
+ deliveries.add(d.deliveryId);
17003
+ try {
17004
+ const full = await rail.delivery(d.deliveryId);
17005
+ const entries = full.entries.map((e) => ({ id: e.entryId, words: entryWords(e) }));
17006
+ if (!entries.length || entries.some((e) => !e.words.trim())) continue;
17007
+ direct.push({ eventId: d.deliveryId, entries, entryIds: entries.map((e) => e.id), words: [] });
17008
+ } catch (error) {
17009
+ log2?.(`\u26A0 could not read instruction ${d.deliveryId.slice(0, 8)}: ${error.message}`);
17010
+ }
17011
+ }
16926
17012
  const listed = /* @__PURE__ */ new Map();
16927
17013
  const list = (goalId, eventId) => {
16928
17014
  const ids = listed.get(goalId) ?? [];
@@ -16930,19 +17016,21 @@ async function gather(state, rail, held) {
16930
17016
  listed.set(goalId, ids);
16931
17017
  };
16932
17018
  const announced = state.announced ??= /* @__PURE__ */ new Set();
16933
- if (waiting) {
16934
- for (const id3 of announced) if (!(waiting.assigned ?? []).some((g) => g.goalId === id3)) announced.delete(id3);
16935
- }
16936
- for (const g of waiting?.assigned ?? []) if (g.goalId !== state.goal.id && !announced.has(g.goalId)) look2(g.goalId, "assigned", g.title);
16937
- for (const g of waiting?.review ?? []) {
16938
- list(g.goalId, g.eventId);
16939
- look2(g.goalId, "waiting work", g.title);
16940
- }
16941
- for (const d of waiting?.deliveries ?? []) for (const id3 of d.goalIds) {
16942
- list(id3, d.deliveryId);
16943
- look2(id3, "waiting work");
17019
+ if (held) {
17020
+ if (waiting) {
17021
+ for (const id3 of announced) if (!(waiting.assigned ?? []).some((g) => g.goalId === id3)) announced.delete(id3);
17022
+ }
17023
+ for (const g of waiting?.assigned ?? []) if (g.goalId !== state.goal.id && !announced.has(g.goalId)) look2(g.goalId, "assigned", g.title);
17024
+ for (const g of waiting?.review ?? []) {
17025
+ list(g.goalId, g.eventId);
17026
+ look2(g.goalId, "waiting work", g.title);
17027
+ }
17028
+ for (const d of waiting?.deliveries ?? []) for (const id3 of d.goalIds) {
17029
+ list(id3, d.deliveryId);
17030
+ look2(id3, "waiting work");
17031
+ }
16944
17032
  }
16945
- const moved = movedBelow(held, state);
17033
+ const moved = held ? movedBelow(held, state) : [];
16946
17034
  for (const { goal, since } of moved) look2(goal.goalId, "subtree", goal.title, since);
16947
17035
  const heard = [];
16948
17036
  const read3 = /* @__PURE__ */ new Set();
@@ -16959,10 +17047,15 @@ async function gather(state, rail, held) {
16959
17047
  read3.add(goalId);
16960
17048
  const fresh = !brief.reviewSince && (found.via === "waiting work" || found.via === "assigned") ? sinceTheWork(brief) : { after: -1 };
16961
17049
  if (found.via === "assigned") announced.add(goalId);
16962
- heard.push({ goalId, ...found, brief, words: unheard(brief, state, brief.reviewSince ? null : since ?? null, fresh.after, brief.reviewSince), ...fresh.note ? { note: fresh.note } : {} });
17050
+ heard.push({ goalId, ...found, brief, words: unheard(brief, state, brief.reviewSince ? null : since ?? null, fresh.after, brief.reviewSince, staged), ...fresh.note ? { note: fresh.note } : {} });
16963
17051
  }
17052
+ for (const d of direct) d.words = d.entries.filter((e) => {
17053
+ if (staged.has(e.id)) return false;
17054
+ staged.add(e.id);
17055
+ return true;
17056
+ }).map((e) => e.words);
16964
17057
  for (const { goal, since } of moved) if (read3.has(goal.goalId)) state.below.goals.set(goal.goalId, { since, at: goal.updatedAt ?? null });
16965
- return { heard, listed };
17058
+ return { heard, listed, direct };
16966
17059
  }
16967
17060
  var GIVEN = "This Goal was given to you, and nobody has started it. Carry on with what you're doing unless it changes that; your first write to it (a contact update naming it) starts it when you take it up.";
16968
17061
  var says = (h) => h.words.length > 0 || h.via === "assigned";
@@ -16980,12 +17073,21 @@ var block = (h) => [
16980
17073
  ].join("\n\n");
16981
17074
  var short = (ids) => ids.map((id3) => id3.slice(0, 8)).join(", ");
16982
17075
  var given = (heard) => heard.filter((h) => h.via === "assigned").map((h) => h.goalId);
16983
- async function hear(session, state, rail, held, opts) {
17076
+ async function hear(session, state, rail, held, opts, waiting) {
16984
17077
  const log2 = opts?.log;
16985
- const { heard, listed } = await gather(state, rail, held);
17078
+ const { heard, listed, direct } = await gather(state, rail, held, waiting, log2);
16986
17079
  const said = heard.filter(says);
16987
- if (said.length) {
16988
- send2(session, state, said.map(block).join("\n\n"));
17080
+ const words = [...direct.flatMap((d) => d.words), ...said.map(block)];
17081
+ if (words.length) {
17082
+ const text = words.join("\n\n");
17083
+ try {
17084
+ await send2(session, state, text);
17085
+ } catch (error) {
17086
+ (state.noted ??= []).push({ name: "", goals: said.map((h) => h.goalId), words: text, eventIds: direct.map((d) => d.eventId), at: Date.now(), cause: "read" });
17087
+ for (const d of direct) for (const id3 of d.entryIds) (state.seen ??= /* @__PURE__ */ new Set()).add(id3);
17088
+ throw error;
17089
+ }
17090
+ for (const d of direct) for (const id3 of d.entryIds) (state.seen ??= /* @__PURE__ */ new Set()).add(id3);
16989
17091
  const worded = said.filter((h) => h.words.length);
16990
17092
  if (worded.length) log2?.(`\u25B8 typed the person's words from ${worded.map((h) => `${h.goalId.slice(0, 8)} (via ${h.via})`).join(", ")}`);
16991
17093
  const goals = given(said);
@@ -16994,18 +17096,21 @@ async function hear(session, state, rail, held, opts) {
16994
17096
  if (opts?.tally) opts.tally.announced += goals.length;
16995
17097
  }
16996
17098
  }
17099
+ await acknowledgeDirect(direct.map((d) => d.eventId), state, rail, log2);
16997
17100
  for (const h of heard) {
16998
17101
  const events = listed.get(h.goalId);
16999
17102
  if (events) await acknowledge2(h.goalId, h.brief, events, state, rail, log2);
17000
17103
  }
17104
+ return direct.length > 0 || said.length > 0;
17001
17105
  }
17002
17106
  var NOTED = "New from the person while you work. Carry on unless it changes what you're doing: it comes back to you as your next turn when this one ends.";
17003
17107
  var SEEN_MID_TURN = "While you worked, the person said this, and you saw it mid-task. Act on it now, unless you already have.";
17004
- async function note(state, rail, held, inbox, how) {
17005
- const { heard } = await gather(state, rail, held);
17108
+ async function note(state, rail, held, inbox, how, waiting) {
17109
+ const { heard, direct } = await gather(state, rail, held, waiting, how.log);
17006
17110
  const said = heard.filter(says);
17007
- if (!said.length) return 0;
17008
- const words = said.map(block).join("\n\n");
17111
+ const parts = [...direct.flatMap((d) => d.words), ...said.map(block)];
17112
+ if (!parts.length) return 0;
17113
+ const words = parts.join("\n\n");
17009
17114
  const goals = said.map((h) => h.goalId);
17010
17115
  const assigned = given(said);
17011
17116
  if (how.tally) how.tally.announced += assigned.length;
@@ -17019,7 +17124,8 @@ ${words}`);
17019
17124
  } catch (error) {
17020
17125
  how.log?.(`\u26A0 could not note mid-turn (${error.message}): ${short(goals)} wait for the turn's end`);
17021
17126
  }
17022
- (state.noted ??= []).push({ name, goals, words, at: how.now(), cause: how.cause === "wake" ? "wake" : "read" });
17127
+ (state.noted ??= []).push({ name, goals, words, eventIds: direct.map((d) => d.eventId), at: how.now(), cause: how.cause === "wake" ? "wake" : "read" });
17128
+ for (const d of direct) for (const id3 of d.entryIds) (state.seen ??= /* @__PURE__ */ new Set()).add(id3);
17023
17129
  return 1;
17024
17130
  }
17025
17131
  function receipts(state, inbox, log2) {
@@ -17034,15 +17140,33 @@ function receipts(state, inbox, log2) {
17034
17140
  }
17035
17141
  return seen;
17036
17142
  }
17037
- function settle(session, state, inbox, log2) {
17143
+ async function settle(session, state, rail, inbox, log2) {
17038
17144
  const batches = state.noted ?? [];
17039
- state.noted = [];
17040
17145
  const seen = [];
17041
17146
  const unseen = [];
17042
17147
  for (const b of batches) (b.name && inbox?.settle(b.name) === "taken" ? seen : unseen).push(b);
17043
- send2(session, state, [...seen.length ? [SEEN_MID_TURN, ...seen.map((b) => b.words)] : [], ...unseen.map((b) => b.words)].join("\n\n"));
17148
+ await send2(session, state, [...seen.length ? [SEEN_MID_TURN, ...seen.map((b) => b.words)] : [], ...unseen.map((b) => b.words)].join("\n\n"));
17149
+ state.noted = [];
17150
+ await acknowledgeDirect(batches.flatMap((b) => b.eventIds ?? []), state, rail, log2);
17044
17151
  log2?.(`\u25B8 typed the person's words from ${short([...new Set(batches.flatMap((b) => b.goals))])} (noted mid-turn: ${seen.length} seen by the hook, ${unseen.length} not)`);
17045
17152
  }
17153
+ async function acknowledgeDirect(events, state, rail, log2) {
17154
+ const acked = state.acked ??= /* @__PURE__ */ new Map();
17155
+ const ids = [...new Set(events)].filter((id3) => acked.get(`delivery:${id3}`) !== "handled");
17156
+ for (let at = 0; at < ids.length; at += 100) {
17157
+ const batch = ids.slice(at, at + 100);
17158
+ try {
17159
+ const results = await rail.ack(batch);
17160
+ for (const id3 of batch) {
17161
+ const result = results.find((r) => r.eventId === id3);
17162
+ if (result?.acknowledged) acked.set(`delivery:${id3}`, "handled");
17163
+ else log2?.(`\u26A0 could not acknowledge ${id3.slice(0, 8)}: ${result?.refused ?? "no result"} \u2014 its words are typed in; it stays listed until acknowledged`);
17164
+ }
17165
+ } catch (error) {
17166
+ log2?.(`\u26A0 could not acknowledge instructions: ${error.message} \u2014 their words are typed in; they stay listed until acknowledged`);
17167
+ }
17168
+ }
17169
+ }
17046
17170
  async function acknowledge2(goalId, brief, events, state, rail, log2) {
17047
17171
  const ids = [.../* @__PURE__ */ new Set([...brief.reviewEventId ? [brief.reviewEventId] : [], ...events])];
17048
17172
  if (!ids.length) return;
@@ -17068,17 +17192,23 @@ async function step(session, state, rail, opts) {
17068
17192
  state.carryOn = true;
17069
17193
  }
17070
17194
  await look(session, state, rail, opts);
17071
- if (state.carryOn && state.resting && !state.asks?.size) send2(session, state, "");
17195
+ if (state.carryOn && state.resting && !state.asks?.size) await send2(session, state, "");
17072
17196
  }
17073
17197
  async function look(session, state, rail, opts) {
17074
17198
  if (opts?.inbox) {
17075
17199
  const seen = receipts(state, opts.inbox, opts.log);
17076
17200
  if (opts.tally) opts.tally.seen += seen;
17077
17201
  }
17078
- if (state.resting && state.noted?.length && !state.asks?.size) return settle(session, state, opts?.inbox, opts?.log);
17202
+ if (state.resting && state.noted?.length && !state.asks?.size) return settle(session, state, rail, opts?.inbox, opts?.log);
17079
17203
  if (!state.goal) {
17204
+ if (state.asks?.size || !state.resting && !opts?.inbox) return;
17205
+ const waiting = await rail.waiting();
17206
+ if (waiting.deliveries.some((d) => !d.goalIds.length && state.acked?.get(`delivery:${d.deliveryId}`) !== "handled")) {
17207
+ const handled = state.resting ? await hear(session, state, rail, void 0, opts, waiting) : await note(state, rail, void 0, opts.inbox, { cause: opts?.cause ?? "safety", now: opts?.now ?? Date.now, log: opts?.log, tally: opts?.tally }, waiting);
17208
+ if (handled) return;
17209
+ }
17080
17210
  if (!state.resting) return;
17081
- let brief2 = await rail.next();
17211
+ let brief2 = await rail.next(waiting);
17082
17212
  if (!brief2.goalId || brief2.revision === void 0) {
17083
17213
  if (opts?.initialPrompt?.trim()) {
17084
17214
  const prompt = opts.initialPrompt.trim();
@@ -17087,7 +17217,7 @@ async function look(session, state, rail, opts) {
17087
17217
  brief2 = await rail.next();
17088
17218
  if (!brief2.goalId || brief2.revision === void 0) {
17089
17219
  state.goal = { id: admitted.goalId, revision: admitted.revision };
17090
- send2(session, state, [HARNESS_INSTRUCTION, named(admitted.goalId, void 0, { outcome: prompt }), prompt].join("\n\n"));
17220
+ await send2(session, state, [HARNESS_INSTRUCTION, named(admitted.goalId, void 0, { outcome: prompt }), prompt].join("\n\n"));
17091
17221
  return;
17092
17222
  }
17093
17223
  } else {
@@ -17105,7 +17235,7 @@ async function look(session, state, rail, opts) {
17105
17235
  const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
17106
17236
  const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goals with history: true reads them in full.)` : "";
17107
17237
  const fullText = [outcome2, earlier, ...said].filter(Boolean).join("\n\n");
17108
- send2(session, state, [HARNESS_INSTRUCTION, named(brief2.goalId, void 0, brief2), fullText].filter(Boolean).join("\n\n"));
17238
+ await send2(session, state, [HARNESS_INSTRUCTION, named(brief2.goalId, void 0, brief2), fullText].filter(Boolean).join("\n\n"));
17109
17239
  if (ended(brief2.state)) await acknowledge2(brief2.goalId, brief2, [], state, rail, opts?.log);
17110
17240
  return;
17111
17241
  }
@@ -17134,19 +17264,27 @@ async function look(session, state, rail, opts) {
17134
17264
  }
17135
17265
  await hear(session, state, rail, brief, { ...opts?.log ? { log: opts.log } : {}, ...opts?.tally ? { tally: opts.tally } : {} });
17136
17266
  }
17137
- function send2(session, state, text) {
17138
- state.resting = false;
17267
+ async function send2(session, state, text) {
17139
17268
  let words = text;
17140
17269
  if (state.carryOn) {
17141
- state.carryOn = false;
17142
- state.carriedOn = true;
17143
17270
  const owed = state.owed;
17144
- state.owed = void 0;
17145
17271
  words = [owed, text].filter(Boolean).join("\n\n");
17146
17272
  text = [CARRY_ON, ...owed ? [OWED, owed] : [], text].filter(Boolean).join("\n\n");
17147
17273
  }
17274
+ const resting = state.resting;
17275
+ state.resting = false;
17276
+ try {
17277
+ await session.send(text);
17278
+ } catch (error) {
17279
+ state.resting = resting;
17280
+ throw error;
17281
+ }
17148
17282
  state.typed = words || void 0;
17149
- session.send(text);
17283
+ if (state.carryOn) {
17284
+ state.carryOn = false;
17285
+ state.carriedOn = true;
17286
+ state.owed = void 0;
17287
+ }
17150
17288
  }
17151
17289
  async function pump(session, state, rail, opts) {
17152
17290
  const clock5 = opts.beat ?? beat();
@@ -17286,9 +17424,6 @@ ${event.message}`, state, rail, { log: opts.log });
17286
17424
  opts.log("\u25A0 stopped");
17287
17425
  }
17288
17426
  return {
17289
- send(text) {
17290
- session?.send(text);
17291
- },
17292
17427
  working: () => running && state.resting !== true,
17293
17428
  // Paused by a usage limit, the activity line says so: the quiet place the pause is shown.
17294
17429
  tail: () => state.paused ? [pausedLine(state.paused)] : [...tail],
@@ -17411,7 +17546,7 @@ var claimedRun = (sessionId2) => {
17411
17546
  const slot = sessionSlot(sessionId2);
17412
17547
  return { slot, key: runKey(slot) };
17413
17548
  };
17414
- var launchRun = (spec) => ({ session: spec.sessionId });
17549
+ var launchRun = (spec) => ({ session: spec.sessionId, model: spec.model });
17415
17550
  var BOUND_ELSEWHERE_MS = 10 * 6e4;
17416
17551
  var boundElsewhere = /* @__PURE__ */ new Map();
17417
17552
  var BOUND_SKIP = "is bound to the session it was paired in, which this host cannot name \u2014 run `paigy-harness handoff` as it, in its folder, to record it";
@@ -17519,7 +17654,7 @@ function startHost(opts) {
17519
17654
  });
17520
17655
  void heartbeat({
17521
17656
  version: VERSION,
17522
- harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
17657
+ harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status, models: a.models })),
17523
17658
  workspaces: listWorkspaces(opts.wsDeps),
17524
17659
  // The repos inside them, so a session started for work on one opens IN it (Goal 26982211).
17525
17660
  repos: listRepos(opts.wsDeps)
@@ -17532,6 +17667,17 @@ function startHost(opts) {
17532
17667
  const specs = await claimSessions(asHost).catch(() => []);
17533
17668
  for (const spec of specs) {
17534
17669
  const label = spec.name ?? spec.harness;
17670
+ let workspace = spec.workspace;
17671
+ if (spec.worktree) {
17672
+ const suffix = randomBytes(4).toString("hex");
17673
+ const newWt = `/tmp/paigy-${spec.harness}-${suffix}`;
17674
+ try {
17675
+ execSync(`git worktree add ${newWt} -b paigy-${suffix}`, { cwd: workspace, stdio: "ignore" });
17676
+ workspace = newWt;
17677
+ } catch (e) {
17678
+ opts.log(`[${label}] Failed to create worktree, falling back to ${workspace}`);
17679
+ }
17680
+ }
17535
17681
  const { slot, key: key3 } = claimedRun(spec.sessionId);
17536
17682
  const log2 = (line) => opts.log(`[${label}] ${line}`);
17537
17683
  try {
@@ -17549,7 +17695,7 @@ function startHost(opts) {
17549
17695
  }
17550
17696
  const run = runHarness({
17551
17697
  harness: spec.harness,
17552
- cwd: spec.workspace,
17698
+ cwd: workspace,
17553
17699
  mode: "bypass",
17554
17700
  prompt: "",
17555
17701
  token: spec.token,