@freibergergarcia/phone-a-friend 2.7.1 → 2.8.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "phone-a-friend",
3
3
  "description": "CLI relay that lets AI coding agents collaborate by sending prompts and repository context to backend agents.",
4
- "version": "2.7.1",
4
+ "version": "2.8.0",
5
5
  "author": {
6
6
  "name": "Bruno Freiberger"
7
7
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phone-a-friend",
3
- "version": "2.7.1",
3
+ "version": "2.8.0",
4
4
  "description": "CLI relay that lets AI coding agents collaborate by sending prompts and repository context to backend agents.",
5
5
  "author": {
6
6
  "name": "Bruno Freiberger"
package/README.md CHANGED
@@ -194,7 +194,7 @@ phone-a-friend --to codex --prompt "Review the auth module" --session auth-revie
194
194
  phone-a-friend --to codex --prompt "Now fix those issues" --session auth-review
195
195
  ```
196
196
 
197
- Sessions work reliably with Claude, Codex, and OpenCode. Ollama replays history (may hit token limits on long conversations). Gemini sessions are currently unsupported.
197
+ Sessions work reliably with Claude, Codex, Gemini, and OpenCode. Ollama replays history (may hit token limits on long conversations). (Gemini resumes natively via `--session-id`/`--resume`; resume depends on Gemini's session retention.)
198
198
 
199
199
  ### Job tracking
200
200
 
@@ -186,8 +186,8 @@ PAF_CONTEXT_EOF
186
186
 
187
187
  "$RELAY_BIN" --to codex --repo "$PWD" --prompt "$(cat "$PROMPT_FILE")" --context-file "$CONTEXT_FILE" $PAF_NO_DIFF [--fast] [--session <id>]
188
188
  # For gemini, omit --model by default (let auto-routing pick); see "Gemini model selection" below.
189
- # Do NOT pass --session to gemini it will error (see "Session continuity" below):
190
- "$RELAY_BIN" --to gemini --repo "$PWD" --prompt "$(cat "$PROMPT_FILE")" --context-file "$CONTEXT_FILE" $PAF_NO_DIFF [--fast]
189
+ # Gemini supports --session via native resume (see "Session continuity" below):
190
+ "$RELAY_BIN" --to gemini --repo "$PWD" --prompt "$(cat "$PROMPT_FILE")" --context-file "$CONTEXT_FILE" $PAF_NO_DIFF [--fast] [--session <id>]
191
191
  ```
192
192
 
193
193
  Use delimiter names that do not appear in the payload. The quoted heredoc
@@ -264,14 +264,10 @@ Benefits: the backend keeps full conversation history, so follow-up prompts
264
264
  can be shorter (no need to re-send context from previous turns).
265
265
 
266
266
  **Backend-specific behavior:**
267
- - **Codex, Claude, OpenCode**: native session resume. Follow-up prompts
268
- can send deltas only.
267
+ - **Codex, Claude, Gemini, OpenCode**: native session resume. Follow-up
268
+ prompts can send deltas only.
269
269
  - **Ollama**: replays full history each call. Sessions work but prompt
270
270
  size grows with each turn. Keep follow-ups concise.
271
- - **Gemini**: `--session` is **not supported**. PaF rejects it with a
272
- RelayError (`--session is not supported by the gemini backend ...`).
273
- Each Gemini relay call must be self-contained. Do not pass `--session`
274
- with `--to gemini`.
275
271
 
276
272
  On the FIRST relay under a new session label, PaF prints an informational
277
273
  stderr line: `[phone-a-friend] Session label "..." not found in store.
@@ -320,9 +320,8 @@ command:
320
320
 
321
321
  **Generate session IDs for every backend that supports session resume.**
322
322
  PaF declares a resume strategy per backend (`native-session` for
323
- codex, claude, opencode; `transcript-replay` for ollama; `unsupported`
324
- for gemini). Generate a session ID for any backend whose strategy is
325
- NOT `unsupported`:
323
+ codex, claude, gemini, opencode; `transcript-replay` for ollama).
324
+ Generate a session ID for every backend:
326
325
 
327
326
  | Backend | resumeStrategy | Generate SESSION_ID? |
328
327
  |---|---|---|
@@ -330,11 +329,11 @@ command:
330
329
  | claude | native-session | yes |
331
330
  | opencode | native-session | yes |
332
331
  | ollama | transcript-replay | yes |
333
- | gemini | unsupported | NO, omit `--session` entirely |
332
+ | gemini | native-session | YES, generate a SESSION_ID |
334
333
 
335
- For `--backend both` (codex + gemini), generate a SESSION_ID for codex
336
- only. For `--backend all`, generate SESSION_IDs for codex, claude,
337
- opencode, ollama (every backend that runs), but never gemini.
334
+ For `--backend both` (codex + gemini), generate a SESSION_ID for both
335
+ codex and gemini. For `--backend all`, generate SESSION_IDs for codex,
336
+ claude, opencode, ollama, and gemini (every backend that runs).
338
337
 
339
338
  ### Algorithm
340
339
 
@@ -404,9 +403,7 @@ command:
404
403
  asked for a diff/branch/staged review.
405
404
 
406
405
  Include `--session <SESSION_ID>` for every session-capable backend
407
- (`codex`, `claude`, `opencode`, `ollama`). Omit `--session` only for
408
- `gemini` because PaF rejects it (Gemini's resume strategy is
409
- `unsupported`).
406
+ (`codex`, `claude`, `gemini`, `opencode`, `ollama`).
410
407
 
411
408
  On the FIRST relay under a new session label, PaF prints an
412
409
  informational stderr line: `[phone-a-friend] Session label "..." not
@@ -605,15 +602,12 @@ PAF_TEAM_CONTEXT_EOF
605
602
 
606
603
  Always include `--fast` (relay prompts are self-contained). For
607
604
  `--to claude`, `--fast` has no effect. Include `--session` for every
608
- session-capable backend: `codex`, `claude`, `opencode`, `ollama`. Pass
609
- the backend-specific ID from `SESSION_IDS`. For `gemini`, omit
610
- `--session` entirely; PaF rejects `--session` against Gemini (resume
611
- strategy declared `unsupported`).
605
+ session-capable backend: `codex`, `claude`, `gemini`, `opencode`,
606
+ `ollama`. Pass the backend-specific ID from `SESSION_IDS`.
612
607
 
613
608
  When `--session` is used, the session lets the backend remember
614
609
  previous rounds, so follow-up prompts can focus on feedback deltas
615
- rather than re-sending full context. For Gemini (no session), each
616
- round is stateless — see "Per-round relay rules" below.
610
+ rather than re-sending full context.
617
611
 
618
612
  **Direct mode** (`RELAY_MODE = direct`):
619
613
  ```bash
@@ -774,11 +768,6 @@ The backend already has them in its session history.
774
768
  call (prompt size grows per turn). Sessions work but follow-up prompts
775
769
  must stay concise to avoid hitting size limits.
776
770
 
777
- **Exception: Gemini (no `--session`)**: Gemini runs stateless every round.
778
- Each Gemini relay call must include enough context for the backend to
779
- answer independently — include a brief task recap and the latest output
780
- alongside the feedback in every round.
781
-
782
771
  **Per-round relay rules (direct mode, no session)**:
783
772
  - Each relay call sends ONLY:
784
773
  - The original TASK_DESCRIPTION
package/dist/index.js CHANGED
@@ -6979,6 +6979,9 @@ function looksLikeCodexAlready(output) {
6979
6979
  "already registered"
6980
6980
  ].some((token) => text.includes(token));
6981
6981
  }
6982
+ function codexMarketplaceSource(resolvedRepo) {
6983
+ return resolvedRepo.includes("@") ? GITHUB_REPO : resolvedRepo;
6984
+ }
6982
6985
  function syncCodexPluginRegistration(source) {
6983
6986
  const lines = [];
6984
6987
  try {
@@ -7248,7 +7251,7 @@ function installHosts(opts) {
7248
7251
  }
7249
7252
  }
7250
7253
  if (shouldInstallCodex && syncCodexCli) {
7251
- lines.push(...syncCodexPluginRegistration(resolvedRepo));
7254
+ lines.push(...syncCodexPluginRegistration(codexMarketplaceSource(resolvedRepo)));
7252
7255
  }
7253
7256
  return lines;
7254
7257
  }
@@ -77825,15 +77828,16 @@ var GeminiBackend = class {
77825
77828
  "workspace-write",
77826
77829
  "danger-full-access"
77827
77830
  ]);
77828
- // Session resume is declared 'unsupported' rather than 'transcript-replay':
77829
- // run() never reads opts.sessionHistory, and the --resume code path below
77830
- // depends on a session ID that the upstream extractor cannot reliably
77831
- // produce (see extractGeminiSessionId). Until the Gemini CLI's session
77832
- // surface is verified, --session against this backend is rejected at the
77833
- // relay layer instead of silently no-opping.
77831
+ // Session resume mirrors Claude's native-session model: PaF generates the
77832
+ // session UUID client-side (requiresClientSessionId), pins it on the first
77833
+ // call with `--session-id <uuid>`, and resumes later calls with
77834
+ // `--resume <uuid>`. Because the ID is client-generated and deterministic,
77835
+ // PaF never relies on extracting an ID from Gemini's output and never uses
77836
+ // `--resume latest`. History is not replayed (server-side session state),
77837
+ // so opts.sessionHistory is intentionally unused.
77834
77838
  capabilities = {
77835
- resumeStrategy: "unsupported",
77836
- requiresClientSessionId: false
77839
+ resumeStrategy: "native-session",
77840
+ requiresClientSessionId: true
77837
77841
  };
77838
77842
  async run(opts) {
77839
77843
  if (!isInPath("gemini")) {
@@ -77902,22 +77906,17 @@ var GeminiBackend = class {
77902
77906
  }
77903
77907
  }
77904
77908
  async runOnce(opts, model) {
77905
- const args = [];
77906
77909
  const useJsonOutput = Boolean(opts.schema);
77907
77910
  const prompt = opts.schema ? injectSchemaPrompt(opts.prompt, opts.schema) : opts.prompt;
77908
- if (opts.sandbox !== "danger-full-access") {
77909
- args.push("--sandbox");
77910
- }
77911
- args.push("--yolo");
77912
- args.push("--include-directories", opts.repoPath);
77913
- args.push("--output-format", useJsonOutput ? "json" : "text");
77914
- if (opts.resumeSession && opts.sessionId) {
77915
- args.push("--resume", opts.sessionId);
77916
- }
77917
- if (model) {
77918
- args.push("-m", model);
77919
- }
77920
- args.push("--prompt", prompt);
77911
+ const args = buildGeminiArgs({
77912
+ prompt,
77913
+ repoPath: opts.repoPath,
77914
+ sandbox: opts.sandbox,
77915
+ model,
77916
+ useJsonOutput,
77917
+ sessionId: opts.sessionId ?? null,
77918
+ resumeSession: Boolean(opts.resumeSession)
77919
+ });
77921
77920
  try {
77922
77921
  const result = await spawnCli("gemini", args, {
77923
77922
  timeoutMs: opts.timeoutSeconds * 1e3,
@@ -77945,10 +77944,42 @@ var GeminiBackend = class {
77945
77944
  }
77946
77945
  throw new GeminiBackendError("Gemini reached turn limit, response may be incomplete");
77947
77946
  }
77947
+ if (err instanceof SpawnCliError && opts.sessionId && isUnknownSessionFlagError(err.stderr)) {
77948
+ const flag = opts.resumeSession ? "--resume" : "--session-id";
77949
+ throw new GeminiBackendError(
77950
+ `The installed Gemini CLI does not support the \`${flag}\` flag required for --session resume. Upgrade it (\`${INSTALL_HINTS.gemini}\`), or drop --session to run a one-shot relay.`
77951
+ );
77952
+ }
77948
77953
  throw err;
