agent-dag 1.34.5 → 1.35.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.
- package/README.md +3 -1
- package/bin/deck.js +13 -3
- package/dist/web/assets/{index-CDgBtXXg.js → index-CMZYhML1.js} +1 -1
- package/dist/web/index.html +1 -1
- package/hook/hook.js +64 -21
- package/package.json +1 -1
- package/src/server/index.mjs +93 -3
- package/src/server/log-writer.mjs +34 -19
package/dist/web/index.html
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
document.documentElement.setAttribute("data-theme", stored === "light" ? "light" : "dark");
|
|
41
41
|
})();
|
|
42
42
|
</script>
|
|
43
|
-
<script type="module" crossorigin src="/assets/index-
|
|
43
|
+
<script type="module" crossorigin src="/assets/index-CMZYhML1.js"></script>
|
|
44
44
|
<link rel="stylesheet" crossorigin href="/assets/index-CRPobBZf.css">
|
|
45
45
|
</head>
|
|
46
46
|
<body>
|
package/hook/hook.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// agent-dag hook forwarder. Invoked by Claude Code or Codex CLI as a command
|
|
3
3
|
// hook. Reads stdin (event JSON), tags it with the provider passed via
|
|
4
|
-
// `--provider <name>`, finds
|
|
5
|
-
// discovery files in <claude config dir>/agent-dag
|
|
6
|
-
// is the deck
|
|
7
|
-
// cleaned up.
|
|
4
|
+
// `--provider <name>`, finds every agent-dag server whose workspace contains the
|
|
5
|
+
// session — via the discovery files in <claude config dir>/agent-dag/ — makes
|
|
6
|
+
// each one prove it is the deck its file describes, and POSTs the payload. Dead
|
|
7
|
+
// instances are cleaned up.
|
|
8
8
|
"use strict";
|
|
9
9
|
|
|
10
10
|
const fs = require("fs");
|
|
@@ -86,6 +86,36 @@ function cwdInWorkspace(cwd, workspace, platform = process.platform) {
|
|
|
86
86
|
return a.startsWith(b.endsWith(p.sep) ? b : b + p.sep);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Does a deck scoped to `workspace` capture a session running in `cwd`? This is
|
|
91
|
+
* the whole of what `--workspace` means, and it is a question about ONE deck: it
|
|
92
|
+
* asks nothing about the others that may also be up, so a deck's answer never
|
|
93
|
+
* depends on who else is running.
|
|
94
|
+
*
|
|
95
|
+
* An empty workspace is the default — machine-wide — and captures everything.
|
|
96
|
+
* It is answered before cwdInWorkspace rather than passed to it because
|
|
97
|
+
* p.resolve("") is the resolving process's own cwd, which here is the agent's,
|
|
98
|
+
* so an unscoped deck would be silently scoped to whatever directory the user
|
|
99
|
+
* happened to run their agent in.
|
|
100
|
+
*
|
|
101
|
+
* A session that never said where it runs is inside no workspace, so only an
|
|
102
|
+
* unscoped deck sees it. Unreachable from main(), which exits before this on a
|
|
103
|
+
* payload with no cwd — it is here because the rule has to be stated the same
|
|
104
|
+
* way on both sides to be pinned against the other one.
|
|
105
|
+
*
|
|
106
|
+
* src/server/log-writer.mjs answers this same question, for the sessions the
|
|
107
|
+
* server builds itself out of Codex's rollout files, under the name
|
|
108
|
+
* codexCwdInWorkspace — this script is copied out of the package and run
|
|
109
|
+
* standalone, so it cannot import that copy. A test walks one table of paths
|
|
110
|
+
* through both: a disagreement between them is `--workspace` meaning two
|
|
111
|
+
* different things depending on which CLI produced the session.
|
|
112
|
+
*/
|
|
113
|
+
function capturesSession(cwd, workspace, platform = process.platform) {
|
|
114
|
+
if (!workspace || typeof workspace !== "string") return true;
|
|
115
|
+
if (!cwd || typeof cwd !== "string") return false;
|
|
116
|
+
return cwdInWorkspace(cwd, workspace, platform);
|
|
117
|
+
}
|
|
118
|
+
|
|
89
119
|
function isAlive(pid) {
|
|
90
120
|
try { process.kill(pid, 0); return true; }
|
|
91
121
|
catch (e) { return e && e.code === "EPERM"; }
|
|
@@ -315,7 +345,24 @@ function main() {
|
|
|
315
345
|
} catch { return process.exit(0); }
|
|
316
346
|
if (!files.length) return process.exit(0);
|
|
317
347
|
|
|
318
|
-
|
|
348
|
+
// Every deck whose workspace contains this cwd, and nothing else decides it.
|
|
349
|
+
//
|
|
350
|
+
// This used to sort the matches by how long each deck's workspace path was
|
|
351
|
+
// and deliver only to the longest — so a deck scoped to /Users/x/proj TOOK
|
|
352
|
+
// that tree's sessions away from a machine-wide deck, which then sat there
|
|
353
|
+
// showing nothing while `--all` promised it captured every session on this
|
|
354
|
+
// machine. Nothing documented that, and the server's own Codex capture never
|
|
355
|
+
// did it: each deck tails the rollout files itself and evaluates its own
|
|
356
|
+
// workspace, so a Codex session inside a scoped tree appeared on both decks
|
|
357
|
+
// while the Claude session beside it appeared on one. One flag, one path,
|
|
358
|
+
// two answers.
|
|
359
|
+
//
|
|
360
|
+
// The fan-out is the documented meaning and the one kept: `--workspace` says
|
|
361
|
+
// which sessions a deck captures, not which sessions it takes from the decks
|
|
362
|
+
// around it. It is also what electWriters below already assumes — several
|
|
363
|
+
// decks drawing one event is the case it exists to keep from being written
|
|
364
|
+
// to one log several times.
|
|
365
|
+
const targets = [];
|
|
319
366
|
for (const file of files) {
|
|
320
367
|
let d;
|
|
321
368
|
try { d = JSON.parse(fs.readFileSync(path.join(DIR, file), "utf8")); } catch { continue; }
|
|
@@ -329,29 +376,25 @@ function main() {
|
|
|
329
376
|
continue;
|
|
330
377
|
}
|
|
331
378
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
379
|
+
// "" is machine-wide and must never reach normPath: resolving it would
|
|
380
|
+
// produce this hook's own cwd — the agent's — and scope a deck that asked
|
|
381
|
+
// for no scope at all. Any other spelling is canonicalized here, which is
|
|
382
|
+
// now a second pass over a path bin/deck.js already canonicalized before
|
|
383
|
+
// publishing it — kept because a deck old enough to have published a
|
|
384
|
+
// relative one is still entitled to its events.
|
|
385
|
+
const ws = d.workspace === "" ? "" : normPath(d.workspace);
|
|
386
|
+
if (capturesSession(resolvedCwd, ws)) targets.push(d);
|
|
340
387
|
}
|
|
341
388
|
|
|
342
|
-
if (!
|
|
343
|
-
|
|
344
|
-
matches.sort((a, b) => b.wsLen - a.wsLen);
|
|
345
|
-
const bestLen = matches[0].wsLen;
|
|
346
|
-
const targets = matches.filter(m => m.wsLen === bestLen);
|
|
389
|
+
if (!targets.length) return process.exit(0);
|
|
347
390
|
|
|
348
391
|
// One deck per events log records this event; the others only draw it.
|
|
349
|
-
const writers = electWriters(targets
|
|
392
|
+
const writers = electWriters(targets);
|
|
350
393
|
|
|
351
394
|
let pending = targets.length;
|
|
352
395
|
const done = () => { if (--pending <= 0) process.exit(0); };
|
|
353
396
|
|
|
354
|
-
for (const
|
|
397
|
+
for (const d of targets) deliver(d, taggedInput, writers.has(d), done);
|
|
355
398
|
});
|
|
356
399
|
}
|
|
357
400
|
|
|
@@ -360,5 +403,5 @@ function main() {
|
|
|
360
403
|
// require() it exports the rules it decides by — matching, election, the
|
|
361
404
|
// handshake — and starts nothing, which is what lets them be tested without a
|
|
362
405
|
// 1.5s exit timer in the test runner.
|
|
363
|
-
module.exports = { cwdInWorkspace, foldsCase, electWriters, challengeProof, requiresProof };
|
|
406
|
+
module.exports = { capturesSession, cwdInWorkspace, foldsCase, electWriters, challengeProof, requiresProof };
|
|
364
407
|
if (require.main === module) main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-dag",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.0",
|
|
4
4
|
"description": "Live deck of Claude Code and Codex agents — watch parallel subagents fork, call tools, and return on one calm canvas. Also available as npx ccdeck and npx agent-dag.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/server/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Single-file pure Node HTTP server, zero deps.
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { readFile, stat, mkdir, appendFile, open, truncate, readdir, unlink } from "node:fs/promises";
|
|
5
|
-
import { createReadStream, existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { createReadStream, existsSync, readFileSync, realpathSync } from "node:fs";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { extname, join, resolve, dirname as pdirname } from "node:path";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -785,12 +785,17 @@ function maybeResolveCodex(payload) {
|
|
|
785
785
|
// object {timestamp, type, payload}; we map the relevant ones to the same
|
|
786
786
|
// synthetic hook payloads the reducer already understands:
|
|
787
787
|
// session_meta → SessionStart
|
|
788
|
-
// event_msg/user_message → UserPromptSubmit
|
|
788
|
+
// event_msg/user_message → UserPromptSubmit (Codex ≤ 0.144)
|
|
789
|
+
// event_msg/item_completed/UserMessage → UserPromptSubmit (Codex ≥ 0.147)
|
|
789
790
|
// response_item/function_call → PreToolUse
|
|
790
791
|
// response_item/function_call_output → PostToolUse
|
|
791
792
|
// event_msg/token_count → UsageObserved
|
|
792
793
|
// event_msg/task_started (+window) → ModelObserved (context window)
|
|
794
|
+
// event_msg/task_complete → Stop (the turn finished)
|
|
795
|
+
// event_msg/turn_aborted → Stop (the turn was interrupted)
|
|
793
796
|
// turn_context / response_item.model → model snapshot (ModelObserved on change)
|
|
797
|
+
// There is deliberately no SessionEnd here: Codex writes no session-close
|
|
798
|
+
// record, so the end of a SESSION is still inferred by sweepStaleSessions.
|
|
794
799
|
// Events are emitted with source "codex" so pushEvent skips the Claude-only
|
|
795
800
|
// transcript enrichment (which needs transcript_path / hook events) but still
|
|
796
801
|
// broadcasts them exactly like a hook event, and persists them when this deck is
|
|
@@ -890,10 +895,27 @@ async function readCodexHeader(path) {
|
|
|
890
895
|
return null;
|
|
891
896
|
}
|
|
892
897
|
|
|
898
|
+
/**
|
|
899
|
+
* The human's prompt out of a 0.147-era `item_completed` item.
|
|
900
|
+
*
|
|
901
|
+
* `item.content` is an array of parts — every UserMessage observed carries a
|
|
902
|
+
* single `{ type: "text", text, text_elements }` — so the parts are joined
|
|
903
|
+
* rather than indexed, and a part with no string `text` contributes nothing
|
|
904
|
+
* instead of printing "undefined" into the prompt the card shows.
|
|
905
|
+
*/
|
|
906
|
+
function codexItemText(item) {
|
|
907
|
+
const parts = Array.isArray(item && item.content) ? item.content : [];
|
|
908
|
+
return parts.map(p => (p && typeof p.text === "string" ? p.text : "")).join("");
|
|
909
|
+
}
|
|
910
|
+
|
|
893
911
|
// Map one parsed rollout object to a synthetic hook payload (or null to skip).
|
|
894
912
|
// Mutates codexSessionModel and returns { payload, modelEvent } where
|
|
895
913
|
// modelEvent is an optional ModelObserved to emit first when the model changed.
|
|
896
|
-
|
|
914
|
+
//
|
|
915
|
+
// Exported for the tests: this is the whole of the Codex translation, and the
|
|
916
|
+
// lifecycle it produces is worth pinning against the real reducer without
|
|
917
|
+
// standing up a watcher, a temp home and a 1.5s poll to get at it.
|
|
918
|
+
export function codexObjToPayload(obj, sid, cwd) {
|
|
897
919
|
const type = obj && obj.type;
|
|
898
920
|
const pl = (obj && obj.payload) || {};
|
|
899
921
|
const base = { session_id: sid, cwd, provider: "codex" };
|
|
@@ -913,12 +935,49 @@ function codexObjToPayload(obj, sid, cwd) {
|
|
|
913
935
|
const prompt = typeof pl.message === "string" ? pl.message : "";
|
|
914
936
|
return { ...base, hook_event_name: "UserPromptSubmit", prompt, model };
|
|
915
937
|
}
|
|
938
|
+
// Codex 0.147 stopped writing `user_message` and writes the same submission
|
|
939
|
+
// as an `item_completed` carrying a `UserMessage` item instead. The two are
|
|
940
|
+
// mutually exclusive per CLI version — across the rollouts sampled here the
|
|
941
|
+
// 0.144 files have `user_message` and no `item_completed` at all, and the
|
|
942
|
+
// 0.147 files have exactly one `UserMessage` item per turn and no
|
|
943
|
+
// `user_message` — so handling both names emits one prompt per turn either
|
|
944
|
+
// way rather than two on either version. The item is the human's typed text
|
|
945
|
+
// only: the AGENTS.md preamble Codex prepends is written as a bare
|
|
946
|
+
// `response_item` with role "user" and never gets an item of its own.
|
|
947
|
+
//
|
|
948
|
+
// This matters far beyond the prompt text. `UserPromptSubmit` is what puts
|
|
949
|
+
// a settled root back to `active` (reducer.ts), so on 0.147 it is the ONLY
|
|
950
|
+
// thing that reopens a session for its second turn — without it the `Stop`
|
|
951
|
+
// below would trade "live forever" for "done forever", which is not better.
|
|
952
|
+
if (pl.type === "item_completed" && pl.item && pl.item.type === "UserMessage") {
|
|
953
|
+
return { ...base, hook_event_name: "UserPromptSubmit", prompt: codexItemText(pl.item), model };
|
|
954
|
+
}
|
|
916
955
|
if (pl.type === "token_count" && pl.info && pl.info.total_token_usage) {
|
|
917
956
|
return { ...base, hook_event_name: "UsageObserved", usage: pl.info.total_token_usage, model };
|
|
918
957
|
}
|
|
919
958
|
if (pl.type === "task_started" && typeof pl.model_context_window === "number") {
|
|
920
959
|
return { ...base, hook_event_name: "ModelObserved", model, model_context_window: pl.model_context_window };
|
|
921
960
|
}
|
|
961
|
+
// The end of a turn, which is the only end Codex ever announces. Both names
|
|
962
|
+
// are one outcome as far as the deck is concerned — the turn is over and
|
|
963
|
+
// nothing is running — so both settle the root the way Claude's own Stop
|
|
964
|
+
// hook does. They are also exhaustive: across the rollouts sampled here
|
|
965
|
+
// every `task_started` is answered by exactly one of the two (54 completes
|
|
966
|
+
// + 1 abort for 55 starts), so no turn is left open by this mapping and
|
|
967
|
+
// none is closed twice. `turn_aborted` is the Esc key, and it is the case
|
|
968
|
+
// that matters most on the deck: pressing Esc is precisely when the user is
|
|
969
|
+
// watching to confirm the thing stopped.
|
|
970
|
+
//
|
|
971
|
+
// Per TURN, not per session, and that is correct: Codex writes no
|
|
972
|
+
// session-close record at all — a rollout simply stops growing when the
|
|
973
|
+
// terminal goes away — so `sweepStaleSessions` remains the only thing that
|
|
974
|
+
// ends a Codex *session*, and it must. What changes is that it now only
|
|
975
|
+
// ever sees the sessions it was written for: the ones that died without
|
|
976
|
+
// finishing. A turn that really ended is settled here, at the moment it
|
|
977
|
+
// ended, with no `reaped` flag, because it was a finish and not a guess.
|
|
978
|
+
if (pl.type === "task_complete" || pl.type === "turn_aborted") {
|
|
979
|
+
return { ...base, hook_event_name: "Stop", model };
|
|
980
|
+
}
|
|
922
981
|
return null;
|
|
923
982
|
}
|
|
924
983
|
if (type === "response_item") {
|
|
@@ -1859,6 +1918,37 @@ async function handleSoundHookSet(req, res) {
|
|
|
1859
1918
|
// can read before a single event has arrived.
|
|
1860
1919
|
let _workspace = "";
|
|
1861
1920
|
|
|
1921
|
+
/**
|
|
1922
|
+
* The one spelling of `--workspace` everything downstream compares against.
|
|
1923
|
+
* Empty — including a value that is nothing but spaces — stays empty, which is
|
|
1924
|
+
* how every reader of it says "machine-wide".
|
|
1925
|
+
*
|
|
1926
|
+
* Called once, in bin/deck.js, where the flag arrives: that process's cwd is the
|
|
1927
|
+
* shell the user typed the command in, and it is the only process on either
|
|
1928
|
+
* capture path whose cwd is the one a relative `--workspace ./sub` was written
|
|
1929
|
+
* against. The raw string used to go straight into the discovery file, and
|
|
1930
|
+
* hook.js resolved it inside its own process — which the host CLI runs with the
|
|
1931
|
+
* AGENT's cwd — so `--workspace ./sub` scoped Claude sessions to a `sub`
|
|
1932
|
+
* directory under whatever the agent happened to be working in, a different
|
|
1933
|
+
* directory per agent and none of them the one asked for. The Codex path
|
|
1934
|
+
* resolved the same string in the server process and was right. Resolving here
|
|
1935
|
+
* makes both of them right, and for the same reason bin/deck.js already resolves
|
|
1936
|
+
* the events log before publishing it: what goes in the discovery file is read
|
|
1937
|
+
* by other processes that cannot reconstruct the context it was written in.
|
|
1938
|
+
*
|
|
1939
|
+
* Symlinks are resolved too, because a process's cwd — which is what both
|
|
1940
|
+
* providers report — comes from getcwd() and has none left in it. Without this,
|
|
1941
|
+
* `--workspace /tmp/proj` on a Mac is scoped to /tmp/proj while every session
|
|
1942
|
+
* inside it reports /private/tmp/proj, and the deck stays empty. A path that
|
|
1943
|
+
* does not exist yet keeps its resolved form rather than failing: scoping a deck
|
|
1944
|
+
* to a directory you are about to create is not an error.
|
|
1945
|
+
*/
|
|
1946
|
+
export function canonicalWorkspace(raw) {
|
|
1947
|
+
if (typeof raw !== "string" || raw.trim() === "") return "";
|
|
1948
|
+
const abs = resolve(raw);
|
|
1949
|
+
try { return realpathSync(abs); } catch { return abs; }
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1862
1952
|
function handleHealth(_req, res) {
|
|
1863
1953
|
send(res, 200, {
|
|
1864
1954
|
ok: true,
|
|
@@ -73,17 +73,29 @@ export function electWriters(decks, platform = process.platform) {
|
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* Would a deck scoped to `workspace` capture a rollout running in `cwd`? An
|
|
76
|
-
* empty workspace is unscoped and captures every session
|
|
76
|
+
* empty workspace is unscoped and captures every session; a rollout that never
|
|
77
|
+
* said where it runs is inside no workspace, so only an unscoped deck draws it.
|
|
77
78
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
79
|
+
* This answers two questions with one function — whether THIS deck tails a
|
|
80
|
+
* rollout, and whether another deck tails it too — and that is only sound while
|
|
81
|
+
* the rule below is the rule every deck actually runs. Model another deck's
|
|
82
|
+
* capture with anything else and the election covers the wrong set: a deck that
|
|
83
|
+
* writes without being elected, or an elected deck that never opened the file.
|
|
84
|
+
*
|
|
85
|
+
* It is also the rule hook/hook.js runs for the sessions it delivers, under the
|
|
86
|
+
* name capturesSession — that script is copied out of the package and run
|
|
87
|
+
* standalone, so the two are written twice and pinned equal by a test walking
|
|
88
|
+
* one table of paths through both. They were not equal: case was folded here on
|
|
89
|
+
* every platform, so on Linux a deck scoped to /srv/proj captured Codex sessions
|
|
90
|
+
* from /srv/Proj and Claude sessions from neither. Those are two real
|
|
91
|
+
* directories there, and the hook's own comment says what folding them together
|
|
92
|
+
* costs — a deck handed the events of a tree it was not scoped to. So the fold
|
|
93
|
+
* is per-platform on both sides now, and `--workspace` means one thing.
|
|
94
|
+
*
|
|
95
|
+
* (The narrow window that opens: two decks on Linux whose workspaces differ only
|
|
96
|
+
* in case, one of them old enough to still fold, both containing one rollout's
|
|
97
|
+
* cwd. Each models the other as tailing the file; one of them is wrong, and the
|
|
98
|
+
* cost is a single log line written twice.)
|
|
87
99
|
*
|
|
88
100
|
* The platform is a parameter, following the hook's cwdInWorkspace and
|
|
89
101
|
* spawnSpec in src/server/exec.mjs, so the Windows separator is testable from a
|
|
@@ -93,8 +105,9 @@ export function codexCwdInWorkspace(cwd, workspace, platform = process.platform)
|
|
|
93
105
|
if (!workspace || typeof workspace !== "string") return true;
|
|
94
106
|
if (!cwd || typeof cwd !== "string") return false;
|
|
95
107
|
const p = platform === "win32" ? win32 : posix;
|
|
96
|
-
const
|
|
97
|
-
const
|
|
108
|
+
const fold = s => (foldsCase(platform) ? s.toLowerCase() : s);
|
|
109
|
+
const a = fold(p.resolve(cwd));
|
|
110
|
+
const b = fold(p.resolve(workspace));
|
|
98
111
|
if (a === b) return true;
|
|
99
112
|
// A root ("C:\", "/") already ends in the separator; appending a second one
|
|
100
113
|
// would match nothing.
|
|
@@ -106,13 +119,15 @@ export function codexCwdInWorkspace(cwd, workspace, platform = process.platform)
|
|
|
106
119
|
* it? `decks` is every deck registered right now, `pid` identifies this one
|
|
107
120
|
* among them, and `cwd` is the workspace the rollout is running in.
|
|
108
121
|
*
|
|
109
|
-
* The group is every deck that tails this same rollout
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
122
|
+
* The group is every deck that tails this same rollout: each deck decides for
|
|
123
|
+
* itself, so all of them whose workspace contains the cwd read the file and all
|
|
124
|
+
* of them would write it. The hook builds the same group the same way for the
|
|
125
|
+
* events it delivers — it used to narrow them to the longest workspace match
|
|
126
|
+
* first, which is the asymmetry the predicate above describes the end of. A deck
|
|
127
|
+
* started with `--no-codex` tails nothing and is left out; electing it would
|
|
128
|
+
* mean the rollout's events reach no log at all. A deck too old to say either
|
|
129
|
+
* way is assumed to be tailing, which is what it was doing before this field
|
|
130
|
+
* existed.
|
|
116
131
|
*/
|
|
117
132
|
export function writesCodexLog({ decks, pid, cwd, platform = process.platform }) {
|
|
118
133
|
const live = Array.isArray(decks) ? decks : [];
|