pisesh 0.1.11 → 0.1.12

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/CHANGELOG.md +6 -0
  2. package/bin/pisesh +43 -12
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.1.12] - 2026-07-20
6
+
7
+ ### Fixed
8
+ - Skip interrupted tool calls from assistant turns that ended with `error` or `aborted`, preventing OpenAI Responses from receiving an unmatched `function_call_output` after resume or fork.
9
+ - Remove incompatible synthetic results previously written by pisesh and repair their child links when affected sessions are resumed.
10
+
5
11
  ## [0.1.11] - 2026-07-14
6
12
 
7
13
  ### Fixed
package/bin/pisesh CHANGED
@@ -843,25 +843,54 @@ function tryCopy(text) {
843
843
  // interrupted when the prior session hit a usage limit or was closed mid-tool.
844
844
  // Left unhealed, the spawned pi crashes (exit 1), which surfaces to the parent
845
845
  // as "pisesh exited with code N". We inject a synthetic "[interrupted]"
846
- // toolResult for every orphaned toolCall so the resumed history is always
847
- // well-formed and pi never re-fires a dead tool. Idempotent; never throws.
846
+ // toolResult for unfinished valid turns, but not errored/aborted assistant turns
847
+ // that pi omits during replay. Old incompatible synthetic results are removed.
848
+ // Idempotent; never throws.
848
849
  function healOrphanedToolCalls(file) {
849
850
  try {
850
851
  const lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
852
+ const entries = lines.map(raw => {
853
+ try { return { raw, value: JSON.parse(raw) }; } catch { return { raw, value: null }; }
854
+ });
855
+ const droppedToolCallIds = new Set();
856
+ for (const { value: o } of entries) {
857
+ const m = o && o.message;
858
+ if (m && m.role === 'assistant' && (m.stopReason === 'error' || m.stopReason === 'aborted')) {
859
+ for (const b of Array.isArray(m.content) ? m.content : []) {
860
+ if (b && b.type === 'toolCall' && b.id) droppedToolCallIds.add(b.id);
861
+ }
862
+ }
863
+ }
864
+
865
+ const removedParents = new Map();
866
+ const kept = entries.filter(({ value: o }) => {
867
+ const m = o && o.message;
868
+ const text = m && Array.isArray(m.content) && m.content.find(b => b && b.type === 'text');
869
+ const isBadSynthetic = m && m.role === 'toolResult'
870
+ && droppedToolCallIds.has(m.toolCallId)
871
+ && text && text.text.startsWith('[tool call interrupted');
872
+ if (isBadSynthetic) removedParents.set(o.id, o.parentId);
873
+ return !isBadSynthetic;
874
+ });
875
+ for (const { value: o } of kept) {
876
+ if (!o || typeof o.parentId === 'undefined') continue;
877
+ while (removedParents.has(o.parentId)) o.parentId = removedParents.get(o.parentId);
878
+ }
879
+
851
880
  const resultIds = new Set();
852
- for (const l of lines) {
853
- let o; try { o = JSON.parse(l); } catch { continue; }
881
+ for (const { value: o } of kept) {
854
882
  if (o && o.message && o.message.role === 'toolResult') resultIds.add(o.message.toolCallId);
855
883
  }
856
884
  const rnd = () => Math.random().toString(16).slice(2, 10);
857
885
  const out = [];
858
886
  let inserted = 0, prevSynthId = null;
859
- for (const l of lines) {
860
- let o; try { o = JSON.parse(l); } catch { out.push(l); continue; }
861
- if (prevSynthId && o && typeof o.parentId !== 'undefined') { o.parentId = prevSynthId; prevSynthId = null; }
887
+ for (const { raw, value: o } of kept) {
888
+ if (!o) { out.push(raw); continue; }
889
+ if (prevSynthId && typeof o.parentId !== 'undefined') { o.parentId = prevSynthId; prevSynthId = null; }
862
890
  out.push(JSON.stringify(o));
863
- const c = o && o.message && o.message.content;
864
- if (o && o.message && o.message.role === 'assistant' && Array.isArray(c)) {
891
+ const m = o.message;
892
+ const c = m && m.content;
893
+ if (m && m.role === 'assistant' && m.stopReason !== 'error' && m.stopReason !== 'aborted' && Array.isArray(c)) {
865
894
  for (const b of c) {
866
895
  if (b && b.type === 'toolCall' && b.id && !resultIds.has(b.id)) {
867
896
  const synth = {
@@ -881,11 +910,12 @@ function healOrphanedToolCalls(file) {
881
910
  }
882
911
  }
883
912
  }
884
- if (inserted > 0) {
913
+ const changed = removedParents.size + inserted;
914
+ if (changed > 0) {
885
915
  try { const bak = file + '.bak-orphanheal'; if (!fs.existsSync(bak)) fs.copyFileSync(file, bak); } catch {}
886
916
  fs.writeFileSync(file, out.join('\n') + '\n');
887
917
  }
888
- return inserted;
918
+ return changed;
889
919
  } catch { return 0; }
890
920
  }
891
921
 
@@ -1010,4 +1040,5 @@ function main() {
1010
1040
  render();
1011
1041
  }
1012
1042
 
1013
- main();
1043
+ if (require.main === module) main();
1044
+ else module.exports = { healOrphanedToolCalls };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pisesh",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Bookmark, search, and resume pi coding-agent sessions with a fast keyboard-driven TUI.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -46,7 +46,7 @@
46
46
  "image": "https://raw.githubusercontent.com/Blue-B/pisesh/main/assets/preview.png"
47
47
  },
48
48
  "scripts": {
49
- "test": "node --check bin/pisesh && echo 'pisesh: syntax OK'",
49
+ "test": "node --check bin/pisesh && node --test test/*.test.js",
50
50
  "prepublishOnly": "npm test"
51
51
  }
52
52
  }