77949
77954
  }
77950
77955
  }
77951
77956
  };
77957
+ function buildGeminiArgs(opts) {
77958
+ const args = [];
77959
+ if (opts.sandbox !== "danger-full-access") {
77960
+ args.push("--sandbox");
77961
+ }
77962
+ args.push("--yolo");
77963
+ args.push("--include-directories", opts.repoPath);
77964
+ args.push("--output-format", opts.useJsonOutput ? "json" : "text");
77965
+ if (opts.sessionId) {
77966
+ if (opts.resumeSession) {
77967
+ args.push("--resume", opts.sessionId);
77968
+ } else {
77969
+ args.push("--session-id", opts.sessionId);
77970
+ }
77971
+ }
77972
+ if (opts.model) {
77973
+ args.push("-m", opts.model);
77974
+ }
77975
+ args.push("--prompt", opts.prompt);
77976
+ return args;
77977
+ }
77978
+ function isUnknownSessionFlagError(stderr) {
77979
+ const text = stderr.toLowerCase();
77980
+ if (!/unknown argument|unknown option|unrecognized/.test(text)) return false;
77981
+ return text.includes("session-id") || text.includes("resume");
77982
+ }
77952
77983
  function formatDeadModelError(model, entry, cachePath) {
77953
77984
  return `Model \`${model}\` returned 404 from Gemini (ModelNotFoundError). Cached as unavailable until ${entry.expiresAt} at ${cachePath}. Run without \`--model\` to use Gemini's auto-routing, set \`PHONE_A_FRIEND_GEMINI_DEAD_CACHE=false\` to bypass the cache, or delete the cache file to clear it.`;
77954
77985
  }
