pi-better-background-tasks 0.3.0 → 0.4.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 +41 -0
- package/package.json +1 -1
- package/src/failures.ts +77 -3
- package/src/index.ts +5 -0
- package/src/navigator-provider.ts +2 -1
- package/src/output.ts +78 -48
- package/src/runtime.ts +173 -17
- package/src/sandbox.ts +6 -4
- package/src/shared-callback-batcher.ts +34 -3
- package/src/shared-failure-observations.ts +378 -47
- package/src/shared-log-utils.ts +114 -0
- package/src/shared-navigator.ts +12 -10
- package/src/shared-sandbox-core.ts +660 -17
- package/src/tools.ts +107 -32
- package/src/types.ts +7 -0
package/src/shared-log-utils.ts
CHANGED
|
@@ -798,6 +798,120 @@ export function revisionOf(value: unknown): string {
|
|
|
798
798
|
return createHash("sha256").update(JSON.stringify(value) ?? "undefined").digest("base64url").slice(0, 32);
|
|
799
799
|
}
|
|
800
800
|
|
|
801
|
+
// ---------------------------------------------------------------------------
|
|
802
|
+
// Shared output-control parameters, session scope keys, and log identity facts
|
|
803
|
+
// (#321/#323). Subagent and background-task tools both read these, so the two
|
|
804
|
+
// families cannot drift.
|
|
805
|
+
// ---------------------------------------------------------------------------
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Canonical output-control parameter names and their deprecated aliases.
|
|
809
|
+
* Canonical names follow this repo's dominant snake_case tool-parameter style
|
|
810
|
+
* (`max_log_bytes`, `timeout_seconds`, `exclude_tools`, `sandbox_dir`, ...).
|
|
811
|
+
*/
|
|
812
|
+
export const OUTPUT_CONTROL_ALIASES = {
|
|
813
|
+
max_bytes: "maxBytes",
|
|
814
|
+
lines: "tail_lines",
|
|
815
|
+
} as const;
|
|
816
|
+
|
|
817
|
+
export interface OutputControls {
|
|
818
|
+
/** Raw requested byte budget (canonical `max_bytes`, else deprecated `maxBytes`). */
|
|
819
|
+
maxBytes?: unknown;
|
|
820
|
+
/** Raw requested line count (canonical `lines`, else deprecated `tail_lines`). */
|
|
821
|
+
lines?: unknown;
|
|
822
|
+
/** Deprecated alias names the caller used (whether or not the canonical name won). */
|
|
823
|
+
deprecated: string[];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Resolve output-control parameters. Both spellings work; when both are given
|
|
828
|
+
* the canonical name wins. Values are returned unvalidated so each surface
|
|
829
|
+
* keeps its own defaults and hard caps.
|
|
830
|
+
*/
|
|
831
|
+
export function readOutputControls(params: unknown): OutputControls {
|
|
832
|
+
const p = (params && typeof params === "object" ? params : {}) as Record<string, unknown>;
|
|
833
|
+
const deprecated: string[] = [];
|
|
834
|
+
const pick = (canonical: keyof typeof OUTPUT_CONTROL_ALIASES): unknown => {
|
|
835
|
+
const alias = OUTPUT_CONTROL_ALIASES[canonical];
|
|
836
|
+
if (p[alias] !== undefined) deprecated.push(alias);
|
|
837
|
+
return p[canonical] !== undefined ? p[canonical] : p[alias];
|
|
838
|
+
};
|
|
839
|
+
const maxBytes = pick("max_bytes");
|
|
840
|
+
const lines = pick("lines");
|
|
841
|
+
return {
|
|
842
|
+
...(maxBytes !== undefined ? { maxBytes } : {}),
|
|
843
|
+
...(lines !== undefined ? { lines } : {}),
|
|
844
|
+
deprecated,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** Explicit opt-in sections for otherwise-omitted spend and tool facts. */
|
|
849
|
+
export const OUTPUT_INCLUDE_VALUES = ["cost", "tools"] as const;
|
|
850
|
+
export type OutputInclude = (typeof OUTPUT_INCLUDE_VALUES)[number];
|
|
851
|
+
|
|
852
|
+
/** Parse an `include` request into known values and unknown ones (both deduplicated). */
|
|
853
|
+
export function readOutputInclude(value: unknown): { include: Set<OutputInclude>; unknown: string[] } {
|
|
854
|
+
const raw = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
|
|
855
|
+
const include = new Set<OutputInclude>();
|
|
856
|
+
const unknown: string[] = [];
|
|
857
|
+
for (const entry of raw) {
|
|
858
|
+
const name = String(entry ?? "").trim().toLowerCase();
|
|
859
|
+
if (!name) continue;
|
|
860
|
+
if ((OUTPUT_INCLUDE_VALUES as readonly string[]).includes(name)) include.add(name as OutputInclude);
|
|
861
|
+
else if (!unknown.includes(name)) unknown.push(name);
|
|
862
|
+
}
|
|
863
|
+
return { include, unknown };
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/** Session identity that a read or mutation is scoped to. */
|
|
867
|
+
export interface ScopeOrigin {
|
|
868
|
+
cwd: string;
|
|
869
|
+
sessionId?: string;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/** Stable digest of one cwd + session origin (the digest the subagent registry indexes by). */
|
|
873
|
+
export function originScopeDigest(origin: ScopeOrigin): string {
|
|
874
|
+
return createHash("sha256").update(origin.cwd).update("\0").update(origin.sessionId ?? "").digest("hex").slice(0, 24);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Cursor scope. Every pagination and revision cursor binds this key, so a
|
|
879
|
+
* cursor never crosses session scopes. `unavailable` wins over an origin: a
|
|
880
|
+
* caller whose session identity could not be read is never the same scope as
|
|
881
|
+
* a readable one.
|
|
882
|
+
*/
|
|
883
|
+
export function sessionScopeKey(input: {
|
|
884
|
+
all?: boolean;
|
|
885
|
+
unavailable?: boolean;
|
|
886
|
+
origin?: ScopeOrigin;
|
|
887
|
+
/** Key when there is no origin at all (for example, a per-process scope). */
|
|
888
|
+
fallback?: string;
|
|
889
|
+
}): string {
|
|
890
|
+
if (input.all) return "all";
|
|
891
|
+
if (input.unavailable) return "session:unavailable";
|
|
892
|
+
if (input.origin) return `session:${originScopeDigest(input.origin)}`;
|
|
893
|
+
return input.fallback ?? "none";
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Identity facts of a retained log for a content revision: device, inode,
|
|
898
|
+
* size, and whole-millisecond mtime, or the read error. A deleted, replaced,
|
|
899
|
+
* or appended log is a content change.
|
|
900
|
+
*/
|
|
901
|
+
export function logIdentityFacts(path: string): unknown {
|
|
902
|
+
try {
|
|
903
|
+
const stats = statSync(path);
|
|
904
|
+
return [stats.dev, stats.ino, stats.size, Math.trunc(stats.mtimeMs)];
|
|
905
|
+
} catch (error) {
|
|
906
|
+
return ["unreadable", (error as NodeJS.ErrnoException).code ?? String(error)];
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** Content revision: consumer lifecycle facts followed by the retained log's identity. */
|
|
911
|
+
export function lifecycleContentRevision(facts: readonly unknown[], logPath: string): string {
|
|
912
|
+
return revisionOf([...facts, logIdentityFacts(logPath)]);
|
|
913
|
+
}
|
|
914
|
+
|
|
801
915
|
// ---------------------------------------------------------------------------
|
|
802
916
|
// Row pages (lists). Keyset cursors keep paging stable while new rows arrive.
|
|
803
917
|
// ---------------------------------------------------------------------------
|
package/src/shared-navigator.ts
CHANGED
|
@@ -106,7 +106,7 @@ export const CLOSE_CONFIRM_STATUS_KEY = "background-work-close";
|
|
|
106
106
|
export const MAIN_LIST_WIDGET_KEY = "background-work-list";
|
|
107
107
|
export const DETAIL_TICK_MS = 10_000;
|
|
108
108
|
export const CLOSE_ARM_MS = 3000;
|
|
109
|
-
export const DEFAULT_LOG_TAIL_ROWS =
|
|
109
|
+
export const DEFAULT_LOG_TAIL_ROWS = 25;
|
|
110
110
|
export const LOG_TAIL_ROW_CHOICES = [10, 25] as const;
|
|
111
111
|
const MAIN_LIST_FALLBACK_WIDTH = 100;
|
|
112
112
|
const DETAIL_OVERLAY_BOTTOM_MARGIN_ROWS = 0;
|
|
@@ -542,7 +542,10 @@ function stableFacts(facts: string[] | undefined): string[] {
|
|
|
542
542
|
|
|
543
543
|
function rowSummary(row: InternalRow): string {
|
|
544
544
|
if (row.providerId === "subagents") {
|
|
545
|
+
const attention = row.facts?.map(singleLine).filter(Boolean);
|
|
546
|
+
if (attention?.length) return `${singleLine(row.status)} · ${attention.join(" · ")}`;
|
|
545
547
|
const parts: string[] = [];
|
|
548
|
+
if (row.statusTone !== "running" && row.kind !== "main agent") parts.push(singleLine(row.status));
|
|
546
549
|
if (row.model) parts.push(row.effort ? `${singleLine(row.model)} ${singleLine(row.effort)}` : singleLine(row.model));
|
|
547
550
|
if (row.tool) parts.push(`tool ${singleLine(row.tool)}`);
|
|
548
551
|
if (row.tokens) parts.push(singleLine(row.tokens));
|
|
@@ -928,7 +931,7 @@ function createOverlayComponent(
|
|
|
928
931
|
transcriptDetail = null;
|
|
929
932
|
transcriptComponent = null;
|
|
930
933
|
contentLines = mode === "detail"
|
|
931
|
-
? buildDetailLines(detail, width, deps.truncate, fg, { expandedSections, logTailRows, minRows: detailRows, bottomFooter: false })
|
|
934
|
+
? buildDetailLines(detail, width, deps.truncate, fg, { expandedSections, logTailRows, minRows: detailRows, maxRows: detailRows, bottomFooter: false })
|
|
932
935
|
: buildListLines(overlayState, width, deps.truncate, fg);
|
|
933
936
|
}
|
|
934
937
|
if (mode !== "detail") return contentLines;
|
|
@@ -1024,14 +1027,11 @@ function buildTranscriptDetailLines(
|
|
|
1024
1027
|
];
|
|
1025
1028
|
const transcriptHeader = ["", dim(section(`transcript · latest ${tailRows} rows`, width), fg)];
|
|
1026
1029
|
if (detail.transcriptDiagnostic) transcriptHeader.push(` ${dim(detail.transcriptDiagnostic, fg)}`);
|
|
1030
|
+
// Metadata always renders; the transcript tail takes only the rows left over (the tail size is a cap, not a guarantee).
|
|
1031
|
+
lines.push(...optionalMetadata);
|
|
1027
1032
|
if (options.maxRows !== undefined) {
|
|
1028
|
-
const
|
|
1029
|
-
const transcriptBudget = Math.max(1, options.maxRows - fixedRows);
|
|
1033
|
+
const transcriptBudget = Math.max(1, options.maxRows - lines.length - transcriptHeader.length - 1);
|
|
1030
1034
|
shownTranscriptLines = shownTranscriptLines.slice(-transcriptBudget);
|
|
1031
|
-
const metadataBudget = Math.max(0, options.maxRows - fixedRows - shownTranscriptLines.length);
|
|
1032
|
-
lines.push(...optionalMetadata.slice(0, metadataBudget));
|
|
1033
|
-
} else {
|
|
1034
|
-
lines.push(...optionalMetadata);
|
|
1035
1035
|
}
|
|
1036
1036
|
lines.push(...transcriptHeader);
|
|
1037
1037
|
lines.push(...shownTranscriptLines);
|
|
@@ -1120,7 +1120,7 @@ function buildDetailLines(
|
|
|
1120
1120
|
width: number,
|
|
1121
1121
|
truncate: (s: string, width: number) => string,
|
|
1122
1122
|
fg: (color: string, value: string) => string,
|
|
1123
|
-
options: { expandedSections?: Set<string>; logTailRows?: number; minRows?: number; bottomFooter?: boolean } = {},
|
|
1123
|
+
options: { expandedSections?: Set<string>; logTailRows?: number; minRows?: number; maxRows?: number; bottomFooter?: boolean } = {},
|
|
1124
1124
|
): string[] {
|
|
1125
1125
|
if (!detail) {
|
|
1126
1126
|
const lines = [rule("Work unavailable", width), dim(" ← back · Esc close", fg), ""];
|
|
@@ -1184,7 +1184,9 @@ function buildDetailLines(
|
|
|
1184
1184
|
const wrapped = /log/i.test(detail.evidence.label)
|
|
1185
1185
|
? wrapLogText(body, width - 6)
|
|
1186
1186
|
: wrapEvidenceText(body, width - 6);
|
|
1187
|
-
|
|
1187
|
+
let shown = tailEvidence ? wrapped.slice(-tailRows) : wrapped;
|
|
1188
|
+
// Metadata above always renders; a tail that outgrows the remaining rows drops its oldest rows so the newest stay on screen (+1 for the trailing blank).
|
|
1189
|
+
if (tailEvidence && options.maxRows !== undefined) shown = shown.slice(-Math.max(1, options.maxRows - lines.length - 1));
|
|
1188
1190
|
for (const raw of shown) lines.push(raw ? ` ${raw}` : " ");
|
|
1189
1191
|
}
|
|
1190
1192
|
lines.push("");
|