@@ -78071,9 +78102,27 @@ async function* parseNDJSONStream(body, signal) {
78071
78102
  throw new Error("Stream ended unexpectedly");
78072
78103
  }
78073
78104
  }
78105
+ function extractOpenCodeErrorMessage(event) {
78106
+ const error3 = event.error;
78107
+ if (error3 && typeof error3 === "object") {
78108
+ const data = error3.data;
78109
+ if (data && typeof data.message === "string" && data.message.trim()) {
78110
+ return data.message;
78111
+ }
78112
+ if (typeof error3.name === "string" && error3.name.trim()) {
78113
+ return error3.name;
78114
+ }
78115
+ try {
78116
+ return JSON.stringify(error3);
78117
+ } catch {
78118
+ }
78119
+ }
78120
+ return "opencode reported an error";
78121
+ }
78074
78122
  async function* parseOpenCodeStreamJSON(stdout, opts) {
78075
78123
  let buffer = "";
78076
78124
  let sessionReported = false;
78125
+ let errorReported = false;
78077
78126
  function* processLines(lines) {
78078
78127
  for (const line of lines) {
78079
78128
  const trimmed = line.trim();
@@ -78088,6 +78137,10 @@ async function* parseOpenCodeStreamJSON(stdout, opts) {
78088
78137
  opts.onSessionCreated(parsed.sessionID);
78089
78138
  sessionReported = true;
78090
78139
  }
78140
+ if (!errorReported && parsed.type === "error" && opts?.onError) {
78141
+ opts.onError(extractOpenCodeErrorMessage(parsed));
78142
+ errorReported = true;
78143
+ }
78091
78144
  if (parsed.type === "text") {
78092
78145
  const part = parsed.part;
78093
78146
  if (part?.text && typeof part.text === "string") {
@@ -78381,6 +78434,18 @@ function isClaudeAuthError(msg) {
78381
78434
  const text = msg.toLowerCase();
78382
78435
  return text.includes("not logged in") || text.includes("please run /login");
78383
78436
  }
78437
+ function extractClaudeSchemaOutput(stdout) {
78438
+ let parsed;
78439
+ try {
78440
+ parsed = JSON.parse(stdout);
78441
+ } catch {
78442
+ return stdout;
78443
+ }
78444
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && Object.prototype.hasOwnProperty.call(parsed, "structured_output")) {
78445
+ return JSON.stringify(parsed.structured_output);
78446
+ }
78447
+ return stdout;
78448
+ }
78384
78449
  var ClaudeBackend = class {
78385
78450
  name = "claude";
78386
78451
  localFileAccess = true;
@@ -78466,7 +78531,7 @@ var ClaudeBackend = class {
78466
78531
  label: "claude"
78467
78532
  });
78468
78533
  if (result.stdout) {
78469
- return result.stdout;
78534
+ return opts.schema ? extractClaudeSchemaOutput(result.stdout) : result.stdout;
78470
78535
  }
78471
78536
  throw new ClaudeBackendError("claude completed without producing output");
78472
78537
  } catch (err) {
@@ -78716,42 +78781,48 @@ var OpenCodeBackend = class {
78716
78781
  process.on("SIGINT", onSigint);
78717
78782
  const stderrChunks = [];
78718
78783
  child.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
78719
- const closePromise = new Promise((resolve5, reject) => {
78720
- child.on("close", (code, signal) => {
78721
- if (timedOut) {
78722
- reject(new OpenCodeBackendError(
78723
- `opencode timed out after ${opts.timeoutSeconds}s`
78724
- ));
78725
- } else if (signal) {
78726
- reject(new OpenCodeBackendError(
78727
- `opencode killed by signal ${signal}`
78728
- ));
78729
- } else if (code !== 0 && code !== null) {
78730
- const stderr = Buffer.concat(stderrChunks).toString().trim();
78731
- reject(new OpenCodeBackendError(
78732
- stderr || `opencode exited with code ${code}`
78733
- ));
78734
- } else {
78735
- resolve5();
78736
- }
78737
- });
78738
- });
78784
+ let streamError = null;
78785
+ const closePromise = new Promise(
78786
+ (resolve5) => {
78787
+ child.on("close", (code, signal) => resolve5({ code, signal }));
78788
+ }
78789
+ );
78739
78790
  let chunkCount = 0;
78740
78791
  try {
78741
78792
  for await (const chunk of parseOpenCodeStreamJSON(
78742
78793
  child.stdout,
78743
- { onSessionCreated: opts.onSessionCreated }
78794
+ {
78795
+ onSessionCreated: opts.onSessionCreated,
78796
+ onError: (msg) => {
78797
+ if (streamError === null) streamError = msg;
78798
+ }
78799
+ }
78744
78800
  )) {
78745
78801
  chunkCount++;
78746
78802
  yield chunk;
78747
78803
  }
78748
- await closePromise;
78804
+ const { code, signal } = await closePromise;
78805
+ if (timedOut) {
78806
+ throw new OpenCodeBackendError(
78807
+ `opencode timed out after ${opts.timeoutSeconds}s`
78808
+ );
78809
+ }
78810
+ if (signal) {
78811
+ throw new OpenCodeBackendError(`opencode killed by signal ${signal}`);
78812
+ }
78813
+ if (code !== 0 && code !== null) {
78814
+ const stderr = Buffer.concat(stderrChunks).toString().trim();
78815
+ throw new OpenCodeBackendError(
78816
+ stderr || streamError || `opencode exited with code ${code}`
78817
+ );
78818
+ }
78819
+ if (streamError) {
78820
+ throw new OpenCodeBackendError(streamError);
78821
+ }
78749
78822
  if (chunkCount === 0) {
78750
78823
  throw new OpenCodeBackendError(OPENCODE_NO_OUTPUT_MESSAGE);
78751
78824
  }
78752
78825
  } catch (err) {
78753
- closePromise.catch(() => {
78754
- });
78755
78826
  if (err instanceof OpenCodeBackendError) throw err;
78756
78827
  const msg = err instanceof Error ? err.message : String(err);
78757
78828
  if (timedOut) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freibergergarcia/phone-a-friend",
3
- "version": "2.7.1",
3
+ "version": "2.8.0",
4
4
  "description": "CLI relay that lets AI coding agents collaborate",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phone-a-friend",
3
- "version": "2.7.1",
3
+ "version": "2.8.0",
4
4
  "description": "CLI relay that lets AI coding agents collaborate by sending prompts and repository context to backend agents.",
5
5
  "author": {
6
6
  "name": "Bruno Freiberger"
@@ -260,8 +260,8 @@ PAF_CONTEXT_EOF
260
260
 
261
261
  "$RELAY_BIN" --to codex --repo "$PWD" --prompt "$(cat "$PROMPT_FILE")" --context-file "$CONTEXT_FILE" $PAF_NO_DIFF [--fast] [--session <id>]
262
262
  # For gemini, omit --model by default (let auto-routing pick); see "Gemini model selection" below.
263
- # Do NOT pass --session to gemini it will error (see "Session continuity" below):
264
- "$RELAY_BIN" --to gemini --repo "$PWD" --prompt "$(cat "$PROMPT_FILE")" --context-file "$CONTEXT_FILE" $PAF_NO_DIFF [--fast]
263
+ # Gemini supports --session via native resume (see "Session continuity" below):
264
+ "$RELAY_BIN" --to gemini --repo "$PWD" --prompt "$(cat "$PROMPT_FILE")" --context-file "$CONTEXT_FILE" $PAF_NO_DIFF [--fast] [--session <id>]
265
265
  ```
266
266
 
267
267
  Use delimiter names that do not appear in the payload. The quoted heredoc
@@ -389,10 +389,10 @@ can be shorter (no need to re-send context from previous turns).
389
389
  can send deltas only.
390
390
  - **Ollama**: replays full history each call. Sessions work but prompt
391
391
  size grows with each turn. Keep follow-ups concise.
392
- - **Gemini**: `--session` is **not supported**. PaF rejects it with a
393
- RelayError (`--session is not supported by the gemini backend ...`).
394
- Each Gemini relay call must be self-contained. Do not pass `--session`
395
- with `--to gemini`.
392
+ - **Gemini**: native session resume (same as Codex/Claude/OpenCode).
393
+ PaF generates the session UUID client-side, pins it with `--session-id`
394
+ on the first call, and resumes with `--resume` later. Follow-up prompts
395
+ can send deltas only.
396
396
 
397
397
  On the FIRST relay under a new session label, PaF prints an informational
398
398
  stderr line: `[phone-a-friend] Session label "..." not found in store.
@@ -65,18 +65,14 @@ For each backend in the comma-separated list, the session label is `${TEAM_ID}-<
65
65
  | codex | native-session | yes (never used here; recursion guard) |
66
66
  | opencode | native-session | yes |
67
67
  | ollama | transcript-replay | yes |
68
- | gemini | unsupported | **no, omit the flag entirely** |
68
+ | gemini | native-session | yes |
69
69
 
70
70
  Build the relay flag accordingly per backend:
71
71
 
72
72
  ```bash
73
73
  session_flag_for() {
74
74
  local b="$1"
75
- if [ "$b" = "gemini" ]; then
76
- echo ""
77
- else
78
- echo "--session ${TEAM_ID}-${b}"
79
- fi
75
+ echo "--session ${TEAM_ID}-${b}"
80
76
  }
81
77
  ```
82
78
 
@@ -260,8 +260,8 @@ PAF_CONTEXT_EOF
260
260
 
261
261
  "$RELAY_BIN" --to codex --repo "$PWD" --prompt "$(cat "$PROMPT_FILE")" --context-file "$CONTEXT_FILE" $PAF_NO_DIFF [--fast] [--session <id>]
262
262
  # For gemini, omit --model by default (let auto-routing pick); see "Gemini model selection" below.
263
- # Do NOT pass --session to gemini it will error (see "Session continuity" below):
264
- "$RELAY_BIN" --to gemini --repo "$PWD" --prompt "$(cat "$PROMPT_FILE")" --context-file "$CONTEXT_FILE" $PAF_NO_DIFF [--fast]
263
+ # Gemini supports --session via native resume (see "Session continuity" below):
264
+ "$RELAY_BIN" --to gemini --repo "$PWD" --prompt "$(cat "$PROMPT_FILE")" --context-file "$CONTEXT_FILE" $PAF_NO_DIFF [--fast] [--session <id>]
265
265
  ```
266
266
 
267
267
  Use delimiter names that do not appear in the payload. The quoted heredoc
@@ -389,10 +389,10 @@ can be shorter (no need to re-send context from previous turns).
389
389
  can send deltas only.
390
390
  - **Ollama**: replays full history each call. Sessions work but prompt
391
391
  size grows with each turn. Keep follow-ups concise.
392
- - **Gemini**: `--session` is **not supported**. PaF rejects it with a
393
- RelayError (`--session is not supported by the gemini backend ...`).
394
- Each Gemini relay call must be self-contained. Do not pass `--session`
395
- with `--to gemini`.
392
+ - **Gemini**: native session resume (same as Codex/Claude/OpenCode).
393
+ PaF generates the session UUID client-side, pins it with `--session-id`
394
+ on the first call, and resumes with `--resume` later. Follow-up prompts
395
+ can send deltas only.
396
396
 
397
397
  On the FIRST relay under a new session label, PaF prints an informational
398
398
  stderr line: `[phone-a-friend] Session label "..." not found in store.
@@ -65,18 +65,14 @@ For each backend in the comma-separated list, the session label is `${TEAM_ID}-<
65
65
  | codex | native-session | yes (never used here; recursion guard) |
66
66
  | opencode | native-session | yes |
67
67
  | ollama | transcript-replay | yes |
68
- | gemini | unsupported | **no, omit the flag entirely** |
68
+ | gemini | native-session | yes |
69
69
 
70
70
  Build the relay flag accordingly per backend:
71
71
 
72
72
  ```bash
73
73
  session_flag_for() {
74
74
  local b="$1"
75
- if [ "$b" = "gemini" ]; then
76
- echo ""
77
- else
78
- echo "--session ${TEAM_ID}-${b}"
79
- fi
75
+ echo "--session ${TEAM_ID}-${b}"
80
76
  }
81
77
  ```
82
78